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

Resuelto Alex Marandon asked hace 11 años • 9 respuestas

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)

Sólo 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 empaquetada en un archivo fn. Parece que hay algo de azúcar sintáctico otherfunction(&myfunction(&1, &2))para hacerlo fácil, pero ¿por qué es necesario en primer lugar? ¿Por qué no podemos simplemente hacerlo otherfunction(myfunction))? ¿Es solo para permitir llamar a funciones del módulo 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 Erlang VM?

¿Hay algún beneficio en tener dos tipos de funciones y convertir de un tipo a otro para pasarlas a otras funciones? ¿Existe alguna ventaja en tener dos notaciones diferentes para llamar a funciones?

Alex Marandon avatar Aug 02 '13 15:08 Alex Marandon
Aceptado

Sólo 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í.

Empecemos por el segundo fn. fnes un cierre, similar a 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 varias cláusulas:

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

Ahora, probemos 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! ¡Recibimos 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 era de esperar, 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 podrá compilar porque cada vez que ve un def, obtiene un alcance de variable vacío. Ésa es una diferencia importante entre ellos. Me gusta especialmente el hecho de que cada función nombrada comienza con un borrón y cuenta nueva y no se mezclan todas las variables de diferentes ámbitos. Tienes un límite claro.

Podríamos recuperar la función hola nombrada arriba como una función anónima. Tú mismo lo mencionaste:

other_function(&hello(&1))

Y luego preguntaste, ¿por qué no puedo simplemente pasarlo como helloen 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 tuvieran el mismo nombre. Entonces, si simplemente aprobamos hello, no tendríamos idea de a qué hellote refieres realmente. ¿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 con nombre:

&hello/1

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

Esta es también la razón por la que Elixir usa un punto para llamar a funciones anónimas. Dado que no puede simplemente pasarla hellocomo una función, sino que 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 hace que todo sea un poco más explícito (Lispers estaría familiarizado con esto debido a la discusión entre Lisp 1 y Lisp 2).

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

José Valim avatar Aug 02 '2013 18:08 José Valim

No sé qué tan útil será esto para nadie má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. 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, sólo hay una forma de obtener una "Función" que pueda pasar como argumento y es utilizar la notación de función anónima.

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

Fred the Magic Wonder Dog avatar May 22 '2015 16:05 Fred the Magic Wonder Dog