erlang - tutorial - josé valim
¿Por qué hay dos tipos de funciones en el elixir? (6)
Estoy aprendiendo Elixir y me pregunto por qué tiene dos tipos de definiciones de funciones:
- Funciones definidas en un módulo con
def
, llamadas utilizandomyfunction(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 se puede pasar 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áctica que se parece a otra otherfunction(myfunction(&1, &2))
para facilitarlo, pero ¿por qué es necesario en primer lugar? ¿Por qué no podemos hacer otra otherfunction(myfunction))
? ¿Es solo para permitir funciones de módulo de llamada sin paréntesis como en Ruby? Parece que heredó esta característica de Erlang, que también tiene funciones de módulo y funciones, ¿así que en realidad proviene de cómo funciona internamente la máquina virtual de Erlang?
¿Hay algún beneficio al tener dos tipos de funciones y la conversión de un tipo a otro para pasarlas a otras funciones? ¿Hay algún beneficio al tener dos notaciones diferentes para llamar a las funciones?
Solo el segundo tipo de función parece ser un objeto de primera clase y se puede pasar 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áctica que se parece a otra
otherfunction(myfunction(&1, &2))
para facilitarlo, pero ¿por qué es necesario en primer lugar? ¿Por qué no podemos hacer otraotherfunction(myfunction))
?
Puedes hacer otra otherfunction(&myfunction/2)
Como el elixir puede ejecutar funciones sin los corchetes (como myfunction
), utilizando otra otherfunction(myfunction))
, intentará ejecutar la myfunction/0
.
Por lo tanto, debe utilizar 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
.
Hay una excelente publicación en el blog sobre este comportamiento: link
Dos tipos de funciones
Si un módulo contiene esto:
fac(0) when N > 0 -> 1; fac(N) -> N* fac(N-1).
No puedes simplemente cortar y pegar esto en la cubierta y obtener el mismo resultado.
Es porque hay un error en Erlang. Los módulos en Erlang son secuencias de FORMAS . La cáscara de Erlang evalúa una secuencia de EXPRESIONES . En Erlang las FORMAS 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. Este poco de estupidez ha sido Erlang desde siempre, pero no lo notamos y aprendimos a vivir con él.
Punto en 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 las funciones escribiendo f (10) no f. (10); esto 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 sé cuán útil será esto para nadie más, pero la forma en que finalmente envolví mi cabeza en torno al concepto fue darme cuenta de que las funciones del elixir no son funciones.
Todo en el elixir es una expresión. Asi que
MyModule.my_function(foo)
no es una función, pero la expresión se devuelve 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 la función anónima.
Es tentador referirse a la fn o la notación como un puntero de función, pero en realidad es mucho más. Es un cierre del entorno circundante.
Si te preguntas:
¿Necesito un entorno de ejecución o un valor de datos en este lugar?
Y si necesita ejecución fn, entonces la mayoría de las dificultades se vuelven mucho más claras.
Nunca he entendido por qué las explicaciones de esto son tan complicadas.
En realidad, es solo una distinción excepcionalmente pequeña combinada con las realidades del estilo de Ruby "ejecución de función sin parens".
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 condef
. -
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 se hace referencia o simplemente hace referencia a un valor fuera de la memoria?
No hay una buena manera de saber en tiempo de compilación. Ruby tiene el lujo de realizar una introspección del espacio de nombres de variables para averiguar si un enlace variable ha sombreado una función en algún momento en el tiempo. El elixir, al ser 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. Imagina que no había una notación de puntos. Considere 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é tiene que darle la pista a Elixir. ¿Imagina si cada variable de-referencia tuviera que verificar una función? Alternativamente, imagina qué actos heroicos serían necesarios para inferir siempre que la desreferencia variable estaba usando una función.
Puede que me equivoque ya que nadie lo mencionó, pero también tenía la impresión de que la razón de esto es también la herencia de rubí de poder llamar a funciones sin corchetes.
Arity obviamente está involucrado, pero dejémoslo 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í te 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 quiere llamar a la función. Esto es lo que el &
es para. Si deja arity appart por un segundo, añadir el nombre de su función con &
significa que desea utilizar explícitamente esta función como argumento, no lo que esta función devuelve.
Ahora la función anónima es un poco diferente en que se usa principalmente como argumento. Nuevamente, esta es una elección de diseño, pero lo racional detrás de esto es que es utilizado principalmente por iteradores de tipo de funciones que toman funciones como argumentos. Obviamente, no es necesario usar &
porque ya se consideran argumentos de forma predeterminada. Es su propósito.
Ahora, el último problema es que a veces tiene que llamarlos en su código, porque no siempre se usan con una función de tipo iterador, o puede que esté 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 método de call
en el objeto. De esa manera, podría mantener el comportamiento no obligatorio de los paréntesis.
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)
Una vez más, esta es una opción de diseño. Ahora el elixir no está orientado a objetos y, por lo tanto, la llamada no usa el primer formulario con seguridad. No puedo hablar por José, pero parece que la segunda forma se usó en elixix porque todavía parece una llamada de función con un carácter adicional. Es 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 salirse con la suya solo con los corchetes siempre que los corchetes sean obligatorios para las funciones anónimas. Parece que es:
Corchetes obligatorios VS Notación ligeramente diferente
En ambos casos, usted hace una excepción porque hace que ambos se comporten de manera diferente. Ya que hay una diferencia, también puede ser obvio e ir por la notación diferente. Los paréntesis obligatorios parecerí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. También la mayoría son opciones de diseño y traté de dar una razón para ellos sin juzgarlos. Me encanta el elixir, me encanta el rubí, me gustan las llamadas de función sin paréntesis, pero como usted, encuentro las consecuencias bastante erróneas de vez en cuando.
Y en el elixir, es solo este punto extra, mientras que en el rubí tienes bloques encima de esto. Los bloques son asombrosos y me sorprende lo mucho que puedes hacer con solo los bloques, pero solo funcionan cuando necesitas una función anónima, que es el último argumento. Entonces, como debería poder lidiar con otros escenarios, aquí viene todo el método / lambda / proc / block confusión.
De todos modos ... esto está fuera de alcance.
Solo para aclarar el nombre, 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í.
Empecemos por 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 puede tener múltiples cláusulas también:
x = 1
fun = fn
y when y < 0 -> x - y
y -> x + y
end
fun.(2) #=> 3
fun.(-2) #=> 3
Ahora, intentemos algo diferente. Intentemos 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! Obtenemos un error! No podemos mezclar cláusulas que esperan 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 podrá compilarse porque cada vez que vea una def
, obtendrá un ámbito de variable vacío. Esa es una diferencia importante entre ellos. Me gusta particularmente 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. Usted mismo lo mencionó:
other_function(&hello(&1))
Y luego preguntas, ¿por qué no puedo simplemente pasarlo tan bien como en otros idiomas? Eso es porque las funciones en Elixir se identifican por nombre y aridad. Así que una función que espera dos argumentos es una función diferente a la que espera tres argumentos, incluso si tuvieran el mismo nombre. Entonces, si simplemente pasáramos hello
, no tendríamos idea de a qué se refería. ¿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 diferentes aridades.
Desde Elixir v0.10.1, tenemos una sintaxis para capturar funciones nombradas:
&hello/1
Eso capturará la función local llamada hola con aridad 1. A lo largo del lenguaje y su documentación, es muy común identificar funciones en esta sintaxis hello/1
.
Esta es también la razón por la que Elixir usa un punto para llamar a funciones anónimas. Ya que no puede simplemente pasar hello
como una función, en lugar de eso, debe capturarla explícitamente, hay una distinción natural entre las funciones anónimas y con nombre y una sintaxis distinta para llamar a cada una hace que todo sea un poco más explícito (Lispers estaría familiarizado esto debido a la discusión de 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.