Синтаксический дизайн. Зачем использовать круглые скобки, если аргументы не передаются?

65

Во многих языках синтаксис function_name(arg1, arg2, ...)используется для вызова функции. Когда мы хотим вызвать функцию без каких-либо аргументов, мы должны это сделать function_name().

Я нахожу странным, что компилятор или интерпретатор сценариев потребовал ()бы успешного обнаружения его как вызова функции. Если известно, что переменная может быть вызвана, почему этого function_name;недостаточно?

С другой стороны, на некоторых языках мы можем сделать: function_name 'test';или даже function_name 'first' 'second';вызвать функцию или команду.

Я думаю, что круглые скобки были бы лучше, если бы они были необходимы только для объявления порядка приоритета, а в других местах были необязательными. Например, делать if expression == true function_name;должно быть так же, как if (expression == true) function_name();.

Самое неприятное, на мой взгляд, это делать, 'SOME_STRING'.toLowerCase()когда явно не нужны аргументы для функции-прототипа. Почему дизайнеры решили против простого 'SOME_STRING'.lower?

Отказ от ответственности: не поймите меня неправильно, я люблю C-подобный синтаксис! ;) Я просто спрашиваю, может ли это быть лучше. Есть ли у требования ()преимущества в производительности или облегчает понимание кода? Мне действительно любопытно, в чем именно причина.

Дэвид Рефуа
источник
103
Что бы вы сделали, если бы хотели передать функцию в качестве аргумента другой функции?
Винсент Савард
30
Пока код необходим для чтения людьми, удобочитаемость - это главное.
Тулаинс Кордова
75
Это вопрос читабельности, спросите вы (), и все же в вашем посте выделяется if (expression == true)утверждение. Вы беспокоитесь о лишних (), но тогда используйте лишнее == true:)
David Arno
15
Visual Basic позволил это
edc65
14
Это было обязательно в Паскале, вы использовали только парены для функций и процедур, которые принимали аргументы.
RemcoGerlich

Ответы:

250

Для языков, которые используют первоклассные функции , довольно часто синтаксис ссылки на функцию:

a = object.functionName

в то время как акт вызова этой функции:

b = object.functionName()

aв приведенном выше примере будет ссылка на вышеуказанную функцию (и вы можете вызвать ее, выполнив a()), а bбудет содержать возвращаемое значение функции.

Хотя некоторые языки могут делать вызовы функции без скобок, это может привести к путанице , являются ли они вызов функции, или просто обращаясь к функции.

Натан Меррилл
источник
9
Вы можете упомянуть, что эта разница интересна только при наличии побочных эффектов - для чистой функции это не имеет значения
Bergi
73
@ Берги: это имеет большое значение для чистых функций! Если у меня есть функция, make_listкоторая упаковывает свои аргументы в список, есть большая разница между f(make_list)передачей функции fили передачей пустого списка.
user2357112
12
@ Берджи: Даже тогда я все еще хотел бы иметь возможность передавать SATInstance.solveили другую невероятно дорогую чистую функцию другой функции, не пытаясь немедленно запустить ее. Это также делает вещи действительно неловкими, если someFunctionделает что-то другое в зависимости от того someFunction, объявлен ли он чистым. Например, если у меня есть (псевдокод) func f() {return g}, func g() {doSomethingImpure}и func apply(x) {x()}, затем apply(f)звонит f. Если я тогда заявляю, что fэто чисто, то внезапно apply(f)переходит gк apply, applyзвонит gи делает что-то нечистое.
user2357112
6
@ user2357112 Стратегия оценки не зависит от чистоты. Использование синтаксиса вызовов в качестве аннотации, когда нужно оценить, что возможно (но, вероятно, неудобно), однако это не меняет результат или его правильность. Что касается вашего примера apply(f), если gэто нечисто ( applyа также?), Тогда все это нечисто и, конечно, разрушается.
Берги
14
Примерами этого являются Python и ECMAScript, оба из которых на самом деле не имеют основной концепции «методов», вместо этого у вас есть именованное свойство, которое возвращает анонимный объект-функцию первого класса, а затем вы вызываете эту анонимную функцию первого класса. В частности, и в Python, и в ECMAScript выражение foo.bar()не является одной операцией «вызов метода barобъекта foo», а скорее двумя операциями: «поле разыменования barобъекта foo и затем вызов объекта, возвращенного из этого поля». Таким образом, .это оператор поля селектора и ()является оператором вызова , и они независимы.
Йорг Миттаг
62

Действительно, Scala позволяет это, хотя и существует соглашение: если метод имеет побочные эффекты, в любом случае следует использовать круглые скобки.

Как писатель компилятора, я считаю, что наличие круглых скобок довольно удобно; Я всегда знал бы, что это вызов метода, и мне не пришлось бы встраивать бифуркацию для нечетного случая.

Как программист и читатель кода, наличие круглых скобок не оставляет сомнений в том, что это вызов метода, даже если параметры не передаются.

Передача параметров не является единственной определяющей характеристикой вызова метода. Почему я должен относиться к методу без параметров, который отличается от метода с параметрами?

Роберт Харви
источник
Спасибо, и вы дали веские причины, почему это важно. Не могли бы вы также привести несколько примеров того, почему разработчики языка должны использовать функции в прототипах, пожалуйста?
Дэвид Рефуа
наличие скобок не оставляет сомнений в том, что это вызов метода, с которым я полностью согласен. Я предпочитаю, чтобы мои языки программирования были более явными, чем неявными.
Кори Огберн
20
Scala на самом деле немного более тонкая, чем это: в то время как другие языки допускают только один список параметров с нулевым или большим количеством параметров, Scala допускает ноль или более списков параметров с нулевым или большим количеством параметров. Итак, def foo()это метод с одним пустым списком параметров, и def fooэто метод без списка параметров, и это разные вещи ! Как правило, метод без списка параметров должен вызываться без списка аргументов, а метод с пустым списком параметров должен вызываться с пустым списком аргументов. Вызов метода без списка параметров с пустым аргументом на самом деле ...
Йорг W Mittag
19
… Интерпретируется как вызов applyметода объекта, возвращаемого вызовом метода с пустым списком аргументов, тогда как вызов метода с пустым списком параметров без списка аргументов может в зависимости от контекста интерпретироваться по-разному как вызов метода с пустым списком аргументов, η-расширение в частично примененный метод (т. е. «справочник по методу»), иначе он может не скомпилироваться вообще. Кроме того, Scala следует принципу унифицированного доступа, не разделяя (синтаксически) различие между вызовом метода без списка аргументов и ссылкой на поле.
Йорг Миттаг
3
Даже когда я начинаю ценить Руби за отсутствие требуемой «церемонии» - скобок, скобок, явного типа - я могу сказать, что метод-скобка читается быстрее ; но однажды знакомый идиоматичный Ruby вполне читабелен и определенно быстрее пишет.
радаробоб
19

На самом деле это довольно тонкая случайность выбора синтаксиса. Я буду говорить с функциональными языками, которые основаны на типизированном лямбда-исчислении.

В указанных языках каждая функция имеет ровно один аргумент . То, что мы часто называем «множественными аргументами», на самом деле является единственным параметром типа продукта. Так, например, функция, которая сравнивает два целых числа:

leq : int * int -> bool
leq (a, b) = a <= b

занимает одну пару целых чисел. Скобки не обозначают параметры функции; они используются для сопоставления с аргументом. Чтобы убедить вас, что это действительно один аргумент, мы можем вместо этого использовать проективные функции вместо сопоставления с образцом для деконструкции пары:

leq some_pair = some_pair.1 <= some_pair.2

Таким образом, круглые скобки действительно являются удобством, которое позволяет нам сопоставлять шаблоны и сохранять некоторые данные. Они не обязательны.

Как насчет функции, которая якобы не имеет аргументов? Такая функция на самом деле имеет домен Unit. Одиночный член Unitобычно пишется как (), поэтому и появляются круглые скобки.

say_hi : Unit -> string
say_hi a = "Hi buddy!"

Чтобы вызвать эту функцию, мы должны применить ее к значению типа Unit, которое должно быть (), поэтому мы заканчиваем писать say_hi ().

Таким образом, действительно нет такой вещи, как список аргументов!

gardenhead
источник
3
И именно поэтому пустые скобки ()напоминают ложки минус ручку.
Миндвин
3
Определение leqфункции, которая использует, <а не <=довольно запутанно!
KRyan
5
Этот ответ может быть легче понять, если использовать слово «кортеж» в дополнение к фразам типа «тип продукта».
Кевин
Большинство формализмов трактуют int f (int x, int y) как функцию от I ^ 2 до I. Лямбда-исчисление отличается, он не использует этот метод для сопоставления с тем, что в других формализмах является высокой арностью. Вместо лямбда-исчисления используется карри. Сравнение будет x -> (y -> (x <= y)). (x -> (y -> (x <= y)) 2 будет оцениваться в (y-> 2 <= y) и (x -> (y -> (x <= y)) 2 3 будет оцениваться в ( у-> 2 <= у) 3 , который в свою очередь , принимает значение 2 <= 3.
Taemyr
1
@PeriataBreatta Я не знаком с историей использования ()для представления устройства. Я не удивлюсь, если хотя бы одна из причин будет напоминать «функцию без параметров». Однако есть и другое возможное объяснение синтаксиса. В алгебре типов Unitфактически есть тождество для типов товаров. Бинарный продукт записывается как (a,b), мы могли бы написать унарный продукт как (a), поэтому логическим расширением было бы написать нулевой продукт так же просто ().
садовник
14

Например, в Javascript использование имени метода без () возвращает саму функцию без ее выполнения. Таким образом, вы можете, например, передать функцию в качестве аргумента другому методу.

В Java был сделан выбор, что идентификатор, за которым следует () или (...), означает вызов метода, а идентификатор без () ссылается на переменную-член. Это может улучшить читабельность, так как вы не сомневаетесь, имеете ли вы дело с методом или переменной-членом. Фактически, одно и то же имя может использоваться как для метода, так и для переменной-члена, оба будут доступны с соответствующим синтаксисом.

Флориан Ф
источник
4
+1 Пока человек должен читать код, читаемость - это главное.
Тулаинс Кордова
1
@ TulainsCórdova Я не уверен, что Perl с тобой согласен.
Рашит
@Racheet: Perl не может, но Perl Best Practicesделает
slebetman
1
@Racheet Perl очень читабелен, если он написан для чтения. То же самое касается практически всех неэзотерических языков. Короче говоря, лаконичный Perl очень удобен для чтения программистами Perl, точно так же, как код Scala или Haskell читается людьми, знакомыми с этими языками. Я также могу говорить с вами по-немецки очень запутанным образом, что грамматически совершенно правильно, но вы все равно не поймете слова, даже если вы владеете немецким языком. Это тоже не вина Германии. ;)
simbabque
в Java это не «идентифицировать» метод. Это вызывает это. Object::toStringопределяет метод.
njzk2
10

Синтаксис следует семантике, поэтому давайте начнем с семантики:

Каковы способы использования функции или метода?

Есть, на самом деле, несколько способов:

  • функция может быть вызвана с аргументами или без
  • функция может рассматриваться как значение
  • функция может быть применена частично (создание замыкания, принимающего как минимум на один аргумент меньше и закрывающего переданные аргументы)

Наличие схожего синтаксиса для разных целей - лучший способ создать либо неоднозначный язык, либо, по крайней мере, запутанный (и у нас его достаточно).

В C и C-подобных языках:

  • вызов функции выполняется с помощью круглых скобок для включения аргументов (их может не быть), как в func()
  • обработка функции как значения может быть сделана простым использованием ее имени как в &func(C создает указатель на функцию)
  • в некоторых языках у вас есть сокращенные синтаксисы для частичного применения; Java позволяет, someVariable::someMethodнапример (ограничено приемником метода, но все еще полезно)

Обратите внимание, что каждое использование имеет свой синтаксис, что позволяет легко их различать.

Матье М.
источник
2
Ну, «легко» - это одна из тех «ясно, только если ее уже поняли» ситуации. Начинающему вполне справедливо будет отметить, что отнюдь не очевидно без объяснения, почему некоторые знаки препинания имеют значение, которое они имеют.
Эрик Липперт
2
@EricLippert: Действительно, легко не обязательно означает интуитивно. Хотя в этом случае я бы отметил, что круглые скобки также используются для вызова функции в математике. При этом я обнаружил, что новички во многих языках больше борются с понятиями / семантикой, чем с синтаксисом в целом.
Матье М.
Этот ответ путает различие между «карри» и «частичным применением». ::Оператор в Java является частичным оператором приложения, а не оператор Выделки. Частичное приложение - это операция, которая принимает функцию и одно или несколько значений и возвращает функцию, принимающую меньшее количество значений. Карринг действует только на функции, а не на значения.
Периата Breatta
@PeriataBreatta: Хороший вопрос, исправлено.
Матье М.
8

Ни один из других ответов не пытался решить вопрос: сколько должно быть избыточности в дизайне языка? Потому что даже если вы можете разработать язык так, чтобы x = sqrt yустановить x в квадратный корень из y, это не означает, что вы обязательно должны это делать.

В языке без избыточности каждая последовательность символов означает что-то, что означает, что если вы допустите одну ошибку, вы не получите сообщение об ошибке, ваша программа будет делать не то, что может сильно отличаться от того, что вы предназначено и очень сложно для отладки. (Как знает любой, кто работал с регулярными выражениями.) Небольшая избыточность - это хорошо, потому что она позволяет обнаруживать многие из ваших ошибок, и чем больше избыточность, тем выше вероятность того, что диагностика будет точной. Сейчас, конечно, вы можете зайти слишком далеко (никто из нас не хочет писать на языке COBOL в наши дни), но есть правильный баланс.

Избыточность также помогает удобочитаемости, потому что есть больше подсказок. Английский без избыточности будет очень трудно читать, и то же самое можно сказать о языках программирования.

Майкл Кей
источник
В некотором смысле я согласен: языки программирования должны иметь механизм «безопасности». Но я не согласен с тем, что «небольшая избыточность» в синтаксисе является хорошим способом решения этой проблемы - это типичный пример , и в основном он вносит шум, который затрудняет обнаружение ошибок и открывает возможности для синтаксических ошибок, которые отвлекают от настоящие ошибки Тип многословия, который помогает удобочитаемости, имеет мало общего с синтаксисом, больше с соглашениями об именах . И система безопасности, безусловно, наиболее эффективно реализуется как система строгого типа , в которой большинство случайных изменений приведут к неправильной типизации программы.
оставил около
@leftaroundabout: мне нравится думать о решениях по языку с точки зрения расстояния Хэмминга. Если две конструкции имеют разные значения, часто бывает полезно, чтобы они отличались как минимум двумя способами и имели форму, отличающуюся только одним способом, чтобы генерировать шум компилятора. Разработчик языка, как правило, не может нести ответственность за то, чтобы идентификаторы были легко различимы, но если, например, требуется присваивание :=и сравнение ==, и =его можно использовать только для инициализации во время компиляции, то вероятность опечаток, превращающих сравнения в назначения, будет значительно уменьшена.
суперкат
@leftaroundabout: Точно так же, если бы я разрабатывал язык, мне, вероятно, потребовался бы ()вызов функции с нулевым аргументом, но также потребовался бы токен, чтобы «взять адрес» функции. Первое действие встречается гораздо чаще, поэтому сохранение токена в менее распространенном случае не так уж и полезно.
суперкат
@supercat: ну, опять же, я думаю, что это решает проблему на неправильном уровне. Присвоение и сравнение являются концептуально различными операциями, равно как и оценка функции и получение адреса функции, соответственно. Следовательно, они должны иметь четко различимые типы, тогда больше не имеет значения, имеют ли операторы небольшое расстояние Хемминга, потому что любая опечатка будет ошибкой типа во время компиляции.
оставил около
@leftaroundabout: если присваивается переменная логического типа, или если возвращаемый тип функции сам по себе является указателем на функцию (или - в некоторых языках - фактической функцией), заменяя сравнение на присвоение, или вызов функции со ссылкой на функцию может привести к программе, которая синтаксически допустима, но имеет совершенно другое значение по сравнению с исходной версией. В общем случае проверка типов найдет несколько таких ошибок, но лучше выделить синтаксис.
суперкат
7

На языке с побочными эффектами IMO действительно полезно различать (теоретически без побочных эффектов) чтение переменной

variable

и вызов функции, которая может вызвать побочные эффекты

launch_nukes()

OTOH, если нет никаких побочных эффектов (кроме тех, которые закодированы в системе типов), тогда нет никакого смысла даже различать чтение переменной и вызов функции без аргументов.

Даниэль Жур
источник
1
Обратите внимание, что OP представил случай, когда объект является единственным аргументом и представлен перед именем функции (что также обеспечивает область действия для имени функции). noun.verbсинтаксис может быть таким же понятным по смыслу, как noun.verb()и немного менее загроможденным. Точно так же функция, member_возвращающая левую ссылку, может предложить дружественный синтаксис, чем обычная функция-установщик: obj.member_ = new_valuevs. obj.set_member(new_value)( _постфикс - это подсказка, говорящая «выставлено», напоминающее _ префиксы для «скрытых» функций).
Пол А. Клейтон
1
@ PaulA.Clayton Я не понимаю, как это не охватывается моим ответом: если noun.verbозначает вызов функции, как вы отличаете ее от простого доступа к члену?
Даниэль Жур
1
Относительно того, что чтение переменных теоретически не имеет побочных эффектов, я просто хочу сказать следующее: всегда остерегайтесь ложных друзей .
Джастин Тайм 2 Восстановите Монику
5

В большинстве случаев это синтаксический выбор грамматики языка. Для грамматики полезно, чтобы различные индивидуальные конструкции были (относительно) однозначными, когда все они взяты вместе. (Если есть неоднозначности, как в некоторых объявлениях C ++, должны быть определенные правила для разрешения.) У компилятора нет широты для угадывания; необходимо следовать спецификации языка.


Visual Basic в различных формах различает процедуры, которые не возвращают значение, и функции.

Процедуры должны быть вызваны как операторы, и для них не нужны парены, просто аргументы, разделенные запятыми, если они есть. Функции должны быть вызваны как часть выражения и требуют скобок.

Это относительно ненужное различие, которое делает ручной рефакторинг между двумя формами более болезненным, чем должно быть.

(С другой стороны , Visual Basic использует те же скобки ()для ссылок на массивы как для вызовов функций, поэтому ссылки на массив выглядят вызовы функций. И это облегчает ручную рефакторинг массива в вызов функции! Таким образом, мы могли бы подумать другие языки используют []«ю.ш. для ссылок на массивы, но я отвлекся ...)


В языках C и C ++ доступ к содержимому переменных осуществляется автоматически с использованием их имени, и если вы хотите обратиться к самой переменной, а не к ее содержимому, вы применяете унарный &оператор.

Этот вид механизма также может быть применен к именам функций. Необработанное имя функции может подразумевать вызов функции, тогда как унарный &оператор будет использоваться для ссылки на функцию (саму) как на данные. Лично мне нравится идея доступа к функциям без аргументов без побочных эффектов с тем же синтаксисом, что и для переменных.

Это вполне правдоподобно (как и другие синтаксические варианты для этого).

Эрик Эйдт
источник
2
Конечно, хотя в Visual Basic отсутствие скобок для вызовов процедур является ненужным различием по сравнению с вызовами функций, добавление к ним необходимых скобок было бы ненужным различием между операторами , многие из которых в языке, основанном на BASIC, имеют эффекты, которые очень напоминает процедуры. Прошло также много времени с тех пор, как я выполнил какую-либо работу в версии MS BASIC, но не поддерживает ли она синтаксис call <procedure name> (<arguments>)? Я уверен, что это был верный способ использования процедур, когда я занимался программированием QuickBASIC в другой жизни ...
Periata Breatta
1
@PeriataBreatta: VB все еще разрешает синтаксис «вызова», и иногда требует его в случаях, когда вызываемая вещь является выражением [например, можно сказать «Call If (someCondition, Delegate1, Delegate2) (Arguments)», но прямой вызов результата оператора «If» не будет действительным как отдельное утверждение].
суперкат
@PeriataBreatta Мне понравились вызовы процедур без скобок в VB6, потому что они заставляли DSL-подобный код выглядеть хорошо.
Марк Херд
@supercat В LinqPad удивительно, как часто мне нужно использовать Callметод, который мне почти никогда не нужен в «нормальном» рабочем коде.
Марк Херд
5

Согласованность и удобочитаемость.

Если я узнаю , что вы вызываете функцию Xвроде этого: X(arg1, arg2, ...), то я ожидаю , что это работает так же , как для каких - либо аргументов: X().

Теперь в то же время я узнаю, что вы можете определить переменную как некоторый символ и использовать ее следующим образом:

a = 5
b = a
...

Что бы я подумал, когда найду это?

c = X

Твоя догадка так же хороша как и моя. В нормальных условиях Xпеременная. Но если бы мы пошли по твоему пути, это тоже могло бы стать функцией! Должен ли я вспомнить, соответствует ли какой символ какой группе (переменным / функциям)?

Мы могли бы наложить искусственные ограничения, например «Функции начинаются с заглавной буквы. Переменные начинаются с строчной буквы», но это излишне и усложняет задачу, хотя иногда может помочь некоторым целям разработки.

Примечание 1: Другие ответы полностью игнорируют еще одну вещь: язык может позволить вам использовать одно и то же имя как для функции, так и для переменной, и различать по контексту. Смотрите Common Lispв качестве примера. Функция xи переменная xпрекрасно сосуществуют.

Побочное примечание 2: Принятый ответ показывает нам синтаксис: object.functionname. Во-первых, он не универсален для языков с первоклассными функциями. Во-вторых, как программист, я бы отнесся к этому как к дополнительной информации: functionnameпринадлежит object. Является ли objectобъект, класс или пространство имен не имеет большого значения, но он говорит мне, что он где-то принадлежит . Это означает, что вы должны либо добавить искусственный синтаксис object.для каждой глобальной функции, либо создать такой, objectчтобы он содержал все глобальные функции.

И в любом случае, вы теряете возможность иметь отдельные пространства имен для функций и переменных.

MatthewRock
источник
Ага. Последовательность - это достаточно веская причина, полная остановка (не то, что другие ваши пункты не действительны - они есть).
Мэтью Прочитал
4

Чтобы добавить к другим ответам, возьмите этот пример C:

void *func_factory(void)
{
        return 0;
}

void *(*ff)(void);

void example()
{
        ff = func_factory;
        ff = func_factory();
}

Если оператор вызова был необязательным, не было бы никакой возможности различить назначение функции и вызов функции.

Это еще более проблематично в языках, в которых отсутствует система типов, например, JavaScript, где вывод типа не может использоваться для определения того, что является и не является функцией.

Марк К Коуэн
источник
3
JavaScript не имеет системы типов. В нем отсутствуют объявления типов . Но он полностью осознает разницу между разными типами стоимости.
Периата Breatta
1
Periata Breatta: дело в том, что переменные и свойства Java могут содержать типы любого значения, поэтому вы не всегда можете знать, является ли это функцией во время чтения кода.
reinierpost
Например, что делает эта функция? function cross(a, b) { return a + b; }
Марк К Коуэн
@MarkKCowan Это следует из этого: ecma-international.org/ecma-262/6.0/…
curiousdannii