Conceptos básicos de Elixir
Introducción
Este notebook contiene una pequeña introducción sobre el lenguaje de programación Elixir
Exploraremos los conceptos básicos para definir una estrategia de movimientos para el juego Elixir Adventure, desarrollado por el equipo de Truedat.
Para más información sobre Elixir, puedes acceder a los siguientes enlaces: Elixir’s Getting Started guide y Elixir Learning.
Iniciamos
¿Qué es lo que aprenderemos?
- Elixir Basic Types
- Pattern matching
- Inmutability
- Piping
-
Enum:
- .filter
- .find
- Send / Receive
¿Intrigado? ¡Empecemos!
Elixir basic types
Los tipos básicos de Elixir son: numbers, strings y variables. Los comentarios de código empiezan con #
:
# Numbers
IO.inspect(40 + 2)
# Strings
variable = "hello" <> " world"
IO.inspect(variable)
Ejecutando la celda de arriba, se imprime el numero 42
y el string "hello world"
. Para ello
usaremos la función inspect
del módulo IO
, lo utilizaremos de la siguiente manera: IO.inspect(...)
. Esta función imprime la estructura de datos en la terminal. En este caso del notebook y retorna el valor dado.
En elixir también existen tres valores especiales true
, false
y nil
.
Todo en Elixir es considerado un valor verdadero, excepto por false
y nil
:
# && is the logical and operator
IO.inspect(true && true)
IO.inspect(13 && 42)
# || is the logical or operator
IO.inspect(true || false)
IO.inspect(nil || 42)
Para trabajar con colecciones de datos, Elixir tiene tres tipos:
- Lists (Listas)
- Tuples (Tuplas)
- Maps (Mapas)
# Lists (typically hold a dynamic amount of items)
IO.inspect([1, 2, "three"])
# Tuples (typically hold a fixed amount of items)
IO.inspect({:ok, "value"})
# Maps (key-value data structures)
IO.inspect(%{"key" => "value"})
Si inspeccionamos el código de arriba, podemos observar el dato :ok
.
En Elixir todos los valores que empiezan con :
son llamados atoms (átomos). Los atoms son utilizados como identificadores a través del lenguaje. Comúnmente son :ok
y :error
. Los veremos en la próxima sección: Pattern matching.
Pattern matching
En Elixir el operador =
funciona de una manera un poco diferente a como funciona en otros lenguajes de programación.
x = 1
x
Por aquí todo bien, pero ¿qué pasa si invertimos los operadores?
1 = x
¡Funciona! Esto es por que Elixir trata de hacer match del lado derecho contra el lado izquierdo, y dado que los dos son 1
funciona. Intentemos otra cosa:
2 = x
Ahora los lados no son iguales y retorna un match error. En Elixir también se utiliza el pattern maching para las colecciones. Por ejemplo podemos utilizar un [head | tail]
para extraer el head (el primer elemento) y el tail (el resto) de una lista.
[head | tail] = [1, 2, 3]
IO.inspect(head)
IO.inspect(tail)
Si intentamos hacer un matching con una lista vacía contra un [head | tail]
nos retorna un error de tipo match.
[head | tail] = []
Finalmente, podemos utilizar la expresión [head | tail]
para añadir elementos al principio de la lista.
list = [1, 2, 3]
[0 | list]
También podemos realizar el pattern macthing con tuplas. Esto se suele utilizar para hacer match con el tipo de resultado en la llamada de una función. Por ejemplo, la función Date.from_iso8601(string)
retorna {:ok, date}
si el string representa una fecha válida en un formato YYYY-MM-DD
, por el contrario retorna un {:error, reason}
:
# A valid date
Date.from_iso8601("2020-02-29")
# An invalid date
Date.from_iso8601("2020-02-30")
Ahora, ¿qué pasaría si queremos que nuestro código tenga un comportamiento
diferente si la fecha es válida o no? Podemos utilizar un case
con un pattern maching con las diferentes tuplas:
# Give an invalid date as input
input = "2020-02-30"
# And then match on the return value
case Date.from_iso8601(input) do
{:ok, date} ->
"We got a valid date: #{inspect(date)}"
{:error, reason} ->
"Oh no, the date is invalid. Reason: #{inspect(reason)}"
end
En este ejemplo, estamos utilizando case
para realizar un pattern matching con el retorno
de la función Date.from_iso8601
. Tenemos un case
con dos cláusulas, una hace un match con
{:ok, date}
y otra con {:error, reason}
.
Ahora intenta cambiar la variable del input
del código de arriba y reevalua la celda.
¿Qué pasa cuando tienes una fecha válida?
También podemos utilizar el pattern maching con mapas. Esto se utiliza para extraer los valores de las claves.
map = %{:elixir => :functional, :python => :object_oriented}
%{:elixir => type} = map
type
Si la clave no existe en el mapa, esto provoca una excepción.
%{:c => type} = map
Pero esto no se termina aquí, una las ventajas de que Elixir sea un lenguaje funcional, podemos realizar un pattern matching utilizando las funciones, combinando lo aprendido anteriormente.
Utilizando el ejemplo de las fechas visto anteriormente, podemos acceder a diferentes funciones dependiendo de lo que recibamos en los parámetros, en nuestro caso un {:ok, date}
y un {:error, reason}
.
defmodule MyModule do
def check_my_date({:ok, date}) do
"We got a valid date: #{inspect(date)}"
:valid_date
end
def check_my_date({:error, reason}) do
"Oh no, the date is invalid. Reason: #{inspect(reason)}"
:invalid_date
end
end
input = "2020-02-30"
MyModule.check_my_date(Date.from_iso8601(input))
Si cambiamos el input
a una fecha correcta del código de arriba podemos observar un comportamiento parecido al del case
Inmutabilidad
Inmutabilidad implica que el estado de los datos no puede ser alterado después de ser creado.
Por ejemplo:
my_map = %{a: "valor_a"}
IO.inspect(Map.put(my_map, :b, "valor_b"), label: "Nuevo mapa")
IO.inspect(my_map, label: "Mapa original sigue siendo lo mismo")
Esto no significa que una variable no se pueda reasignar:
my_map = %{a: "valor_a"}
IO.inspect(my_map, label: "my_map")
my_map = Map.put(my_map, :b, "valor_b")
IO.inspect(my_map, label: "my_map reasignado a nuevo mapa")
Piping
El operador pipe permite componer funciones en Elixir. Toma el resultado de la expresión anterior y lo pasa como primer argumento de la siguiente función.
"Elixir mola" |> String.split()
"Elixir mola"
|> String.upcase()
|> String.split()
Esto evita tener que hacer una anidación de funciones:
String.split(String.upcase("Elixir mola"))
Enum
Un enumerable es cualquier dato sobre el que se pueda iterar. Por ejemplo, listas y mapas. El módulo Enum permite transformar y operar sobre enumerables. Muchas de las funciones de Enum son funciones “higher-order”, a las que le pasamos datos y una función que opera sobre esos datos.
Enum.filter
filtra elementos de un enumerable según la función que se le pase. Si la función devuelve true para un elemento, se conserva; si no, se filtra. Por ejemplo, para quedarnos con los elementos pares de una lista:
Enum.filter([1, 2, 3], fn x -> rem(x, 2) == 0 end)
Otro ejemplo: filtrar elementos “interesantes” en un mapa de puntos. La clave es una tupla de coordenadas {x,y}, y el valor una cadena que describe una “característica” del punto.
puntos = %{
{1, 2} => "interesante",
{3, 4} => "interesante",
{5, 6} => "no_interesante"
}
puntos_interesantes =
Enum.filter(
puntos,
fn {_coordenada, caracteristica} -> caracteristica == "interesante" end
)
IO.inspect(puntos_interesantes, label: "Puntos interesantes")
Al iterar sobre un mapa, la función recibe un tupla {clave, valor}
, en este caso {coordenada, caracteristica}
.
Enum.find
busca en un enumerable el primer elemento que cumpla lo especificado en la función. Si la función devuelve true en un elemento, find
lo retorna. Si la función devuelve false para todos los elementos, find devuelve un nil
.
puntos = %{
{1, 2} => "primer_punto",
{3, 4} => "segundo_punto",
{5, 6} => "tercer_punto"
}
{coordenada, _nombre_punto} =
Enum.find(
puntos,
fn {_coordenada, nombre_punto} -> nombre_punto == "segundo_punto" end
)
IO.inspect(coordenada, label: "La coordenada buscada es")
Processes Send / Receive
En Elixir, todo el código se ejecuta dentro de procesos. Los procesos están aislados entre sí, y corren concurrentemente comunicandose mediante el paso de mensajes. Los procesos no solo son la base de la concurrencia en Elixir, además proporcionan los medios para construir programas distribuidos y tolerantes a fallos.
Al ser procesos muy ligeros, es posible no sólo tener cientos, si no miles de procesos ejecutandose concurrentemente.
Para poder enviar mensajes a procesos, utilizamos la función send/2
y recibimos con la función receive/1
send(self(), {:hello, "world"})
receive do
{:hello, message} -> message
{:world, _message} -> "won't match"
end
Si evaluamos el código anterior podemos ver que nos enviamos un mensaje a nosotros mismos utilizando la función send(PID, message)
, el PID es el identidicador único del proceso al que queremos enviar el mensaje, en nuestro caso es nuestro propio PID y lo obtenemos por la función self()
, en el segundo parámetro se encuentra el mensaje a enviar.
Cuando se envía un mensaje a un proceso, este mensaje se almacena en el buzón del proceso. El bloque del receive
pasa por el buzón del proceso actual y busca el mensaje que coincida con cualquiera de los patterns especificados.
Como podemos observar, aquí también utilizamos el pattern matching en tuplas para discernir que tipo de mensaje estamos recibiendo y que acción queremos tomar.
Y con esto finalizamos esta guía básica de Elixir…
Y esperamos que no sea el fin, te einvitamos a seguir conociendo este maravilloso lenguaje de programación.
Si te interesa, te invitamos a las charlas que se dan en Meetup Madrid-Elxir
Si quieres aprender más, te dejamos estos enlaces
Si quieres conocer sobre el proyecto de este LiveBook te dejamos el siguiente enlace:
Finalmente queremos agradecer a la Universidad Politécnica de Madrid por permitirnos participar en TryIt.
¡Esperamos veros pronto!