Funciones
En Elixir y en muchos lenguajes funcionales, las funciones son ciudadanos de primera clase. Vamos a aprender acerca de los tipos de funciones en Elixir, qué los hace diferentes, y cómo usarlos.
Funciones anónimas
Tal como el nombre sugiere, una función anónima no tiene nombre.
Como vimos en la lección Enum
, son pasadas frecuentemente a otras funciones.
Para definir una función anónima en Elixir necesitamos las palabras clave fn
y end
.
Dentro de estos podemos definir, separados por ->
, cualquier número de parámetros y el cuerpo de la función.
Vamos a ver un ejemplo básico:
iex> sum = fn (a, b) -> a + b end
iex> sum.(2, 3)
5
El atajo &
Usar funciones anónimas es una práctica común en Elixir, hay un atajo para hacer esto:
iex> sum = &(&1 + &2)
iex> sum.(2, 3)
5
Como probablemente ya has adivinado, en la versión reducida nuestros parámetros están disponibles como: &1
, &2
, &3
.
Coincidencia de patrones
La coincidencia de patrones no está limitada solo a las variables en Elixir, puede ser aplicada a las firmas de la función como veremos en esta sección.
Elixir usa coincidencia de patrones para identificar el primer conjunto de parámetros que coincidan e invocar al cuerpo correspondiente:
iex> handle_result = fn
...> {:ok, result} -> IO.puts "Handling result..."
...> {:ok, _} -> IO.puts "This would be never run as previous will be matched beforehand."
...> {:error} -> IO.puts "An error has occurred!"
...> end
iex> some_result = 1
1
iex> handle_result.({:ok, some_result})
Handling result...
:ok
iex> handle_result.({:error})
An error has occurred!
Funciones con nombre
Podemos definir funciones con nombre para así poder referirnos a ellas luego.
Estas funciones con nombre son definidas con la palabra clave def
dentro de un módulo.
Vamos a aprender más acerca de los módulos en las siguientes lecciones, por ahora nos enfocaremos solamente en las funciones con nombre.
Las funciones definidas dentro de un módulo están disponibles para ser usadas por otros módulos.
defmodule Greeter do
def hello(name) do
"Hello, " <> name
end
end
iex> Greeter.hello("Sean")
"Hello, Sean"
Si el cuerpo de nuestra función solo se extiende a una línea, podemos acortarla con , do:
:
defmodule Greeter do
def hello(name), do: "Hello, " <> name
end
Armados con nuestro conocimiento de coincidencia de patrones, vamos a explorar la recursión usando funciones con nombre:
defmodule Length do
def of([]), do: 0
def of([_ | tail]), do: 1 + of(tail)
end
iex> Length.of []
0
iex> Length.of [1, 2, 3]
3
Nombre de funciones y aridad
Anteriormente mencionamos que las funciones son nombradas por la combinación de nombre y aridad (número de argumentos). Esto significa que puedes hacer cosas como:
defmodule Greeter2 do
def hello(), do: "Hello, anonymous person!" # hello/0
def hello(name), do: "Hello, " <> name # hello/1
def hello(name1, name2), do: "Hello, #{name1} and #{name2}"
# hello/2
end
iex> Greeter2.hello()
"Hello, anonymous person!"
iex> Greeter2.hello("Fred")
"Hello, Fred"
iex> Greeter2.hello("Fred", "Jane")
"Hello, Fred and Jane"
Enumeramos los nombres de las funciones en los comentarios anteriores.
La primera implementación no recibe argumentos, su equivalente es hello/0
; la segunda función recibe un argumento equivalente a hello/1
, y así.
A diferencia de la sobrecarga en otros lenguajes, estas son consideradas funciones diferentes entre sí.
(La coincidencia de patrones, descrita anteriormente, aplica solo cuando se definen varias funciones con el mismo nombre y el mismo número de argumentos).
Funciones y coincidencia de patrones
Detrás de escenas, las funciones se ajustan a el numero de argumentos con los que se llaman.
Digamos que necesitamos una función para aceptar un mapa, pero solo nos interesa utilizar una clave en particular. Podemos coincidir el argumento con la clave de la siguiente forma:
defmodule Greeter1 do
def hello(%{name: person_name}) do
IO.puts "Hello, " <> person_name
end
end
Digamos que tenemos el siguiente mapa describiendo a una persona llamada Fred:
iex> fred = %{
...> name: "Fred",
...> age: "95",
...> favorite_color: "Taupe"
...> }
Estos son los resultados que obtenemos al llamar Greeter1.hello/1
con el mapa fred
:
# call with entire map
...> Greeter1.hello(fred)
"Hello, Fred"
¿Qué sucede cuando llamamos la función con un mapa que no contiene la clave :name
?
# call without the key we need returns an error
...> Greeter1.hello(%{age: "95", favorite_color: "Taupe"})
** (FunctionClauseError) no function clause matching in Greeter1.hello/1
The following arguments were given to Greeter1.hello/1:
# 1
%{age: "95", favorite_color: "Taupe"}
iex:12: Greeter1.hello/1
La razón de este comportamiento es que Elixir busca la coincidencia de los argumentos con los que se llama la función con la aridad con la que se define la función.
Pensemos en como se ven los datos cuando llegan a Greeter1.hello/1
:
# incoming map
iex> fred = %{
...> name: "Fred",
...> age: "95",
...> favorite_color: "Taupe"
...> }
Greeter1.hello/1
espera un argumento como el siguiente:
%{name: person_name}
En Greeter1.hello/1
, el mapa que pasamos (fred
) se evalúa comparandolo con nuestro argumento (%{name: person_name}
):
%{name: person_name} = %{name: "Fred", age: "95", favorite_color: "Taupe"}
Encuentra que existe una clave que corresponde a :name
en el mapa proporcionado.
¡Tenemos una coincidencia! y como resultado de esta coincidencia exitosa, el valor de la clave :name
en el mapa de la derecha (Por ejemplo el mapa fred
) está vinculado a la variable de la izquierda (person_name
).
Ahora, ¿qué sucede si quisiéramos asignar el nombre de Fred a person_name
pero TAMBIÉN quisiéramos acceder a todo el mapa? Digamos que queremos hacer IO.inspect(fred)
despues de saludarlo.
En este punto, debido a que solo buscamos la clave :name
en nuestro mapa, solo vinculamos el valor de esa clave a una variable, la función no tiene conocimiento del resto del mapa.
Para poder conservarlo, debemos asignar ese mapa completo a su propia variable para que podamos utilizarlo.
Empecemos una nueva función:
defmodule Greeter2 do
def hello(%{name: person_name} = person) do
IO.puts "Hello, " <> person_name
IO.inspect person
end
end
Recuerda que Elixir buscará la coincidencia a medida de que se presenta. Por lo tanto, en este caso, cada lado buscará la coincidencia con el argumento entrante y se unirá a lo que corresponda. Tomemos el lado derecho primero:
person = %{name: "Fred", age: "95", favorite_color: "Taupe"}
Ahora, person
ha sido evaluado y vinculado a todo el mapa de fred.
Pasemos a la siguiente coincidencia:
%{name: person_name} = %{name: "Fred", age: "95", favorite_color: "Taupe"}
Esto es lo mismo que nuestra función original Greeter1
en la que la que solo buscábamos la coincidencia con el mapa y solo reteníamos el nombre de Fred.
Lo que hemos logrado son dos variable que podemos usar en lugar de una:
-
person
, refiriéndose a%{name: "Fred", age: "95", favorite_color: "Taupe"}
-
person_name
, refiriéndose a"Fred"
Así que ahora cuando llamamos Greeter2.hello/1
, podemos usar toda la información de Fred:
# call with entire person
...> Greeter2.hello(fred)
"Hello, Fred"
%{age: "95", favorite_color: "Taupe", name: "Fred"}
# call with only the name key
...> Greeter2.hello(%{name: "Fred"})
"Hello, Fred"
%{name: "Fred"}
# call without the name key
...> Greeter2.hello(%{age: "95", favorite_color: "Taupe"})
** (FunctionClauseError) no function clause matching in Greeter2.hello/1
The following arguments were given to Greeter2.hello/1:
# 1
%{age: "95", favorite_color: "Taupe"}
iex:15: Greeter2.hello/1
Así que hemos visto que las coincidencias en Elixir se ajustan a múltiples profundidades porque cada argumento se compara con los datos entrantes de forma independiente, dejándonos las variables para llamarlas dentro de nuestra función.
Si cambiamos el orden de %{name: person_name}
y person
en la lista, obtendremos los mismos resultados ya que cada uno coincide con fred por su cuenta.
Cambiamos la variable y el mapa:
defmodule Greeter3 do
def hello(person = %{name: person_name}) do
IO.puts "Hello, " <> person_name
IO.inspect person
end
end
Y llamémoslo con los mismos datos que usamos en Greeter2.hello/1
:
# call with same old Fred
...> Greeter3.hello(fred)
"Hello, Fred"
%{age: "95", favorite_color: "Taupe", name: "Fred"}
Recordemos que aunque parezca que %{name: person_name} = person
hace coincidir los patrones con %{name: person_name}
contra la variable person
, en realidad esta haciendo coincidir los patrones con los argumentos proporcionados.
Resumen: Las funciones buscan coincidencia con cada uno de los datos proporcionados de forma independiente. Podemos usar esto para vincular valores a variables separadas dentro de la función.
Funciones privadas
Cuando no queremos que otros módulos accedan a una función específica, podemos hacer que la función sea privada.
Las funciones privadas solo pueden ser llamadas desde dentro de su propio módulo.
Las definimos en Elixir con defp
:
defmodule Greeter do
def hello(name), do: phrase() <> name
defp phrase, do: "Hello, "
end
iex> Greeter.hello("Sean")
"Hello, Sean"
iex> Greeter.phrase
** (UndefinedFunctionError) function Greeter.phrase/0 is undefined or private
Greeter.phrase()
Guardias
Hemos cubierto brevemente las guardias en la lección Estructuras de control, ahora veremos cómo aplicarlas a las funciones con nombre. Una vez Elixir ha hecho coincidencia con una función, de existir, las guardias serán evaluadas.
En el siguiente ejemplo tenemos dos funciones con la misma firma, confiamos en las guardias para determinar cuál usar basándonos en el tipo de los argumentos:
defmodule Greeter do
def hello(names) when is_list(names) do
names = Enum.join(names, ", ")
hello(names)
end
def hello(name) when is_binary(name) do
phrase() <> name
end
defp phrase, do: "Hello, "
end
iex> Greeter.hello ["Sean", "Steve"]
"Hello, Sean, Steve"
Argumentos por defecto
Si queremos un valor por defecto para un argumento usamos la sintaxis argument \\ value
:
defmodule Greeter do
def hello(name, language_code \\ "en") do
phrase(language_code) <> name
end
defp phrase("en"), do: "Hello, "
defp phrase("es"), do: "Hola, "
end
iex> Greeter.hello("Sean", "en")
"Hello, Sean"
iex> Greeter.hello("Sean")
"Hello, Sean"
iex> Greeter.hello("Sean", "es")
"Hola, Sean"
Cuando combinamos nuestro ejemplo de guardias con argumentos por defecto, nos encontramos con un problema, vamos a ver algo que podría ser similar:
defmodule Greeter do
def hello(names, language_code \\ "en") when is_list(names) do
names = Enum.join(names, ", ")
hello(names, language_code)
end
def hello(name, language_code \\ "en") when is_binary(name) do
phrase(language_code) <> name
end
defp phrase("en"), do: "Hello, "
defp phrase("es"), do: "Hola, "
end
** (CompileError) iex:8: def hello/2 defines defaults multiple times. Elixir allows defaults to be declared once per definition. Instead of:
def foo(:first_clause, b \\ :default) do ... end
def foo(:second_clause, b \\ :default) do ... end
one should write:
def foo(a, b \\ :default)
def foo(:first_clause, b) do ... end
def foo(:second_clause, b) do ... end
A Elixir no le gustan los parámetros por defecto en múltiples coincidencias de funciones, esto puede ser confuso. Para manejar esto podemos agregar una función al inicio con nuestros argumentos por defecto:
defmodule Greeter do
def hello(names, language_code \\ "en")
def hello(names, language_code) when is_list(names) do
names = Enum.join(names, ", ")
hello(names, language_code)
end
def hello(name, language_code) when is_binary(name) do
phrase(language_code) <> name
end
defp phrase("en"), do: "Hello, "
defp phrase("es"), do: "Hola, "
end
iex> Greeter.hello ["Sean", "Steve"]
"Hello, Sean, Steve"
iex> Greeter.hello ["Sean", "Steve"], "es"
"Hola, Sean, Steve"
¿Encontraste un error o quieres contribuir a la lección? ¡Edita esta lección en GitHub!