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


279

Estoy aprendiendo Elixir y me pregunto por qué tiene dos tipos de definiciones de funciones:

  • funciones definidas en un módulo con def, llamadas usandomyfunction(param1, param2)
  • funciones anónimas definidas con fn, llamadas usandomyfn.(param1, param2)

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

¿Tiene algún beneficio tener dos tipos de funciones y convertir de un tipo a otro para pasarlas a otras funciones? ¿Hay algún beneficio que tenga dos notaciones diferentes para llamar a funciones?

Respuestas:


386

Solo para aclarar el nombre, ambas son funciones. Una es una función con nombre 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,. fnes un cierre, similar a un lambdaen 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 múltiples 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 un número diferente de argumentos:

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

¡Oh no! ¡Tenemos un error! No podemos mezclar cláusulas que esperen un número 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 se compilará porque cada vez que ve un def, obtiene un alcance variable vacío. Esa es una diferencia importante entre ellos. Particularmente me gusta el hecho de que cada función nombrada comienza con una pizarra limpia y no se mezclan las variables de diferentes ámbitos. Tienes un límite claro.

Podríamos recuperar la función de saludo mencionada anteriormente como una función anónima. Lo mencionaste tú mismo:

other_function(&hello(&1))

Y luego preguntaste, ¿por qué no puedo simplemente pasarlo hellocomo en otros idiomas? Esto se debe a que las funciones en Elixir se identifican por nombre y aridad. Entonces, una función que espera dos argumentos es una función diferente de una que espera tres, incluso si tenían el mismo nombre. Entonces, si simplemente aprobamos hello, no tendríamos idea de lo helloque realmente quisiste 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 aridades diferentes.

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

&hello/1

Eso capturará la función local nombrada hola con arity 1. En todo el lenguaje y su documentación, es muy común identificar funciones en esta hello/1sintaxis.

Esta es también la razón por la cual Elixir usa un punto para llamar a funciones anónimas. Dado que no puede simplemente pasar hellocomo una función, en su lugar necesita capturarla explícitamente, existe una distinción natural entre funciones con nombre y anónimas y una sintaxis distinta para llamar a cada una de ellas 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.


30
También estoy aprendiendo Elixir, y este es el primer problema que encontré que me detuvo, algo al respecto parecía inconsistente. Gran explicación, pero para ser claros ... ¿es esto el resultado de un problema de implementación, o refleja una sabiduría más profunda con respecto al uso y el paso de funciones? Dado que las funciones anónimas pueden coincidir en función de los valores de los argumentos, parece que sería útil poder también coincidir en el número de argumentos (y de manera coherente con la coincidencia de patrones de funciones en otros lugares).
Ciclón el

17
No es una restricción de implementación en el sentido de que también podría funcionar como f()(sin el punto).
José Valim

66
Puede coincidir en el número de argumentos mediante el uso is_function/2de un guardia. is_function(f, 2)comprueba que tiene arity de 2. :)
José Valim

20
Hubiera preferido las invocaciones de funciones sin puntos para las funciones con nombre y anónimas. A veces resulta confuso y se olvida si una función en particular era anónima o nombrada. También hay más ruido cuando se trata de curry.
CMCDragonkai

28
En Erlang, las llamadas a funciones anónimas y las funciones regulares ya son sintácticamente diferentes: SomeFun()y some_fun(). En Elixir, si eliminamos el punto, serían los mismos some_fun()y some_fun()porque las variables usan el mismo identificador que los nombres de las funciones. De ahí el punto.
José Valim

17

No sé lo útil que será para alguien más, pero la forma en que finalmente comprendí el concepto fue darme cuenta de que las funciones de elixir no son funciones.

Todo en elixir es una expresión. Entonces

MyModule.my_function(foo) 

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

Es tentador referirse a la notación fn o & como puntero de 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 ejecución, use fn, entonces la mayoría de las dificultades se vuelven mucho más claras.


2
Está claro que MyModule.my_function(foo)es una expresión, pero MyModule.my_function"podría" haber sido una expresión que devuelve una función "objeto". Pero como necesitas contarle al arity, necesitarías algo así MyModule.my_function/1. Y supongo que decidieron que era mejor usar una &MyModule.my_function(&1) sintaxis que permitiera expresar la aridad (y también sirve para otros propósitos). Todavía no está claro por qué hay un ()operador para funciones con nombre y un .()operador para funciones sin nombre
RubenLaguna

Creo que quieres: así &MyModule.my_function/1es como puedes pasarlo como una función.
jnmandal

14

Nunca he entendido por qué las explicaciones de esto son tan complicadas.

Es realmente una distinción excepcionalmente pequeña combinada con las realidades de la "ejecución de funciones sin parentesco" al 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 ...

  • fun1es un identificador que describe una función con nombre 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 fun1o fun2en alguna otra expresión? Al evaluar esa expresión, ¿llama a la función referenciada o simplemente hace referencia a un valor sin memoria?

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

Y esto es realmente difícil. Imagine que no hubiera 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 una pista a Elixir. ¿Imagina si cada desreferencia de variables tuviera que verificar una función? Alternativamente, ¿imagina qué heroicidad sería necesaria para inferir siempre que la desreferencia variable estaba usando una función?


12

Puedo estar equivocado ya que nadie lo mencionó, pero también tenía la impresión de que la razón de esto también es la herencia rubí de poder llamar funciones sin corchetes.

Obviamente, Arity está involucrada, 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. Lo llamas solo cuando usas los corchetes.

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

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

Como puede ver, nombrarlo o no no hace una gran diferencia. Pero el elixir y el rubí 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 que no puede usar solo el nombre sin los corchetes porque podría significar que desea llamar a la función. Esto es para lo que &sirve. Si deja Arity aparte por un segundo, anteponer el nombre de su función &significa que quiere explícitamente 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 la razón detrás de esto es que es utilizada principalmente por el tipo de funciones iteradoras que toman funciones como argumentos. Obviamente, no es necesario usarlo &porque ya se consideran argumentos por defecto. Es su proposito.

Ahora, el último problema es que a veces tiene que llamarlos en su código, porque no siempre se usan con un tipo de función de iterador, o podría estar codificando un iterador usted mismo. Para la pequeña historia, dado que el rubí está orientado a objetos, la forma principal de hacerlo era usar el callmétodo en el objeto. De esa manera, podría mantener el comportamiento de los corchetes no obligatorios consistente.

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)

Nuevamente, esta es una opción de diseño. Ahora el elixir no está orientado a objetos y, por lo tanto, no use la primera forma con seguridad. No puedo hablar por José, pero parece que la segunda forma se usó en elixir porque todavía parece una llamada de 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ías salirte con los corchetes siempre que los corchetes sean obligatorios para las funciones anónimas. Parece que es:

Paréntesis obligatorios VS notación ligeramente diferente

En ambos casos, hace una excepción porque hace que ambos se comporten de manera diferente. Como hay una diferencia, es mejor que lo hagas 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, esta podría no ser la mejor explicación del mundo porque simplifiqué la mayoría de los detalles. Además, la mayoría son opciones de diseño y traté de darles una razón sin juzgarlas. Me encanta el elixir, me encanta el rubí, me gustan las llamadas de función sin paréntesis, pero como tú, las consecuencias me parecen bastante equivocadas de vez en cuando.

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

De todos modos ... esto está fuera de alcance.


9

Hay una excelente publicación de 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 puede simplemente cortar y pegar esto en el shell y obtener el mismo resultado.

Es porque hay un error en Erlang. Los módulos en Erlang son secuencias de FORMAS . El shell 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 al llamar fn

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

En la escuela aprendí a llamar a funciones escribiendo f (10) no f. (10) - esta es "realmente" una función con un nombre como Shell.f (10) (es una función definida en el shell) La parte del shell es implícito, por lo que debería llamarse f (10).

Si lo deja así, espere pasar los próximos veinte años de su vida explicando por qué.


No estoy seguro de la utilidad de responder directamente a la pregunta de OP, pero el enlace proporcionado (incluida la sección de comentarios) es en realidad una excelente lectura de la OMI para el público objetivo de la pregunta, es decir, aquellos de nosotros que somos nuevos y estamos aprendiendo Elixir + Erlang .

El enlace está muerto ahora :(
AnilRedshift

2

Elixir tiene llaves opcionales para funciones, incluidas las funciones con 0 arity. Veamos un ejemplo de por qué hace importante una sintaxis de llamada separada:

defmodule Insanity do
  def dive(), do: fn() -> 1 end
end

Insanity.dive
# #Function<0.16121902/0 in Insanity.dive/0>

Insanity.dive() 
# #Function<0.16121902/0 in Insanity.dive/0>

Insanity.dive.()
# 1

Insanity.dive().()
# 1

Sin hacer una diferencia entre 2 tipos de funciones, no podemos decir qué Insanity.divesignifica: obtener una función en sí misma, llamarla o también llamar a la función anónima resultante.


1

fn ->La sintaxis es para usar funciones anónimas. Hacer var. () Es solo decirle al elixir que quiero que tomes esa var con un func y la ejecutes en lugar de referirte a la var como algo que solo tiene esa función.

Elixir tiene un patrón común en el que, en lugar de tener la lógica dentro de una función para ver cómo se debe ejecutar algo, el patrón coincide con diferentes funciones en función de qué tipo de entrada tenemos. Supongo que es por eso que nos referimos a las cosas por aridad en el function_name/1sentido.

Es un poco extraño acostumbrarse a hacer definiciones de funciones abreviadas (func (& 1), etc.), pero útil cuando intenta canalizar o mantener su código conciso.


0

Solo el segundo tipo de función parece ser un objeto de primera clase y puede pasarse como 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 ve otherfunction(myfunction(&1, &2))para facilitarlo, pero ¿por qué es necesario en primer lugar? ¿Por qué no podemos simplemente hacer otherfunction(myfunction))?

Tu puedes hacer otherfunction(&myfunction/2)

Dado que elixir puede ejecutar funciones sin los corchetes (como myfunction), usarlo otherfunction(myfunction))intentará ejecutarse myfunction/0.

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

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.