Introducción al lenguaje de programación Elixir

Cristian Ernesto Olmos Casanova
Pragma
Published in
10 min readJun 22, 2024

Hoy en día, se requiere que las aplicaciones soporten más carga de trabajo y tengan mejor rendimiento en un mundo donde el usuario final demanda mejores tiempos de respuesta y capacidad para soportar fallos.

Existen muchas soluciones a estas demandas que incluyen componentes especializados para satisfacer estas necesidades técnicas y funcionales. Sin embargo, algunas soluciones pueden ser costosas de implementar debido a su complejidad o al costo de implementar operaciones asíncronas, paralelas y concurrentes. Por lo tanto, no son soluciones viables si se necesita construir aplicaciones de este tipo en un corto período de tiempo. Desde mi rol como Desarrollador Backend, les presento el lenguaje de programación Elixir, una solución que Bancolombia ha estado utilizando en los últimos años con buenos resultados. A diferencia de otros lenguajes como Java con Spring Webflux o Golang, Elixir tiene un enfoque completamente diferente para manejar cargas de trabajo intensas y su administración de recursos es más eficiente.

Elixir es un lenguaje de programación funcional, dinámico y de propósito general con un fuerte enfoque en la concurrencia y paralelismo, fue creado por José Valim con el objetivo de extraer los beneficios que tiene la maquina virtual BEAM (Máquina virtual de Erlang) en la construcción de servicios distribuidos y de alta tolerancia a fallos. Además, usa una sintaxis amigable y su ruta de aprendizaje es más sencilla que la ruta de aprendizaje de Java o Erlang (Lenguaje de programación).

Si bien es un lenguaje relativamente nuevo, cuenta con el respaldo de una comunidad en constante crecimiento. Esta comunidad realiza actualizaciones regulares, lo que contribuye a la robustez del lenguaje y garantiza que esté al día con las últimas funcionalidades. Esto ha llevado a empresas como Pinterest, Discord o Bancolombia a incluirlo dentro de sus stack tecnológicos debido a su eficiente manejo de procesos, programación asíncrona y no bloqueante, tolerancia a fallos y escalabilidad.

Definición de coincidencia de patrones o pattern matching

No podemos realizar la introducción a Elixir sin primero conocer qué es la coincidencia de patrones o pattern matching. La coincidencia de patrones es una técnica utilizada en la programación para encontrar y reconocer ciertos patrones dentro de una secuencia de datos. Se puede usar para extraer datos útiles de una estructura de datos, evaluar expresiones dentro de un condicional o diferenciar el uso de una función definida varias veces con el mismo nombre. En Elixir, es común usar esta técnica porque nos permite encontrar datos o evaluar expresiones de manera más sencilla y visible.

Definición de una variable

Definimos una variable de la siguiente manera:

defmodule DefinicionVariable do
mi_nombre = "John Doe"
IO.inspect(mi_nombre)
end

Como se puede observar, automáticamente Elixir infiere el tipo de dato que se define en la variable mi_nombre.

Definición de condicionales

Elixir cuenta con diferentes bloques de condicionales que se presenta a continuación:

Condicional if-else

Este tipo de condicional evalúa una expresión y ejecuta el bloque de código dentro de if si es verdadero. Si la expresión es falsa, ejecuta el bloque de código dentro de else. La estructura de este condicional se define de la siguiente manera:

defmodule CondicionalIf do
a = 5
if a > 10 do
IO.puts("a es mayor que 10")
else
IO.puts("a es menor o igual que 10")
end
end

Condicional unless-else

La diferencia entre este condicional y el if-else radica en que, en el condicional unless, la condición se cumple cuando es falsa. Por otro lado, en el if-else, la condición dentro del if se cumple cuando es verdadera. A continuación, presentamos un ejemplo de cómo definir este tipo de condicional:

defmodule CondicionalUnless do
a = false
unless a do
IO.puts("La condición no se cumple")
else
IO.puts("¡La condición se cumple!")
end
end

Este ejemplo demuestra que si la condición dentro de unless no se cumple, se mostrará el mensaje ‘La condición no se cumple’. Si se cumple, se mostrará ‘¡La condición se cumple!’.

Condicional case-do

Este condicional permite el uso de coincidencia de patrones para evaluar expresiones, es útil cuando hay pocos posibles resultados dentro de una expresión. Su estructura se define de la siguiente manera:

defmodule CondicionalCase do
valor = 3
case valor do
1 -> IO.puts("El valor es 1")
2 -> IO.puts("El valor es 2")
_ -> IO.puts("El valor no es ni 1 ni 2")
end
end

En este ejemplo, se observa que no se requiere ninguna operación de igualdad o módulo para evaluar la expresión valor, debido a que la coincidencia de patrones evalúa dicha expresión. Hay tres posibles resultados y son los siguientes:

  1. Si la variable valor posee un valor de 1, se imprime el mensaje “El valor es 1”.
  2. Si la variable valor posee un valor de 2, se imprime el mensaje “El valor es 2”.
  3. Si la variable valor no posee ninguno de los valores anteriores, se imprime el mensaje “El valor no es ni 1 ni 2”.

Es importante definir una expresión por defecto en este condicional. Si no se define, Elixir arroja un error tipo CaseClauseError. En este caso, el valor por defecto es _.

Condicional cond-do

Este tipo de condicional es útil cuando existen múltiples resultados posibles y se desea evitar la anidación de numerosos condicionales if-else/unless-else. La ejecución se realiza en orden y se define de la siguiente manera:

defmodule CondicionalCond do
valor = cond do
2 + 2 == 5 -> "Esto no será verdadero"
2 * 2 == 2 -> "Tampoco esto"
true -> "Por si las moscas, esto sí"
end
IO.puts(valor)
end

Dentro de este condicional, en cada línea de código se evalúa una expresión diferente y con esta implementación se logra evitar el uso de if-else/unless-else anidados. Además, es importante definir una expresión por defecto porque de lo contrario, Elixir arroja un error tipo CondClauseError. En este caso, la expresión por defecto es true.

Definición de una función

Para definir una función en Elixir, se sigue el siguiente formato:

defmodule Operaciones do
def division(num1, num2) do
if num2 == 0 do
IO.puts("No se puede dividir por cero")
else
IO.puts(num1 / num2)
end
end
end

Primero hay que definir un módulo (en Java es equivalente a una clase) con la palabra reservada defmodule, en este caso nuestro módulo se llama Operaciones que contiene una función llamada division/2, esta función recibe dos parámetros y la creamos con la palabra reservada def. Los parámetros de una función también se infieren cuando se agregan en cualquier función de Elixir.

Estructura de datos en Elixir

Elixir cuenta con diversas estructuras de datos que nos permite organizar y manipular de manera más eficiente y organizada los datos que se definan allí.

Listas

Definimos una lista de la siguiente manera:

defmodule Lista do
mi_lista = [1, 2, 3, 4, 5]
end

Mediante el módulo List de elixir, podemos manipular el contenido de esta estructura de datos.

Tuplas

Definimos una tupla de la siguiente manera:

defmodule Tupla do
mi_tupla = {:ok, "mensaje", 123}
end

Esta estructura de datos es comúnmente utilizada en Elixir y podemos manipularla utilizando el módulo Tuple. Un ejemplo práctico de su uso puede ser la estructura de respuesta que se obtiene de un servicio externo.

Mapas

Definimos un mapa de la siguiente manera:

defmodule Mapa do
mi_mapa = %{
:nombre => "John Doe",
:edad => 30
}
end

Mediante el módulo Map de elixir, podemos manipular el contenido de esta estructura de datos.

Struct

Aunque Elixir no proporciona la capacidad de definir Objetos en la misma forma que lenguajes como Java o C#, ofrece la flexibilidad de definir estructuras personalizadas según las necesidades del desarrollador. Podemos definir una estructura personalizada en Elixir de la siguiente manera:

defmodule UserStruct do
defstruct [:name, :age]
def create_user do
%UserStruct{
name: "John Doe",
age: 30
}
end
end

defmodule User do
user = UserStruct.create_user()
IO.inspect(user)
end

Primero definimos un módulo y luego creamos la estructura con la palabra reservada defstruct. Como ejemplo, tenemos la estructura UserStruct que contiene dos campos, en el módulo User creamos la estructura y se imprime el resultado con IO.inspect/2.

Átomos

Los átomos son una estructura de datos en Elixir que se utilizan a menudo, aunque pueden no ser tan conocidos como otras estructuras. Son constantes, y su valor es el mismo que su nombre. Esto significa que si se define una variable con un valor de :<valor>, el <valor> se considera un átomo, tal que así:

defmodule Atomo do
mi_atomo = :hola
IO.inspect(mi_atomo)
end

En este contexto, hemos definido un átomo con el valor :hola. Los átomos, siendo constantes e inmutables, pueden integrarse con otras estructuras de datos en Elixir, como las tuplas o las listas. Esto es debido a su naturaleza constante, que garantiza que su valor permanecerá inalterado a lo largo de la ejecución del programa.

Definición de un encadenador de operaciones

Una de las funcionalidades claves que tiene Elixir en que nos permite realizar una serie de operaciones en conjunto y retornar un único valor, esto con el objetivo de disminuir la definición de variables, manejar errores inesperados o realizar alguna acción en caso de que una respuesta no sea la correcta. Definimos un encadenador de operaciones de la siguiente manera:

defmodule Operaciones do
def sumar(a, b) do
{:ok, a + b}
end

def restar(a, b) do
{:ok, a - b}
end

def multiplicar(a, b) do
{:ok, a * b}
end

def dividir(a, b) do
{:ok, a / b}
end
end

defmodule WithDoElse do
import Operaciones
def realizar_operaciones_conjuntas() do
with {:ok, res_suma} <- sumar(5, 3),
{:ok, res_resta} <- restar(res_suma, 7),
{:ok, res_multi} <- multiplicar(res_resta, 2),
{:ok, res_div} <- dividir(res_multi, 4) do
{:ok, res_div}
else
error->
{:error, error}
end
end
end

defmodule Init do
import WithDoElse
resultado = realizar_operaciones_conjuntas()
IO.inspect(resultado)
end

Con la palabra reservada with-do-else se puede definir un encadenador de operaciones donde:

  1. La tupla que está definida en la parte izquierda representa la respuesta esperada de cada operación.
  2. La parte derecha está definida por operaciones.
  3. La comparación se realiza mediante el uso de coincidencia de patrones, si alguna de las respuesta no coinciden, el flujo se irá por el bloque else y realiza alguna acción, de lo contrario se irá por el bloque do y será exitoso.

La palabra reservada import nos permite extraer funcionalidades de otros módulos, hay otras palabras reservadas que nos permite realizar el mismo objetivo, pero este tema se verá en un próximo artículo.

Definición del operador pipe

Como Elixir es un lenguaje funcional, permite crear flujos de operaciones sin necesidad de almacenar el resultado de cada una en variables. Esto se logra mediante el operador pipe, que se puede utilizar de la siguiente manera:

defmodule OperadorIterable do
resultado = [1, 2, 3, 4, 5]
|> Enum.map(fn x -> x * 2 end)
|> Enum.filter(fn x -> rem(x, 2) == 0 end)
|> Enum.sum()
IO.inspect(resultado)
end

El operador pipe se define como |> y nos permite realizar un flujo con operaciones que cambian el contenido de la lista y cuyo valor final es la suma total de los datos modificados.

Definición del módulo Enum

Elixir cuenta con un módulo llamado Enum, que nos permite realizar diferentes operaciones tales como filtros, sumas, conversiones, entre otras, a los tipos de estructuras que se pueden recorrer como listas, tuplas o rangos. Podemos tomar el ejemplo anterior que realiza las siguientes operaciones:

  1. La función Enum.map/2 me permite convertir el valor contenido en cada ítem de la lista en otro valor, en este caso se está multiplicando cada ítem de la lista por 2 y su resultado es modificador por la multiplicación.
  2. La función Enum.filter/2 me permite filtrar los valores de una lista dependiendo del condicional que se defina dentro de esta función, en este caso se definido un condicional que me permite obtener los valores que son números primos.
  3. Finalmente la función Enum.sum/2 me permite obtener la suma total de los datos contenidos dentro de una lista, en este caso se realiza la suma de todos los números primos definidos dentro de la lista.

¿Cómo ejecutar Elixir?

Para ejecutar una aplicación construida en Elixir o un archivo con la extensión .ex (extensión que identifica archivos de Elixir), primero se tiene que descargar el instalador que encontramos en la página oficial de Elixir. Luego, descargamos el instalador de Erlang aquí. Una vez descargados estos dos instaladores, se agrega la dirección de la carpeta bin de Elixir en variables de entorno para que se identifiquen los comandos relacionados a este. Finalmente, si desea ejecutar Elixir desde una consola de comando, solo se ingresa el comando iex para iniciar un proceso como se muestra a continuación:

Si tiene un archivo con la extensión .ex y quiere ejecutarlo, se muestra un ejemplo a continuación:

Este archivo tiene el nombre func_priv.ex y se ejecuta con el siguiente comando:

Conclusiones

  1. Elixir es una alternativa para construir sistemas distribuidos y de alta tolerancia a fallos.
  2. Su ruta de aprendizaje es más sencilla a comparación de Java o Erlang.
  3. El uso de coincidencias de patrones es una técnica útil para extraer ciertos patrones de una secuencia de datos, esta técnica potencia la capacidad de este lenguaje de programación.
  4. Su actualización y arreglos de bugs es frecuente gracias a su comunidad en crecimiento.
  5. Elixir no cuenta con la facilidad de construir objetos como usualmente se hacen en otro lenguajes como Java, pero permite construir estructuras personalizadas según las necesidades del desarrollador.
  6. Cuenta con diferentes funcionalidades para ejecutar varias operaciones de manera secuencial y con flujos.

--

--