Diseño de sintaxis: ¿por qué usar paréntesis cuando no se pasan argumentos?

66

En muchos idiomas, la sintaxis function_name(arg1, arg2, ...)se usa para llamar a una función. Cuando queremos llamar a la función sin ningún argumento, debemos hacerlo function_name().

Me resulta extraño que un compilador o un intérprete de script requiera ()detectarlo con éxito como una llamada de función. Si se sabe que una variable es invocable, ¿por qué no function_name;sería suficiente?

Por otro lado, en algunos idiomas podemos hacer: function_name 'test';o incluso function_name 'first' 'second';llamar a una función o un comando.

Creo que los paréntesis habrían sido mejores si solo fueran necesarios para declarar el orden de prioridad, y en otros lugares eran opcionales. Por ejemplo, hacer if expression == true function_name;debe ser tan válido como if (expression == true) function_name();.

Lo más molesto en mi opinión es hacerlo 'SOME_STRING'.toLowerCase()cuando claramente la función prototipo no necesita argumentos. ¿Por qué los diseñadores decidieron en contra de lo más simple 'SOME_STRING'.lower?

Descargo de responsabilidad: ¡No me malinterpreten, me encanta la sintaxis tipo C! ;) Solo estoy preguntando si podría ser mejor. ¿El hecho de requerir ()tiene ventajas de rendimiento o facilita la comprensión del código? Tengo mucha curiosidad sobre cuál es exactamente la razón.

David Refoua
fuente
103
¿Qué haría si quisiera pasar una función como argumento a otra función?
Vincent Savard,
30
Mientras los humanos necesiten leer el código, la legibilidad es el rey.
Tulains Córdova
75
Eso es lo que tiene legibilidad, usted pregunta (), pero lo que destaca en su publicación es la if (expression == true)declaración. Te preocupas por lo superfluo (), pero luego usa un superfluo == true:)
David Arno
15
Visual Basic permitió esto
edc65
14
Era obligatorio en Pascal, solo usabas parens para funciones y procedimientos que tomaban argumentos.
RemcoGerlich

Respuestas:

251

Para los lenguajes que usan funciones de primera clase , es bastante común que la sintaxis de referirse a una función sea:

a = object.functionName

mientras que el acto de llamar a esa función es:

b = object.functionName()

aen el ejemplo anterior sería una referencia a la función anterior (y podría llamarla haciendo a()), mientras bque contendría el valor de retorno de la función.

Si bien algunos lenguajes pueden realizar llamadas a funciones sin paréntesis, puede resultar confuso si llaman a la función o simplemente se refieren a la función.

Nathan Merrill
fuente
9
Es posible que desee mencionar que esta diferencia solo es interesante en presencia de efectos secundarios: para una función pura no importa
Bergi el
73
@ Bergi: ¡Importa mucho para las funciones puras! Si tengo una función make_listque agrupa sus argumentos en una lista, hay una gran diferencia entre f(make_list)pasar una función fo pasar una lista vacía.
user2357112
12
@Bergi: Incluso entonces, todavía me gustaría poder pasar SATInstance.solveo alguna otra función pura increíblemente costosa a otra función sin intentar ejecutarla de inmediato. También hace que las cosas sean realmente incómodas si someFunctionhace algo diferente dependiendo de si someFunctionse declara que es puro. Por ejemplo, si tengo (pseudocódigo) func f() {return g}, func g() {doSomethingImpure}y func apply(x) {x()}, a continuación, apply(f)las llamadas f. Si luego declaro que fes puro, de repente apply(f)pasa ga apply, y applyllama gy hace algo impuro.
user2357112
66
@ user2357112 La estrategia de evaluación es independiente de la pureza. Usar la sintaxis de las llamadas como una anotación cuando se evalúa lo que es posible (pero probablemente incómodo), sin embargo, no cambia el resultado o su corrección. Con respecto a su ejemplo apply(f), si ges impuro (¿y applytambién?), Entonces todo es impuro y se descompone, por supuesto.
Bergi
14
Ejemplos de esto son Python y ECMAScript, que en realidad no tienen un concepto central de "métodos", sino que tiene una propiedad con nombre que devuelve un objeto de función anónimo de primera clase y luego llama a esa función anónima de primera clase. Específicamente, tanto en Python como en ECMAScript, decir foo.bar()no es una operación "llamar método barde objeto foo" sino más bien dos operaciones, "desreferenciar campo barde objeto foo y luego llamar al objeto devuelto desde ese campo". Entonces, .es el operador de selector de campo y ()es el operador de llamada y son independientes.
Jörg W Mittag
63

De hecho, Scala permite esto, aunque hay una convención que se sigue: si el método tiene efectos secundarios, los paréntesis deben usarse de todos modos.

Como escritor compilador, la presencia garantizada de paréntesis me parece bastante conveniente; Siempre sabría que es una llamada al método, y no tendría que construir una bifurcación para el caso extraño.

Como programador y lector de código, la presencia de paréntesis no deja dudas de que es una llamada a un método, aunque no se pasan parámetros.

El paso de parámetros no es la única característica definitoria de una llamada a método. ¿Por qué trataría un método sin parámetros diferente de un método que tiene parámetros?

Robert Harvey
fuente
Gracias, y dio razones válidas de por qué esto es importante. ¿También puede dar algunos ejemplos de por qué los diseñadores de idiomas deberían usar funciones en prototipos, por favor?
David Refoua
la presencia de paréntesis no deja dudas de que es una llamada al método , estoy completamente de acuerdo. Prefiero mis lenguajes de programación a más explícitos que implícitos.
Corey Ogburn
20
Scala es en realidad un poco más sutil que eso: mientras que otros idiomas solo permiten una lista de parámetros con cero o más parámetros, Scala permite cero o más listas de parámetros con cero o más parámetros. Por lo tanto, def foo()es un método con una lista de parámetros vacía, y def fooes un método sin lista de parámetros, ¡ y los dos son cosas diferentes ! En general, un método sin lista de parámetros debe llamarse sin lista de argumentos y un método con una lista de parámetros vacía debe llamarse con una lista de argumentos vacía. Llamar a un método sin lista de parámetros con un argumento vacío es en realidad ...
Jörg W Mittag el
19
… Interpretado como llamar al applymétodo del objeto devuelto por la llamada al método con una lista de argumentos vacía, mientras que llamar a un método con una lista de parámetros vacía sin una lista de argumentos puede, según el contexto, interpretarse de diversas maneras como llamar al método con una lista de argumentos vacía, η-expansión en un método parcialmente aplicado (es decir, "referencia de método"), o podría no compilarse en absoluto. Además, Scala sigue el Principio de acceso uniforme, al no distinguir (sintácticamente) entre llamar a un método sin una lista de argumentos y hacer referencia a un campo.
Jörg W Mittag el
3
Incluso cuando llego a apreciar a Ruby por la ausencia de la "ceremonia" requerida (parens, llaves, tipo explícito), puedo decir que el método-paréntesis se lee más rápido ; pero una vez que el idiomático familiar Ruby es bastante legible y definitivamente más rápido de escribir.
radarbob
19

Esto es realmente una casualidad bastante sutil de opciones de sintaxis. Hablaré idiomas funcionales, que se basan en el cálculo lambda escrito.

En dichos idiomas, cada función tiene exactamente un argumento . Lo que a menudo pensamos como "argumentos múltiples" es en realidad un único parámetro del tipo de producto. Entonces, por ejemplo, la función que compara dos enteros:

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

toma un solo par de enteros. Los paréntesis no denotan los parámetros de la función; se usan para emparejar patrones con el argumento. Para convencerlo de que este es realmente un argumento, podemos usar las funciones proyectivas en lugar de la coincidencia de patrones para deconstruir el par:

leq some_pair = some_pair.1 <= some_pair.2

Por lo tanto, los paréntesis son realmente una conveniencia que nos permite combinar patrones y guardar algunos tipos de escritura. No son obligatorios

¿Qué pasa con una función que aparentemente no tiene argumentos? Tal función en realidad tiene dominio Unit. El miembro único de Unitgeneralmente se escribe como (), por eso aparecen los paréntesis.

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

Para llamar a esta función, tendríamos que aplicarla a un valor de tipo Unit, que debe ser (), por lo que terminamos escribiendo say_hi ().

Entonces, ¡realmente no existe una lista de argumentos!

cabeza de jardín
fuente
3
Y es por eso que los paréntesis vacíos se ()parecen a cucharas menos el mango.
Mindwin
3
¡Definir una leqfunción que usa en <lugar de <=es bastante confuso!
KRyan
55
Esta respuesta podría ser más fácil de entender si usara la palabra "tupla" además de frases como "tipo de producto".
Kevin
La mayoría de los formalismos tratarán int f (int x, int y) como una función de I ^ 2 a I. El cálculo de Lambda es diferente, no utiliza este método para igualar lo que en otros formalismos es de alta aridad. En cambio, el cálculo lambda usa curry. La comparación sería x -> (y -> (x <= y)). (x -> (y -> (x <= y)) 2 evaluaría a (y-> 2 <= y) y (x -> (y -> (x <= y)) 2 3 evaluaría a ( y-> 2 <= y) 3 que a su vez se evalúa como 2 <= 3.
Taemyr
1
@PeriataBreatta No estoy familiarizado con el historial de uso ()para representar la unidad. No me sorprendería que al menos parte de la razón fuera asemejarse a una "función sin parámetros". Sin embargo, hay otra posible explicación para la sintaxis. En el álgebra de tipos, Unites de hecho la identidad de los tipos de productos. Un producto binario se escribe como (a,b), podríamos escribir un producto unario como (a), por lo que una extensión lógica sería escribir el producto nulo simplemente ().
cabeza de jardín
14

En Javascript, por ejemplo, usar un nombre de método sin () devuelve la función misma sin ejecutarla. De esta manera, por ejemplo, puede pasar la función como argumento a otro método.

En Java, se eligió que un identificador seguido de () o (...) significa una llamada al método, mientras que un identificador sin () se refiere a una variable miembro. Esto podría mejorar la legibilidad, ya que no tiene dudas de si se trata de un método o una variable miembro. De hecho, se puede usar el mismo nombre tanto para un método como para una variable miembro, ambos serán accesibles con su respectiva sintaxis.

Florian F
fuente
44
+1 Mientras los humanos necesiten leer el código, la legibilidad es el rey.
Tulains Córdova
1
@ TulainsCórdova No estoy seguro de que Perl esté de acuerdo con usted.
Racheet
@Racheet: Perl puede que no, pero lo Perl Best Practiceshace
slebetman
1
@Racheet Perl es muy legible si está escrito para ser legible. Eso es lo mismo para casi todos los idiomas no esotéricos. El Perl corto y conciso es muy legible para los programadores de Perl, al igual que el código Scala o Haskell es legible para las personas con experiencia en esos idiomas. También puedo hablarte alemán de una manera muy enrevesada que es gramaticalmente correcta, pero aún así no entenderás una palabra, incluso si eres competente en alemán. Eso tampoco es culpa de German. ;)
simbabque
en java no "identifica" un método. Lo llama Object::toStringidentifica un método
njzk2
10

La sintaxis sigue la semántica, así que comencemos por la semántica:

¿Cuáles son las formas de usar una función o método?

En realidad, hay varias formas:

  • Se puede llamar a una función, con o sin argumentos
  • una función puede tratarse como un valor
  • una función puede aplicarse parcialmente (creando un cierre tomando al menos un argumento menos y cerrando sobre los argumentos pasados)

Tener una sintaxis similar para diferentes usos es la mejor manera de crear un lenguaje ambiguo o, al menos, uno confuso (y tenemos suficientes de esos).

En C y lenguajes similares a C:

  • la llamada a una función se realiza mediante paréntesis para encerrar los argumentos (puede que no haya ninguno) como en func()
  • tratar una función como un valor se puede hacer simplemente usando su nombre como en &func(C creando un puntero de función)
  • en algunos idiomas, tiene sintaxis abreviada para aplicar parcialmente; Java permite, someVariable::someMethodpor ejemplo (limitado al receptor del método, pero sigue siendo útil)

Observe cómo cada uso presenta una sintaxis diferente, lo que le permite distinguirlos fácilmente.

Matthieu M.
fuente
2
Bueno, "fácilmente" es una de esas situaciones "está claro solo si ya se entiende". Un principiante estaría completamente justificado al señalar que de ninguna manera es obvio sin una explicación de por qué alguna puntuación tiene el significado que tiene.
Eric Lippert
2
@EricLippert: De hecho, fácilmente no necesariamente significa intuitivo. Aunque en este caso, señalaría que los paréntesis también se usan para la función "invocación" en matemáticas. Dicho esto, he encontrado principiantes en muchos idiomas que luchan más con conceptos / semántica que con la sintaxis en general.
Matthieu M.
Esta respuesta confunde la distinción entre "curry" y "aplicación parcial". El ::operador en Java es un operador de aplicación parcial, no un operador de currículum. La aplicación parcial es una operación que toma una función y uno o más valores y devuelve una función que acepta menos valores. El curry funciona solo con funciones, no con valores.
Periata Breatta
@PeriataBreatta: Buen punto, fijo.
Matthieu M.
8

En un lenguaje con efectos secundarios, la OMI es realmente útil para diferenciar (en teoría, sin efectos secundarios) la lectura de una variable

variable

y llamar a una función, que podría generar efectos secundarios

launch_nukes()

OTOH, si no hay efectos secundarios (aparte de los codificados en el sistema de tipos), entonces no tiene sentido incluso diferenciar entre leer una variable y llamar a una función sin argumentos.

Daniel Jour
fuente
1
Tenga en cuenta que el OP presentó el caso donde un objeto es el único argumento y se presenta antes del nombre de la función (que también proporciona un alcance para el nombre de la función). noun.verbla sintaxis podría tener un significado tan claro noun.verb()y un poco menos abarrotado. Del mismo modo, una función que member_devuelve una referencia izquierda podría ofrecer una sintaxis amigable que una función de establecimiento típica: obj.member_ = new_valuevs. obj.set_member(new_value)(el _postfix es una pista que dice "expuesto" que recuerda _ prefijos para funciones "ocultas").
Paul A. Clayton
1
@ PaulA.Clayton No veo cómo esto no está cubierto por mi respuesta: si noun.verbsignifica invocación de función, ¿cómo lo diferencia del simple acceso de miembro?
Daniel Jour
1
En lo que respecta a la lectura de las variables teóricamente libres de efectos secundarios, solo quiero decir esto: siempre tenga cuidado con los falsos amigos .
Justin Time
8

Ninguna de las otras respuestas ha intentado abordar la pregunta: ¿cuánta redundancia debería haber en el diseño de un lenguaje? Porque incluso si puede diseñar un lenguaje que x = sqrt yestablezca x en la raíz cuadrada de y, eso no significa que necesariamente deba hacerlo.

En un lenguaje sin redundancia, cada secuencia de caracteres significa algo, lo que significa que si comete un solo error, no recibirá un mensaje de error, su programa hará lo incorrecto, lo que podría ser algo muy diferente de lo que usted intencionado y muy difícil de depurar. (Como cualquiera que haya trabajado con expresiones regulares lo sabrá). Un poco de redundancia es algo bueno porque permite detectar muchos de sus errores, y cuanto más redundancia haya, más probable es que los diagnósticos sean precisos. Ahora, por supuesto, puede llevar eso demasiado lejos (ninguno de nosotros quiere escribir en COBOL en estos días), pero hay un equilibrio correcto.

La redundancia también ayuda a la legibilidad, porque hay más pistas. El inglés sin redundancia sería muy difícil de leer, y lo mismo ocurre con los lenguajes de programación.

Michael Kay
fuente
En cierto sentido, estoy de acuerdo: los lenguajes de programación deben tener un mecanismo de "red de seguridad". Pero no estoy de acuerdo con que "un poco de redundancia" en la sintaxis es una buena manera de hacerlo: eso es repetitivo , y lo que principalmente hace es introducir ruido que hace que los errores sean más difíciles de detectar y abre posibilidades para errores de sintaxis que distraen de errores reales El tipo de verbosidad que ayuda a la legibilidad tiene poco que ver con la sintaxis, más con las convenciones de nomenclatura . Y una red de seguridad se implementa de manera más eficiente como un sistema de tipo fuerte , en el que la mayoría de los cambios aleatorios harán que el programa sea incorrecto.
Leftaroundabout
@leftaroundabout: Me gusta pensar en las decisiones de diseño del lenguaje en términos de distancia de Hamming. Si dos construcciones tienen significados diferentes, a menudo es útil hacer que difieran en al menos dos formas, y que tengan una forma que difiera en una sola forma para generar un compilador. El diseñador de lenguaje generalmente no puede ser responsable de garantizar que los identificadores sean fácilmente distinguibles, pero si, por ejemplo, la asignación requiere :=y la comparación ==, y =solo se puede utilizar para la inicialización en tiempo de compilación, entonces la probabilidad de errores tipográficos convertirá las comparaciones en tareas disminuirá considerablemente.
supercat
@leftaroundabout: De igual manera, si estuviera diseñando un lenguaje, probablemente requeriría la ()invocación de una función de argumento cero, pero también requeriría un token para "tomar la dirección de" una función. La primera acción es mucho más común, por lo que guardar un token en el caso menos común no es tan útil.
supercat
@supercat: bueno, nuevamente creo que esto está resolviendo el problema en el nivel equivocado. La asignación y la comparación son operaciones conceptualmente diferentes, al igual que la evaluación de funciones y la toma de dirección de funciones, respectivamente. Por lo tanto, deberían tener tipos claramente distintos, entonces ya no importa si los operadores tienen una distancia de Hamming baja, porque cualquier error tipográfico será un error de tipo de tiempo de compilación.
Leftaroundabout
@leftaroundabout: si se realiza una asignación a una variable de tipo booleano, o si el tipo de retorno de una función es en sí mismo un puntero a una función (o, en algunos idiomas, una función real), reemplazando una comparación con una asignación, o una invocación de función con una referencia de función, puede producir un programa que sea sintácticamente válido, pero que tenga un significado totalmente diferente de la versión original. La verificación de tipos encontrará algunos de estos errores en general, pero hacer que la sintaxis sea distinta parece un mejor enfoque.
supercat
5

En la mayoría de los casos, estas son elecciones sintácticas de la gramática del lenguaje. Es útil para la gramática que las diversas construcciones individuales sean (relativamente) inequívocas cuando se toman en conjunto. (Si hay ambigüedades, como en algunas declaraciones de C ++, debe haber reglas específicas para la resolución). El compilador no tiene la libertad para adivinar; se requiere seguir la especificación del idioma.


Visual Basic, en varias formas, diferencia entre procedimientos que no devuelven un valor y funciones.

Los procedimientos deben llamarse como una declaración y no requieren parens, solo argumentos separados por comas, si los hay. Las funciones deben llamarse como parte de una expresión y requieren los parens.

Es una distinción relativamente innecesaria que hace que la refactorización manual entre las dos formas sea más dolorosa de lo que debe ser.

(Por otra parte de Visual Basic utiliza los mismos parens ()para las referencias de la matriz como para las llamadas de función, por lo que las referencias de la matriz se parece a las llamadas de función. Y esto facilita la refactorización el manual de una matriz en una llamada de función! Por lo tanto, podríamos considerar otros lenguajes usan []'s para referencias de matriz, pero estoy divagando ...)


En los lenguajes C y C ++, se accede automáticamente al contenido de las variables utilizando su nombre, y si desea hacer referencia a la variable en lugar de su contenido, aplica el &operador unario .

Este tipo de mecanismo también podría aplicarse a los nombres de funciones. El nombre de la función sin procesar podría implicar una llamada a la función, mientras que un &operador unario se usaría para referirse a la función (en sí misma) como datos. Personalmente, me gusta la idea de acceder a funciones sin argumentos sin efectos secundarios con la misma sintaxis que las variables.

Esto es perfectamente plausible (al igual que otras opciones sintácticas para esto).

Erik Eidt
fuente
2
Por supuesto, si bien la falta de corchetes de Visual Basic en las llamadas a procedimientos es una distinción innecesaria en comparación con sus llamadas a funciones, agregarles los corchetes necesarios sería una distinción innecesaria entre las declaraciones , muchas de las cuales en un lenguaje derivado de BASIC tienen efectos que son muy que recuerda a los procedimientos. También ha pasado un tiempo desde que hice algún trabajo en una versión MS BASIC, pero ¿aún no permite la sintaxis call <procedure name> (<arguments>)? Estoy bastante seguro de que era una forma válida de usar los procedimientos cuando hice la programación QuickBASIC en otra vida ...
Periata Breatta
1
@PeriataBreatta: VB todavía permite la sintaxis de "llamada", y a veces la requiere en los casos en que lo que se llama es una expresión [por ejemplo, se puede decir "Llamar si (alguna condición, delegado1, delegado2) (argumentos)", pero directo la invocación del resultado de un operador "If" no sería válido como una declaración independiente].
supercat
@PeriataBreatta Me gustaron las llamadas sin procedimiento de soporte de VB6 porque hacía que el código similar a DSL se viera bien.
Mark Hurd
@supercat En LinqPad, es sorprendente la frecuencia con la que necesito usar Callun método donde casi nunca lo necesito en el código de producción "normal".
Mark Hurd
5

Consistencia y legibilidad.

Si me entero de que se llama a la función de Xla siguiente manera: X(arg1, arg2, ...), entonces espero que funcione de la misma manera para que no hay argumentos: X().

Ahora, al mismo tiempo, aprendo que puedes definir la variable como un símbolo y usarlo así:

a = 5
b = a
...

Ahora, ¿qué pensaría cuando encuentre esto?

c = X

Tu invitado es tan bueno como el mío. En circunstancias normales, el Xes una variable. Pero si tomáramos su camino, ¡también podría ser una función! ¿Debo recordar si qué símbolo se asigna a qué grupo (variables / funciones)?

Podríamos imponer restricciones artificiales, por ejemplo, "Las funciones comienzan con mayúscula. Las variables comienzan con minúscula", pero eso es innecesario y complica las cosas, aunque a veces puede ayudar a algunos de los objetivos de diseño.

Nota al margen 1: Otras respuestas ignoran por completo una cosa más: el lenguaje puede permitirle usar el mismo nombre para funciones y variables, y distinguir por contexto. Ver Common Lispcomo un ejemplo. Función xy variable xcoexisten perfectamente bien.

Nota lateral 2: La respuesta aceptada nos muestra la sintaxis: object.functionname. En primer lugar, no es universal para los idiomas con funciones de primera clase. En segundo lugar, como programador trataría esto como una información adicional: functionnamepertenece a un object. Si objectes un objeto, una clase o un espacio de nombres no importa mucho, pero me dice que pertenece a algún lado . Esto significa que debe agregar una sintaxis artificial object.para cada función global o crear alguna objectpara contener todas las funciones globales.

Y de cualquier manera, pierde la capacidad de tener espacios de nombres separados para funciones y variables.

MatthewRock
fuente
Sip. La consistencia es una buena razón, punto final (no es que sus otros puntos no sean válidos, lo son).
Matthew leyó el
4

Para agregar a las otras respuestas, tome este ejemplo de C:

void *func_factory(void)
{
        return 0;
}

void *(*ff)(void);

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

Si el operador de invocación fuera opcional, no habría forma de distinguir entre la asignación de función y la llamada a función.

Esto es aún más problemático en los idiomas que carecen de un sistema de tipos, por ejemplo, JavaScript, donde la inferencia de tipos no se puede utilizar para descubrir qué es y qué no es una función.

Mark K Cowan
fuente
3
JavaScript no carece de un sistema de tipos. Carece de declaraciones de tipo . Pero es completamente consciente de la diferencia entre los diferentes tipos de valor.
Periata Breatta
1
Periata Breatta: el punto es que las variables y propiedades de Java pueden contener tipos de cualquier valor, por lo que no siempre se puede saber si es o no una función en el momento de la lectura del código.
reinierpost
Por ejemplo, ¿qué hace esta función? function cross(a, b) { return a + b; }
Mark K Cowan
@ MarkKCowan Sigue esto: ecma-international.org/ecma-262/6.0/…
curiousdannii