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

63

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 hacer function_name() .

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

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

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

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

Descargo de responsabilidad: ¡No me malinterpretes, me encanta la sintaxis tipo C! ;) Solo pregunto si podría ser mejor. ¿Requerir () tiene alguna ventaja de rendimiento o facilita la comprensión del código? Tengo mucha curiosidad por saber exactamente cuál es la razón.

    
pregunta DRS David Soft 05.10.2016 - 15:49

10 respuestas

250

Para los idiomas que usan funciones de primera clase , es bastante común que la sintaxis de se refiera a una función es:

a = object.functionName

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

b = object.functionName()

a en el ejemplo anterior sería una referencia a la función anterior (y podría llamarlo haciendo a() ), mientras que b contendría el valor de retorno de la función.

Si bien algunos idiomas pueden realizar llamadas a funciones sin paréntesis, puede resultar confuso si están llamando a la función, o simplemente refiriéndose a la función.

    
respondido por el Nathan Merrill 05.10.2016 - 16:15
63

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

Como compilador, creo que la presencia garantizada de paréntesis es bastante conveniente; Siempre sabría que es una llamada de 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 se trata de una llamada de método, aunque no se pasan parámetros.

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

    
respondido por el Robert Harvey 05.10.2016 - 15:54
19

Esto es en realidad una casualidad bastante sutil de opciones de sintaxis. Hablaré con lenguajes 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 "múltiples argumentos" es en realidad un único parámetro del tipo de producto. Así, 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 not indican los parámetros de la función; se utilizan para emparejar patrones el argumento. Para convencerlo de que este es realmente un argumento, podemos utilizar 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 hacer coincidencias de patrones y guardar algo de escritura. No son necesarios.

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

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

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

Por lo tanto, realmente no existe tal cosa como una lista de argumentos.

    
respondido por el gardenhead 05.10.2016 - 16:45
14

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

En Java, se eligió que un identificador seguido de () o (...) signifique una llamada de 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 está tratando con un método o una variable miembro. De hecho, se puede usar el mismo nombre para un método y una variable miembro, ambos serán accesibles con su respectiva sintaxis.

    
respondido por el Florian F 05.10.2016 - 16:10
10

La sintaxis sigue la semántica, entonces comencemos desde 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 se puede tratar como un valor
  • una función se puede aplicar 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 suficiente de ellos).

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 puede hacerse simplemente usando su nombre como en &func (C creando un puntero de función)
  • en algunos idiomas, tiene sintaxis de mano corta para aplicar parcialmente; Java permite someVariable::someMethod por 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.

    
respondido por el Matthieu M. 05.10.2016 - 16:51
8

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

variable

y llamar a una función, lo que podría incurrir en efectos secundarios

launch_nukes()

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

    
respondido por el Daniel Jour 05.10.2016 - 16:25
7

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 puedes diseñar un idioma para que x = sqrt y establezca x en la raíz cuadrada de y, eso no significa que necesariamente debas 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 pretendías y muy difícil de depurar. (Como cualquier persona que haya trabajado con expresiones regulares lo sabrá). Un poco de redundancia es algo bueno porque permite que se detecten muchos de sus errores, y mientras más redundancia haya, es más probable que los diagnósticos sean precisos. Ahora, por supuesto, puede llevarlo demasiado lejos (ninguno de nosotros quiere escribir en COBOL en estos días), pero hay un balance 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 se aplica a los lenguajes de programación.

    
respondido por el Michael Kay 06.10.2016 - 23:23
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 todas juntas. (Si hay ambigüedades, como en algunas declaraciones de C ++, tiene que haber reglas específicas para la resolución). El compilador no tiene la latitud 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 se deben llamar 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 tiene que ser.

(Por otra parte, Visual Basic utiliza el mismo parens () para las referencias de matriz que para las llamadas de función, por lo que las referencias de matriz parecen llamadas de función. ¡Y esto facilita la refactorización manual de una matriz en una llamada de función! ponderar otros idiomas usa [] '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 referirse a la variable en lugar de a su contenido, aplique el operador unario & .

Este tipo de mecanismo también podría aplicarse a los nombres de funciones. El nombre de la función en bruto 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í) 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 (como lo son otras opciones sintácticas para esto).

    
respondido por el Erik Eidt 05.10.2016 - 16:58
4

Para agregar a las otras respuestas, tome este ejemplo en 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 manera de distinguir entre la asignación de función y la llamada de 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 usar para averiguar qué es y qué no es una función.

    
respondido por el Mark K Cowan 05.10.2016 - 23:22
4

Consistencia y legibilidad.

Si me entero de que llamas a la función X así: X(arg1, arg2, ...) , entonces espero que funcione de la misma manera para no tener argumentos: X() .

Ahora, al mismo tiempo, me doy cuenta de que puedes definir la variable como un símbolo y usarla así:

a = 5
b = a
...

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

c = X

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

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

Nota al margen 1: Otras respuestas ignoran completamente una cosa más: el lenguaje podría permitirle usar el mismo nombre para la función y la variable, y distinguirlas por contexto. Ver Common Lisp como ejemplo. La función x y la variable x coexisten perfectamente bien.

Nota al margen 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: functionname pertenece a object . Si object es un objeto, una clase o un espacio de nombres no importa mucho, pero me dice que pertenece a en alguna parte . Esto significa que debe agregar la sintaxis artificial object. para cada función global o crear un poco de object para contener todas las funciones globales.

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

    
respondido por el MatthewRock 06.10.2016 - 11:01

Lea otras preguntas en las etiquetas