¿Por qué hay dos tipos de funciones en Elixir?

Estoy aprendiendo Elixir y me pregunto por qué tiene dos tipos de definiciones de función:

  • funciones definidas en un módulo con def , llamado using myfunction(param1, param2)
  • funciones anónimas definidas con fn , llamadas usando myfn.(param1, param2)

Solo el segundo tipo de función parece ser un objeto de primera clase y puede pasarse como un parámetro a otras funciones. Una función definida en un módulo debe estar envuelta en un fn . Hay un poco de azúcar sintáctico que se parece a otra otherfunction(myfunction(&1, &2)) para facilitar eso, pero ¿por qué es necesario en primer lugar? ¿Por qué no podemos hacer otra otherfunction(myfunction)) ? ¿Es solo para permitir las funciones del módulo de llamada sin paréntesis como en Ruby? Parece haber heredado esta característica de Erlang, que también tiene funciones y funciones de módulo, por lo que en realidad proviene de cómo funciona la VM de Erlang internamente.

¿Hay algún beneficio con dos tipos de funciones y la conversión de un tipo a otro para pasarlos a otras funciones? ¿Hay algún beneficio con dos notaciones diferentes para llamar a funciones?

    Solo para aclarar la denominación, ambas son funciones. Una es una función nombrada y la otra es anónima. Pero tienes razón, funcionan de manera algo diferente y voy a ilustrar por qué funcionan así.

    Comencemos con el segundo, fn . fn es un cierre, similar a un lambda en Ruby. Podemos crearlo de la siguiente manera:

     x = 1 fun = fn y -> x + y end fun.(2) #=> 3 

    Una función también puede tener varias cláusulas:

     x = 1 fun = fn y when y < 0 -> x - y y -> x + y end fun.(2) #=> 3 fun.(-2) #=> 3 

    Ahora, intentemos algo diferente. Tratemos de definir diferentes cláusulas esperando una cantidad diferente de argumentos:

     fn x, y -> x + y x -> x end ** (SyntaxError) cannot mix clauses with different arities in function definition 

    ¡Oh no! ¡Obtenemos un error! No podemos mezclar cláusulas que esperen una cantidad diferente de argumentos. Una función siempre tiene una aridad fija.

    Ahora, hablemos de las funciones nombradas:

     def hello(x, y) do x + y end 

    Como se esperaba, tienen un nombre y también pueden recibir algunos argumentos. Sin embargo, no son cierres:

     x = 1 def hello(y) do x + y end 

    Este código no podrá comstackrse porque cada vez que ve una def , obtiene un ámbito variable vacío. Esa es una diferencia importante entre ellos. Me gusta particularmente el hecho de que cada función nombrada comience con un borrón y cuenta nueva y no se obtienen las variables de diferentes ámbitos mezclados entre sí. Usted tiene un límite claro.

    Podríamos recuperar la función llamada hello anterior como una función anónima. Usted lo mencionó usted mismo:

     other_function(&hello(&1)) 

    Y luego preguntaste, ¿por qué no puedo simplemente pasarlo tan bien como en otros idiomas? Eso es porque las funciones en Elixir se identifican por nombre y arity. Por lo tanto, una función que espera dos argumentos es una función diferente a la que espera tres, incluso si tienen el mismo nombre. Entonces, si simplemente pasamos hello , no tendríamos idea de qué hello realmente quiso decir. ¿El que tiene dos, tres o cuatro argumentos? Esta es exactamente la misma razón por la que no podemos crear una función anónima con cláusulas con aries diferentes.

    Desde Elixir v0.10.1, tenemos una syntax para capturar funciones con nombre:

     &hello/1 

    Eso capturará la función local llamada hello con arity 1. A lo largo del lenguaje y su documentación, es muy común identificar funciones en esta syntax hello/1 .

    Esta es también la razón por la cual Elixir usa un punto para llamar a funciones anónimas. Como no se puede pasar simplemente como una función, en su lugar se debe capturar explícitamente, existe una distinción natural entre funciones nombradas y anónimas y una syntax distinta para llamar a cada una hace que todo sea un poco más explícito (Lispers estaría familiarizado con esto debido a la discusión Lisp 1 vs. Lisp 2).

    En general, esas son las razones por las que tenemos dos funciones y por qué se comportan de manera diferente.

    No sé cuán útil será esto para los demás, pero la forma en que finalmente entendí el concepto fue darme cuenta de que las funciones del elixir no son funciones.

    Todo en elixir es una expresión. Asi que

     MyModule.my_function(foo) 

    no es una función sino la expresión devuelta al ejecutar el código en my_function . En realidad, hay una sola forma de obtener una “Función” que puede pasar como un argumento y es usar la notación de función anónima.

    Es tentador referirse a la notación fn o & como un puntero a la función, pero en realidad es mucho más. Es un cierre del entorno circundante.

    Si te preguntas a ti mismo:

    ¿Necesito un entorno de ejecución o un valor de datos en este lugar?

    Y si necesita ejecutar la ejecución fn, entonces la mayoría de las dificultades se vuelven mucho más claras.

    Puedo estar equivocado ya que nadie lo mencionó, pero también tuve la impresión de que la razón de esto también es el legado Ruby de poder llamar a funciones sin corchetes.

    Arity obviamente está involucrado, pero dejemos de lado por un tiempo y usemos funciones sin argumentos. En un lenguaje como javascript donde los corchetes son obligatorios, es fácil hacer la diferencia entre pasar una función como argumento y llamar a la función. Usted lo llama solo cuando usa los corchetes.

     my_function // argument (function() {}) // argument my_function() // function is called (function() {})() // function is called 

    Como puede ver, nombrarlo o no hacer una gran diferencia. Pero elixir y ruby ​​le permiten llamar a funciones sin los corchetes. Esta es una opción de diseño que personalmente me gusta pero tiene este efecto secundario: no se puede usar solo el nombre sin los corchetes porque podría significar que desea llamar a la función. Esto es para lo que es & Si deja una parte ariad por un segundo, anteponiendo el nombre de su función con & significa que explícitamente desea usar esta función como argumento, no lo que devuelve esta función.

    Ahora la función anónima es un poco diferente ya que se usa principalmente como argumento. Nuevamente, esta es una opción de diseño, pero lo racional detrás de esto es que es principalmente utilizada por los iteradores como funciones que toman funciones como argumentos. Entonces, obviamente, no necesita usar & porque ya se consideran argumentos por defecto. Es su propósito.

    Ahora el último problema es que a veces tienes que llamarlos en tu código, porque no siempre se usan con un tipo de función de iterador, o puedes codificar un iterador tú mismo. Para la pequeña historia, dado que el Ruby está orientado a objetos, la forma principal de hacerlo fue usar el método de call en el objeto. De esta forma, podrías mantener la coherencia del comportamiento de los brackets no obligatorios.

     my_lambda.call my_lambda.call() my_lambda_with_arguments.call :h2g2, 42 my_lambda_with_arguments.call(:h2g2, 42) 

    Ahora a alguien se le ocurrió un atajo que básicamente parece un método sin nombre.

     my_lambda.() my_lambda_with_arguments.(:h2g2, 42) 

    De nuevo, esta es una elección de diseño. Ahora el elixir no está orientado a objetos y, por lo tanto, no use la primera forma para asegurarse. No puedo hablar por José, pero parece que la segunda forma se usó en el elixir porque todavía se parece a una llamada a función con un carácter adicional. Está lo suficientemente cerca de una llamada de función.

    No pensé en todos los pros y los contras, pero parece que en ambos idiomas podría escaparse solo con los corchetes siempre que haga que los corchetes sean obligatorios para las funciones anónimas. Parece que es:

    Paréntesis obligatorios VS Anotación ligeramente diferente

    En ambos casos, usted hace una excepción porque hace que ambos se comporten de manera diferente. Como hay una diferencia, también podrías hacerlo obvio e ir por la notación diferente. Los corchetes obligatorios se verían naturales en la mayoría de los casos, pero muy confusos cuando las cosas no salen según lo planeado.

    Aqui tienes. Ahora bien, esta podría no ser la mejor explicación del mundo porque simplifiqué la mayoría de los detalles. También la mayoría son opciones de diseño y traté de darles una razón sin juzgarlos. Me encanta el elixir, me encanta el Ruby, me gustan las llamadas a la función sin corchetes, pero, como tú, las consecuencias me resultan totalmente erróneas de vez en cuando.

    Y en el elixir, es solo este punto extra, mientras que en ruby ​​tienes bloques encima de esto. Los bloques son increíbles y me sorprende lo mucho que puedes hacer con solo bloques, pero solo funcionan cuando solo necesitas una función anónima, que es el último argumento. Entonces, como deberías poder tratar con otros escenarios, aquí viene todo el método / lambda / proc / block confusion.

    De todos modos … esto está fuera de scope.

    Nunca entendí por qué las explicaciones de esto son tan complicadas.

    Realmente se trata de una distinción excepcionalmente pequeña combinada con las realidades de la “ejecución de funciones sin paréntesis” estilo Ruby.

    Comparar:

     def fun1(x, y) do x + y end 

    A:

     fun2 = fn x, y -> x + y end 

    Si bien ambos son solo identificadores …

    • fun1 es un identificador que describe una función nombrada definida con def .
    • fun2 es un identificador que describe una variable (que contiene una referencia a la función).

    Considera lo que eso significa cuando ves fun1 o fun2 en alguna otra expresión? Al evaluar esa expresión, ¿llama a la función a la que hace referencia o simplemente hace referencia a un valor sin memoria?

    No hay una buena manera de saber en tiempo de comstackción. Ruby tiene el lujo de introspectar el espacio de nombres variable para averiguar si un enlace variable ha sombreado una función en algún momento. Elixir, al comstackrse, no puede hacer esto. Eso es lo que hace la notación de puntos, le dice a Elixir que debería contener una referencia de función y que debería llamarse.

    Y esto es realmente difícil. Imagina que no hubo una notación de puntos. Considera este código:

     val = 5 if :rand.uniform < 0.5 do val = fn -> 5 end end IO.puts val # Does this work? IO.puts val.() # Or maybe this? 

    Dado el código anterior, creo que está bastante claro por qué tienes que darle a Elixir la pista. Imagínese si cada desviación de variable tuviera que verificar una función. Alternativamente, imagina qué heroísmo sería necesario para inferir siempre que la desreferencia variable estaba usando una función.

    Hay una excelente publicación en el blog sobre este comportamiento: enlace

    Dos tipos de funciones

    Si un módulo contiene esto:

     fac(0) when N > 0 -> 1; fac(N) -> N* fac(N-1). 

    No puedes cortar y pegar esto en el caparazón y obtener el mismo resultado.

    Es porque hay un error en Erlang. Los módulos en Erlang son secuencias de FORMS . El shell de Erlang evalúa una secuencia de EXPRESIONES . En Erlang, los FORMULARIOS no son EXPRESIONES .

     double(X) -> 2*X. in an Erlang module is a FORM Double = fun(X) -> 2*X end. in the shell is an EXPRESSION 

    Los dos no son lo mismo. Esta pequeña tontería ha sido Erlang para siempre, pero no nos dimos cuenta y aprendimos a vivir con ella.

    Punto en llamar fn

     iex> f = fn(x) -> 2 * x end #Function iex> f.(10) 20 

    En la escuela aprendí a llamar funciones escribiendo f (10) not f. (10) – esta es “realmente” una función con un nombre como Shell.f (10) (es una función definida en el intérprete de comandos) La parte del intérprete de comandos es implícito, por lo que simplemente debe llamarse f (10).

    Si lo dejas así, espera pasar los próximos veinte años de tu vida explicando por qué.

    Solo el segundo tipo de función parece ser un objeto de primera clase y puede pasarse como un parámetro a otras funciones. Una función definida en un módulo debe estar envuelta en un fn. Hay un poco de azúcar sintáctico que se parece a otra otherfunction(myfunction(&1, &2)) para facilitar eso, pero ¿por qué es necesario en primer lugar? ¿Por qué no podemos hacer otra otherfunction(myfunction)) ?

    Puede hacer otra otherfunction(&myfunction/2)

    Como elixir puede ejecutar funciones sin los paréntesis (como myfunction ), usando otherfunction(myfunction)) intentará ejecutar myfunction/0 .

    Por lo tanto, debe usar el operador de captura y especificar la función, incluso arity, ya que puede tener diferentes funciones con el mismo nombre. Por lo tanto, &myfunction/2 .