¿Por qué la mayoría de los lenguajes de programación tienen una palabra clave especial o una sintaxis para declarar funciones? [cerrado]

39

La mayoría de los lenguajes de programación (tanto los que se escriben dinámicamente como los estáticos) tienen palabras clave especiales y / o sintaxis que se ven muy diferentes a las variables de declaración para declarar funciones. Veo funciones tan solo como declarar otra entidad nombrada:

Por ejemplo en Python:

x = 2
y = addOne(x)
def addOne(number): 
  return number + 1

¿Por qué no?

x = 2
y = addOne(x)
addOne = (number) => 
  return number + 1

Del mismo modo, en un lenguaje como Java:

int x = 2;
int y = addOne(x);

int addOne(int x) {
  return x + 1;
}

¿Por qué no?

int x = 2;
int y = addOne(x);
(int => int) addOne = (x) => {
  return x + 1;
}

Esta sintaxis parece una forma más natural de declarar algo (ya sea una función o una variable) y una palabra clave menos como def o function en algunos idiomas. Y, en mi opinión, es más coherente (busco en el mismo lugar para entender el tipo de una variable o función) y probablemente hace que el analizador / gramática sea un poco más sencillo de escribir.

Sé que muy pocos idiomas usan esta idea (CoffeeScript, Haskell) pero los lenguajes más comunes tienen una sintaxis especial para las funciones (Java, C ++, Python, JavaScript, C #, PHP, Ruby).

Incluso en Scala, que admite ambas formas (y tiene inferencia de tipo), es más común escribir:

def addOne(x: Int) = x + 1

En lugar de:

val addOne = (x: Int) => x + 1

OMI, al menos en Scala, esta es probablemente la versión más fácil de entender, pero este lenguaje rara vez se sigue:

val x: Int = 1
val y: Int = addOne(x)
val addOne: (Int => Int) = x => x + 1

Estoy trabajando en mi propio lenguaje de juguete y me pregunto si hay algún inconveniente si diseño mi lenguaje de tal manera y si hay razones históricas o técnicas, ¿este patrón no se sigue ampliamente?

    
pregunta pathikrit 26.09.2014 - 20:50

12 respuestas

48

Creo que la razón es que la mayoría de los idiomas populares provienen de la familia de idiomas C o están influenciados por ellos, en lugar de los lenguajes funcionales y su raíz, el cálculo lambda.

Y en estos idiomas, las funciones son no simplemente otro valor:

  • En C ++, C # y Java, puede sobrecargar funciones: puede tener dos funciones con el mismo nombre, pero con una firma diferente.
  • En C, C ++, C # y Java, puede tener valores que representan funciones, pero los punteros de función, los funtores, los delegados y las interfaces funcionales son distintos de las funciones en sí. Parte de la razón es que la mayoría de ellas no son en realidad solo funciones, son una función junto con algún estado (mutable).
  • Las variables son mutables de forma predeterminada (tienes que usar const , readonly o final para prohibir la mutación), pero las funciones no se pueden reasignar.
  • Desde una perspectiva más técnica, el código (que se compone de funciones) y los datos están separados. Por lo general, ocupan diferentes partes de la memoria, y se accede a ellos de manera diferente: el código se carga una vez y luego solo se ejecuta (pero no se lee o escribe), mientras que los datos a menudo se asignan y desasignan constantemente y se escriben y leen, pero nunca se ejecutan. / p>

    Y como C estaba destinado a ser "cercano al metal", tiene sentido reflejar esta distinción en la sintaxis del lenguaje también.

  • El enfoque de "función es solo un valor" que forma la base de la programación funcional ha ganado fuerza en los lenguajes comunes hace relativamente poco tiempo, como lo demuestra la introducción tardía de lambdas en C ++, C # y Java (2011, 2007, 2014).

respondido por el svick 26.09.2014 - 22:48
59

Es porque es importante para personas reconocer que las funciones no son solo "otra entidad con nombre". A veces tiene sentido manipularlos como tales, pero aún se pueden reconocer de un vistazo.

Realmente no importa lo que la computadora piense acerca de la sintaxis, ya que una mancha incomprensible de caracteres está bien para que la interprete una máquina, pero eso sería casi imposible de entender y mantener para los humanos.

Realmente es la misma razón por la que tenemos tiempo y para bucles, cambio y, en caso contrario, etc., a pesar de que todos ellos en última instancia se reducen a una instrucción de comparación y salto. La razón es porque está ahí para el beneficio de los humanos que mantienen y comprenden el código.

Tener sus funciones como "otra entidad con nombre" de la forma que está proponiendo hará que su código sea más difícil de ver y, por lo tanto, más difícil de entender.

    
respondido por el whatsisname 26.09.2014 - 21:04
10

Es posible que le interese saber que, en los tiempos prehistóricos, un lenguaje llamado ALGOL 68 usaba una sintaxis similar a la que usted propone. Al reconocer que los identificadores de función están vinculados a valores al igual que otros identificadores, en ese idioma podría declarar una función (constante) utilizando la sintaxis

  

tipo de función nombre = ( lista de parámetros ) tipo de resultado : cuerpo ;

Concretamente tu ejemplo se leería

PROC (INT)INT add one = (INT n) INT: n+1;

Reconociendo la redundancia en que el tipo inicial puede leerse del RHS de la declaración, y al ser un tipo de función siempre comienza con PROC , esto podría (y generalmente sería) contratado para

PROC add one = (INT n) INT: n+1;

pero tenga en cuenta que = todavía aparece antes de la lista de parámetros. También tenga en cuenta que si desea una función variable (a la que más adelante podría asignarse otro valor del mismo tipo de función), = debe reemplazarse por := , dando uno de

PROC (INT)INT func var := (INT n) INT: n+1;
PROC func var := (INT n) INT: n+1;

Sin embargo, en este caso, ambas formas son de hecho abreviaturas; ya que el identificador func var designa una referencia a una función generada localmente, la forma completamente expandida sería

REF PROC (INT)INT func var = LOC PROC (INT)INT := (INT n) INT: n+1;

Es fácil acostumbrarse a esta forma sintáctica en particular, pero claramente no tiene muchos seguidores en otros lenguajes de programación. Incluso los lenguajes de programación funcionales como Haskell prefieren el estilo f n = n+1 con = siguiendo la lista de parámetros. Supongo que la razón es principalmente psicológica; después de todo, incluso los matemáticos no suelen preferir, como yo, f = n n + 1 sobre f ( n ) = n + 1.

Por cierto, la discusión anterior sí resalta una diferencia importante entre las variables y las funciones: las definiciones de funciones generalmente vinculan un nombre a un valor de función específico, que no se puede cambiar más adelante, mientras que las definiciones de variables generalmente introducen un identificador con un valor inicial , pero que puede cambiar más adelante. (No es una regla absoluta; las variables de función y las constantes de no función se producen en la mayoría de los idiomas). Además, en los lenguajes compilados, el límite de valor en una definición de función suele ser una constante de tiempo de compilación, por lo que las llamadas a la función pueden ser compilado utilizando una dirección fija en el código. En C / C ++ esto es incluso un requisito; el equivalente de la ALGOL 68

PROC (REAL) REAL f = IF mood=sunny THEN sin ELSE cos FI;

no se puede escribir en C ++ sin introducir un puntero a función. Este tipo de limitaciones específicas justifican el uso de una sintaxis diferente para las definiciones de funciones. Pero dependen de la semántica del idioma, y la justificación no se aplica a todos los idiomas.

    
respondido por el Marc van Leeuwen 27.09.2014 - 12:20
8

Mencionaste Java y Scala como ejemplos. Sin embargo, pasaste por alto un hecho importante: esas no son funciones, son métodos. Los métodos y funciones son fundamentalmente diferentes. Las funciones son objetos, los métodos pertenecen a objetos.

En Scala, que tiene funciones y métodos, existen las siguientes diferencias entre métodos y funciones:

  • los métodos pueden ser genéricos, las funciones no pueden
  • los métodos pueden tener ninguna, una o varias listas de parámetros, las funciones siempre tienen exactamente una lista de parámetros
  • los métodos pueden tener una lista de parámetros implícita, las funciones no pueden
  • los métodos pueden tener parámetros opcionales con argumentos predeterminados, las funciones no pueden
  • los métodos pueden tener parámetros repetidos, las funciones no pueden
  • los métodos pueden tener parámetros de nombre, las funciones no pueden
  • los métodos pueden llamarse con argumentos con nombre, las funciones no pueden
  • las funciones son objetos, los métodos no son

Por lo tanto, su reemplazo propuesto simplemente no funciona, al menos para esos casos.

    
respondido por el Jörg W Mittag 27.09.2014 - 02:13
3

Cambiando la pregunta, si uno no está interesado en tratar de editar el código fuente en una máquina que tiene una gran capacidad de memoria RAM, o minimizar el tiempo para leerlo en un disquete, ¿qué hay de malo en usar palabras clave?

Ciertamente, es mejor leer x=y+z que store the value of y plus z into x , pero eso no significa que los caracteres de puntuación sean intrínsecamente "mejores" que las palabras clave. Si las variables i , j y k son Integer , y x es Real , considere las siguientes líneas en Pascal:

k := i div j;
x := i/j;

La primera línea realizará una división de enteros truncando, mientras que la segunda realizará una división de números reales. La distinción se puede hacer muy bien porque Pascal usa div como su operador de división de enteros truncado, en lugar de tratar de usar un signo de puntuación que ya tiene otro propósito (división de números reales).

Si bien hay algunos contextos en los que puede ser útil hacer que la definición de una función sea concisa (por ejemplo, una lambda que se usa como parte de otra expresión), se supone que las funciones se destacan y pueden ser fácilmente reconocibles visualmente como funciones. Si bien podría ser posible hacer la distinción mucho más sutil y no usar más que caracteres de puntuación, ¿cuál sería el punto? Decir Function Foo(A,B: Integer; C: Real): String deja claro cuál es el nombre de la función, qué parámetros espera y qué devuelve. Tal vez uno podría acortarlo en seis o siete caracteres reemplazando Function con algunos caracteres de puntuación, pero ¿qué se ganaría?

Otra cosa a tener en cuenta es que hay una mayoría de marcos, una diferencia fundamental entre una declaración que siempre asociará un nombre con un método particular o un enlace virtual particular, y una que crea una variable que inicialmente identifica un método particular o vinculante, pero podría cambiarse en tiempo de ejecución para identificar a otro. Dado que estos son conceptos semánticamente muy diferentes en la mayoría de los marcos de procedimientos, tiene sentido que deban tener una sintaxis diferente.

    
respondido por el supercat 27.09.2014 - 00:13
3

Las razones en las que puedo pensar son:

  • Es más fácil para el compilador saber qué declaramos.
  • Es importante para nosotros saber (de manera trivial) si esta es una función o una variable. Las funciones suelen ser cajas negras y no nos importa su implementación interna. No me gusta la inferencia de tipos en los tipos de devolución en Scala, porque creo que es más fácil usar una función que tiene un tipo de devolución: a menudo es la única documentación proporcionada.
  • Y la más importante es la estrategia seguir a la multitud que se utiliza en el diseño de lenguajes de programación. C ++ fue creado para robar programadores C, y Java fue diseñado de una manera que no asusta a los programadores C ++, y C # para atraer a los programadores Java. Incluso C #, que creo que es un lenguaje muy moderno con un increíble equipo detrás, copió algunos errores de Java o incluso de C.
respondido por el Sleiman Jneidi 26.09.2014 - 22:14
2

Bueno, la razón podría ser que esos idiomas no son lo suficientemente funcionales, por así decirlo. En otras palabras, rara vez se definen funciones. Por lo tanto, el uso de una palabra clave adicional es aceptable.

En los idiomas de la herencia de ML o Miranda, OTOH, la mayoría de las veces se definen funciones. Mira un código de Haskell, por ejemplo. Literalmente es principalmente una secuencia de definiciones de funciones, muchas de ellas tienen funciones locales y funciones locales de esas funciones locales. Por lo tanto, una palabra clave divertido en Haskell sería un error tan grande como requerir una declaración de evaluación en un lenguaje imperativo para comenzar con asignar . La asignación de causa es, con mucho, la declaración más frecuente.

    
respondido por el Ingo 27.09.2014 - 18:48
1

Esto puede ser útil en idiomas dinámicos donde el tipo no es tan importante, pero no es tan legible en idiomas tipográficos estáticos donde siempre desea saber el tipo de su variable. Además, en los lenguajes orientados a objetos es muy importante saber el tipo de su variable, para saber qué operaciones admite.

En su caso, una función con 4 variables sería:

(int, long, double, String => int) addOne = (x, y, z, s) => {
  return x + 1;
}

Cuando miro el encabezado de la función y veo (x, y, z, s) pero no conozco los tipos de estas variables. Si quiero saber el tipo de z que es el tercer parámetro, tendré que mirar el comienzo de la función y comenzar a contar 1, 2, 3 y luego ver que el tipo sea double . De la forma anterior, miro directamente y veo double z .

    
respondido por el m3th0dman 26.09.2014 - 21:24
1

Personalmente, no veo ningún fallo fatal en tu idea; puede encontrar que es más difícil de lo que esperaba expresar ciertas cosas con su nueva sintaxis y / o puede encontrar que necesita revisarlo (agregando varios casos especiales y otras características, etc.), pero dudo que se encuentre Necesitando abandonar la idea por completo.

La sintaxis que ha propuesto se parece más o menos a una variante de algunos de los estilos de notación que se usan a veces para expresar funciones o tipos de funciones en matemáticas. Esto significa que, como todas las gramáticas, probablemente atraerá más a algunos programadores que a otros. (Como matemático, me gusta).

Sin embargo, debe tener en cuenta que en la mayoría de los idiomas, la sintaxis de estilo def (es decir, la sintaxis tradicional) se se comporta de manera diferente a una asignación de variable estándar.

  • En la familia C y C++ , las funciones generalmente no se tratan como "objetos", es decir, trozos de datos escritos para copiar y colocar en la pila y todo eso. (Sí, puede tener punteros de función, pero esos aún apuntan al código ejecutable, no a los "datos" en el sentido típico).
  • En la mayoría de los idiomas OO, hay un manejo especial para los métodos (es decir, funciones miembro); es decir, no son solo funciones declaradas dentro del alcance de una definición de clase. La diferencia más importante es que el objeto sobre el que se llama el método se pasa típicamente como un primer parámetro implícito al método. Python lo hace explícito con self (que, por cierto, no es realmente una palabra clave; podría hacer que cualquier identificador válido sea el primer argumento de un método).

Debe considerar si su nueva sintaxis de manera precisa (y esperemos que sea intuitiva) representa lo que realmente está haciendo el compilador o intérprete. Puede ayudar leer sobre, digamos, la diferencia entre las lambdas y los métodos en Ruby; esto le dará una idea de cómo su paradigma de funciones son datos justos difiere del paradigma de procedimientos / OO típico.

    
respondido por el Kyle Strand 26.09.2014 - 23:46
1

Para algunos lenguajes las funciones no son valores. En tal lenguaje decir que

val f = << i : int  ... >> ;

es una definición de función, mientras que

val a = 1 ;

declara una constante, es confuso porque estás usando una sintaxis para significar dos cosas.

Otros lenguajes, como ML, Haskell y Scheme tratan las funciones como valores de primera clase, pero le brindan al usuario una sintaxis especial para declarar constantes con valores de función. * Aplican la regla de que "el uso acorta la forma". Es decir. Si un constructo es común y detallado, debe darle al usuario una taquigrafía. Es poco elegante darle al usuario dos sintaxis diferentes que significan exactamente lo mismo; a veces la elegancia debe ser sacrificada a la utilidad.

Si, en su idioma, las funciones son de primera clase, ¿por qué no tratar de encontrar una sintaxis que sea lo suficientemente concisa como para que no tenga la tentación de encontrar un azúcar sintáctico?

- Editar -

Otro problema que nadie ha mencionado (aún) es la recursión. Si lo permites

{ 
    val f = << i : int ... g(i-1) ... >> ;
    val g = << j : int ... f(i-1) ... >> ;
    f(x)
}

y tú permites

{
    val a = 42 ;
    val b = a + 1 ;
    a
} ,

¿Se sigue que debes permitir?

{
    val a = b + 1 ; 
    val b = a - 1 ;
    a
} ?

En un lenguaje perezoso (como Haskell), no hay problema aquí. En un lenguaje esencialmente sin controles estáticos (como LISP), no hay problema aquí. Pero en un lenguaje entusiasta comprobado estáticamente, debe tener cuidado de cómo se definen las reglas de la verificación estática, si desea permitir las dos primeras y prohibir la última.

- Fin de edición -

* Se podría argumentar que Haskell no pertenece a esta lista. Proporciona dos formas de declarar una función, pero ambas son, en cierto sentido, generalizaciones de la sintaxis para declarar constantes de otros tipos

    
respondido por el Theodore Norvell 27.09.2014 - 23:06
0

Hay una razón muy simple para tener tal distinción en la mayoría de los idiomas: es necesario distinguir evaluación y declaración . Tu ejemplo es bueno: ¿por qué no como variables? Bueno, las expresiones de las variables son evaluadas inmediatamente.

Haskell tiene un modelo especial donde no hay distinción entre evaluación y declaración, por lo que no hay necesidad de una palabra clave especial.

    
respondido por el Florian Margaine 26.09.2014 - 22:48
0

Las funciones se declaran de manera diferente a los literales, objetos, etc. en la mayoría de los idiomas porque se usan de manera diferente, se depuran de manera diferente y representan diferentes fuentes potenciales de error.

Si una referencia de objeto dinámico o un objeto mutable se pasa a una función, la función puede cambiar el valor del objeto mientras se ejecuta. Este tipo de efecto secundario puede dificultar el seguimiento de lo que hará una función si está anidada dentro de una expresión compleja, y este es un problema común en lenguajes como C ++ y Java.

Considere la posibilidad de depurar algún tipo de módulo del kernel en Java, donde cada objeto tiene una operación toString (). Si bien se puede esperar que el método toString () restaure el objeto, es posible que deba desensamblarlo y volver a ensamblarlo para traducir su valor a un objeto String. Si está intentando depurar los métodos a los que toString () llamará (en un escenario de enganche y plantilla) para hacer su trabajo, y resaltar accidentalmente el objeto en la ventana de variables de la mayoría de los IDE, puede bloquear al depurador. Esto se debe a que el IDE intentará toString () el objeto que llama al mismo código que está en proceso de depuración. Ningún valor primitivo se caga de esta manera porque el significado semántico de los valores primitivos está definido por el lenguaje, no por el programador.

    
respondido por el Trixie Wolf 27.09.2014 - 02:10

Lea otras preguntas en las etiquetas