Logo

dev-resources.site

for different kinds of informations.

Introducci贸n a Elixir

Published at
5/22/2024
Categories
elixir
chile
introduccion
flisol
Author
Camilo
Categories
4 categories in total
elixir
open
chile
open
introduccion
open
flisol
open
Introducci贸n a Elixir

驴Se han preguntado que 茅s lo que hace genial a un lenguaje de programaci贸n?. Una de las razones por la que Elixir es genial es su consistencia. Jos茅 Valim tom贸 ideas de otros lenguajes de programaci贸n como Ruby y Erlang, ideas que han sido desarrolladas por m谩s de 30 a帽os y las hizo mucho m谩s consistentes. 30 a帽os de ideas pensadas para diferentes prop贸sitos por diferentes personas condensadas en una serie de bibliotecas mucho m谩s r谩pidas de usar y entender.

Elixir se ejecuta en la m谩quina virtual BEAM, una VM realmente antigua, ya que fue creada por Ericsson en 1986 para el lenguaje Erlang (M谩s antigua que Java). Estas tecnolog铆as fueron las principales protagonistas de las aplicaciones en un entorno de telecomunicaciones que manejan innumerables solicitudes por segundo de una manera confiable y eficiente.

Unas d茅cadas m谩s tarde, esta VM fue considerada por Jos茅 Valim, quien cre贸 el lenguaje Elixir a mediados de 2011 para utilizar todo el poder de BEAM en un lenguaje moderno y enfocado en la experiencia de desarrollo. Este lenguaje tambi茅n fue responsable de la popularizaci贸n de la m谩quina virtual de Erlang.

Un peque帽o vistazo de Elixir

booleano = true || false
numero = 123_456
string = "Un binario" <> "Concatenada" <> "Interpolaci贸n #{numero}"
char = ?A
atomo = :un_atomo
funcion = fn _ -> "Hola" end
mapa = %{valor: "hola"}
tupla = {:ok, 1234}
lista = [1, "dos", :tres]
keyword_list = [valor: "hola"]
IO.inspect("Imprimir en Terminal")
# Comentario
_ # variable placeholder
_var # variable omitida
defmodule MiModulo do

  @atributo "Un atributo del m贸dulo"

  # Retorno el resultado de la 煤ltima expresion
  def mi_funcion_publica(parametro \\ true) do
    "Funci贸n P煤blica con #{parametro}"
  end

  # Pattern matching selecciona cual ejecutar seg煤n la firma de la funci贸n
  def mi_funcion_publica() do
    "Funci贸n P煤blica Sin par谩metros"
  end

  defp mi_funcion_privada() do
    "Funci贸n Privada"
  end
end

MiModulo.mi_funcion_publica()

驴Qu茅 significa Consistencia?

La palabra consistencia se puede definir seg煤n la RAE como:

  1. f. Duraci贸n, estabilidad, solidez.
  2. f. Trabaz贸n, coherencia entre las part铆culas de una masa o los elementos de un conjunto.

Adicionalmente definiremos coherencia:

  1. f. Conexi贸n, relaci贸n o uni贸n de unas cosas con otras.

As铆 como cohesi贸n:

  1. f. F铆s. Fuerza de atracci贸n que mantiene unidas las mol茅culas de un cuerpo.

En el desarrollo de software los componentes que utilizamos para elaborar el sistema idealmente deben ser consistentes, coherentes y cohesivos, es decir, que tengan una armon铆a y sean una base s贸lida para facilitar la mantenci贸n e implementaci贸n de los requisitos del sistema.

Ejemplos de Inconsistencias

En otras tecnolog铆as podemos encontrar inconsistencias.

Inconsistencias en la API

PHP es un gran ejemplo de inconsistencias en su biblioteca oficial de funciones.
Por ejemplo notemos como las diferencias entre las funciones str_replace y strpos. Ambas funciones para trabajar con strings.

  1. El nombre es inconsistente, la primera utiliza gui贸n bajo para separar str_ y la otra no.
  2. El nombre de los par谩metros es incosistente. $search y $haystack son equivalentes.
 str_replace(
    mixed $search,
    mixed $replace,
    mixed $subject,
    int &$count = ?
): mixed
strpos(string $haystack, string $needle, int $offset = 0): int|false

Adem谩s la funci贸n strpos puede retornar un valor que sea un entero, pero podr铆a ser considerado falso (0).
Por lo que deber铆a usar el operador === para verificar el real valor de retorno.

En Elixir se podr铆a utilizar un retorno de tuplas {:ok, pos} y {:error, motivo}
y utilizar pattern matching, simplificando las validaciones.

Inconsistencias en los Datos

En otros lenguajes de programaci贸n una fuente de inconsistencias es la mutabilidad de sus estructuras de datos. Elixir al ser un lenguaje funcional trabaja con estructuras inmutables que permiten una consistencia en los datos.

El siguiente ejemplo escrito en Python, nos muestra los problemas de inconsistencias en lenguajes de programaci贸n con estructuras de datos mutables.

mapa = {
  'lista': [1,2,3],
  'numero': 10
}

lista = mapa['lista']
numero = mapa['numero']

lista += [4]
numero += 10

print(lista)
# [1, 2, 3, 4]

print(numero)
# 20

print(mapa)
# {'lista': [1, 2, 3, 4], 'numero': 10}

Hemos modificado la variable lista y numero, pero sin embargo la variable mapa fue afectada. Esto es debido a que lista fue accedido por referencia y numero accedido por valor.

En Elixir eso no pasa, gracias a su inmutabilidad. Podemos trabajar con las variables lista y numero sin miedo a alterar el valor de la variable mapa.

mapa = %{
  lista: [1, 2, 3],
  numero: 10
}

lista = mapa.lista
numero = mapa.numero

lista = lista ++ [4]
numero = numero + 10

IO.inspect(lista)
# [1, 2, 3, 4]

IO.inspect(numero)
# 20

IO.inspect(mapa)
# %{lista: [1, 2, 3], numero: 10}

Las diferencias con lenguajes OOP

Acoplamiento en la OOP

Los objetos son el acomplamiento de tres componentes: Comportamiento, Estado y Mutabilidad (cambios en el tiempo). Cada vez que se crea un objeto se obtiene una entidad que acopla esos tres componentes. Este acoplamiento causa muchas veces problemas, 驴C贸mo puedo solamente utilizar uno de los componentes?.

Una de las mayores fuentes de problemas es la herencia entre objetos. Si por ejemplo tenemos un objeto que tiene acoplado estado y comportamientos, luego deseo a帽adir m谩s comportamientos, debo recurrir a la herencia. Los objetos por definici贸n existen para encapsular un estado, para modificar el estado debo interactuar con el objeto. Al utilizar herencia se tiene un mecanismo que permite adulterar el estado directamente, invalidando al objeto. Es as铆 como algunos lenguajes de programaci贸n incluyen operadores como protected, final, entre otros para controlar la visibilidad de lo que se supone no deber铆a tener acceso.

B谩sicamente existe un problema cuya soluci贸n crea otros problemas, sin incluir cosas como la herencia m煤ltiple. Todo esto debido al acomplamiento inherente de la orientaci贸n a objetos.

La Programaci贸n Orientada a Objetos (POO) no se trata de crear una taxonom铆a gigante con una compleja jerarqu铆a de objetos. El uso desmedido de la sintaxis del punto (objeto.metodo()) es una consecuencia de esa mala interpretaci贸n. La POO se trata de tener objetos que parpean como un pato, pero no necesariamente son un pato (quack like a duck, but is not a duck). Hay una gran confusi贸n acerca de que en la POO solo existen m茅todos y no funciones. Hasta Smalltalk (padre de la POO) hay funciones (escondidas bajo una sintaxis especial).

En el conocido libro del Design Patterns: Elements of Reusable Object-Oriented Software mencionan: "Preferir la composici贸n por sobre la herencia".

Los lenguajes funcionales como Haskell, Lisp y Elixir han estado resolviendo problemas complejos sin recurrir a la herencia. Mientras que los lenguajes orientados a objetos pueden utilizar la composici贸n gracias a las interfaces. Ya que las interfaces son un mecanismo de polimorfismo, es decir, que nos permiten trabajar con m煤ltiples formas a trav茅s de un contrato pre-establecido.

Los Componentes de Elixir

En Elixir existe comportamiento (M贸dulos), estado (Datos) y una visi贸n de mutabilidad (Procesos), pero no est谩n acoplados. Lo que nos permite elaborar software utilizando solamente el componente requerido, sin incurrir en los problemas ocasionados por el acoplamiento de la POO. Elixir dispone de tres dimensiones para realizar sistemas, a diferencia de la POO que solamente cuenta con una. Por lo que puede tener un polimorfismo distinto en cada componente (Behaviours, Protocols, Messages).

En el libro Concepts, Techniques, and Models of Computer Programming mencionan la regla de lo menos expresivo. "Cuando se programa un componente, el modelo computacional correcto para lograr un programa natural es el menos expresivo posible". Lo que se puede simplificar como "utilizar la abstracci贸n m谩s simple posible para resolver el problema".

Ejemplo de Acoplamiento

Vamos por un ejemplo simple comparando la forma de contar caracteres en un string.

El siguiente es un ejemplo en Ruby.

"Hola".length
# 4

En Elixir la misma operaci贸n ser铆a:

String.length("Hola")
# 4

En Ruby el string "Hola" es un objeto que adem谩s de tener
la estructura de datos, tiene una serie de comportamientos asociados.
Esta altamente acoplado.

En cambio en Elixir el string "Hola" solamente es un dato
que no tiene comportamientos. Para poder realizar operaciones
debemos utilizar las funciones del m贸dulo String. Existe
un desacople entre datos y comportamientos.

驴Por qu茅 es importante este desacople?

Veamos un ejemplo. Si asignamos una variable a "Hola",
con el tiempo podemos cambiar el contenido de la variable
y ya no podremos utilizar el m茅todo asociado a los tipos string.

variable = "Hola"
variable.length
# 4

variable = 1
variable.length
# (irb):4:in `<main>': undefined method `length' for an instance of Integer (NoMethodError)

Esto puede ser un problema, sobre todo si existe una jerarqu铆a de herencias y combinado con la mutabilidad del lenguaje, es una receta para el caos y soluciones poco elegantes si no se maneja adecuadamente los riesgos.

En Elixir al estar totalmente desacoplados dato, comportamiento y cambios de estado, se puede asegurar productos de software libres de problemas asociados a jerarqu铆as de herencias y mutabilidad.

Pattern Matching

Esta t茅cnica propia de los lenguajes funcionales, se utiliza para buscar patrones y decidir qu茅 hacer en cada momento. Debemos pensar en el operador = no como un signo igual t铆pico de otros lenguajes, sino como el que nos encontramos en una funci贸n matem谩tica del tipo x = a + 1. Es decir que estamos diciendo que x y a + 1 tienen el mismo valor.

# a = 1
# b = "elixir"
# c = "ninjas"
{a, b, c} = {1, "elixir", "ninjas"}

El Operador Pipe

Los sistemas operativos inspirados por Unix vienen usando Pipelines desde sus inicios.
En este ejemplo, listamos el contenido del directorio, filtramos solo las l铆neas que contienen "archivo.txt", y redirigimos la salida a un archivo llamado resultado.txt.

ls -l | grep "archivo.txt" > resultado.txt

https://www.swhosting.com/es/comunidad/manual/uso-de-pipes-en-sistemas-unix

En el caso de Elixir, el operador pipe (tuber铆a) |> es una hermosa herramienta que nos permite expresar una cadena de funciones como una secuencia de acciones.

A煤n si no has usado Elixir podr铆as entender la l贸gica del siguiente c贸digo.

parametros_formulario
|> validar_formulario()
|> insertar_usuario()
|> crear_reporte()
|> mostrar_resultado()

Utilizar una serie de operadores pipe se conoce como un pipeline. Es simple de leer y comprender lo que ocurre. Pero por su simpleza tambi茅n tiene algunas limitaciones. Debido a que las funciones est谩n encadenadas, dependen del resultado anterior. Si alguna de las funciones falla quebrar铆a el flujo completo. No se puede hacer mucho frente a esto a menos que se maneje los casos de error en cada funci贸n.

https://blog.appsignal.com/2022/07/19/writing-predictable-elixir-code-with-reducers.html

En Elixir Todo es un Reductor (Reducer)

Primero partiremos explicando los conceptos de acumulador y reductor.

Acumulador

Un acumulador es una variable que durante la ejecuci贸n de un programa va referenciar as铆 misma y almacenar el resultado de realizar operaciones con los valores contenidos en otras variables.

acumulador = acumulador + variable

Reductor

Un reductor es una forma de procesar una tarea grande poco a poco. Utiliza un acumulador para facilitar las operaciones intermedias y entrega un 煤nico resultado final.

La estructura com煤n de un reductor es la siguiente.

reductor(elementos_enumerables, valor_inicial_acumulador, funcion_reductora)
  • elementos_enumerables: Una lista de elementos que pueden ser enumerados. Ejemplo [1, 2, 3].
  • valor_inicial_acumulador: El valor que tendr谩 nuestro acumulador en la primera ejecuci贸n de la funci贸n reductora. Ejemplo 0.
  • funcion_reductora: Es la funci贸n que recibe dos par谩metros. El elemento en la lista y el valor actual del acumulador. Ejemplo fn elemento, acc -> acc + elemento end

El reductor ejecutar谩 la funci贸n reductora por cada elemento y retornar谩 el valor final del acumulador cuando cada elemento haya sido procesado.

https://redrapids.medium.com/learning-elixir-its-all-reduce-204d05f52ee7

Ejemplo

Vamos a ver un ejemplo concreto de c贸mo funciona un reductor. Primero definiremos una funci贸n para sumar dos valores. Utilizaremos la sintaxis simplificada con el operador de captura (&).
Lo que nos permite expresar una funci贸n de forma m谩s corta.

La siguiente forma de expresar una funci贸n con dos par谩metros

sumar = fn elemento, acc -> elemento + acc end

Puede ser simplificada utilizando el operador de captura &.

sumar = &(&1 + &2)

Sumaremos la lista de elementos [1, 3, 4] para que podamos obtener la sumatoria que es 8.

# sumar = fn elemento, acc -> elemento + acc end
sumar = &(&1 + &2)
&:erlang.+/2

Si utilizamos el m贸dulo Enum y la funci贸n reduce obtendremos nuestro resultado

# reduce(enumerable, acumulador, funcion_reductora)
Enum.reduce([1, 3, 4], 0, sumar)
8

Es equivalente a llamar a la funcion sumar de forma anidada.

sumar.(4, sumar.(3, sumar.(0, 1)))
8

Tambi茅n puede ser expresada como un pipeline de la funci贸n sumar, cuyo valor incial es. 0.

0
|> sumar.(1)
|> sumar.(3)
|> sumar.(4)
8

Este pipeline se podr铆a expresar como llamar a la funci贸n suma utilizando el resultado de la funci贸n anterior. En este caso se podr铆a expresar como lo siguiente:

# 1
sumar.(0, 1)
# 4
sumar.(1, 3)
# 8
sumar.(4, 4)
8

CRC: Crear, Reducir y Convertir

Elixir utiliza m贸dulos y tipos de datos, lo que permite una forma de organizar nuestro c贸digo en lo que se denomina CRC (Constructores, Reductores y Conversores). Por lo que tendremos funciones para (crear) un acumulador, funciones que realizar谩n operaciones (reductores) con este acumulador y finalmente funciones que transformar谩n el acumulador en un formato final (conversores). 脡sto es algo que ha existido por largo tiempo en diferentes lenguajes de programaci贸n como Haskell o Lisp. Lo m谩s importante que puedes hacer en un lenguaje es unir ideas utilizando composici贸n.

La idea es crear un pipeline que reciba como primer par谩metro un acumulador y realizar una serie de operaciones reduce hasta llegar al conversor final.

  entrada
  |> constructor() # crea el acumulador inicial
  |> reductor()
  |> reductor()
  |> reductor()
  |> conversor()  
  # salida de la funci贸n listo para ser utilizado por otro pipeline
  # o ser mostrado al usuario final

Por esta raz贸n podemos considerar que todo en Elixir es un conjunto de acumuladores y reductores. Mucha de las funciones del core de Elixir pueden ser implementadas usando nada m谩s que un acumulador y Enum.reduce.

驴Por qu茅 es importante?

Gracias a 茅sta forma de organizaci贸n podemos ver nuestro c贸digo de forma coherente
y unificada. Al tener una estructura de datos en com煤n con varias funciones
podemos realizar operaciones y expresarnos con una facilidad de lectura mayor.
Nuestros sistemas ser谩n m谩s f谩ciles de entender y mantener en el futuro.

La consistencia es un factor importante de calidad en nuestros sistemas y utilizar CRC es una gran herramienta para lograr eso.


# Creamos un nuevo mapa con nuestro acumulador
constructor = &%{acc: &1}

# Retornamos el valor del acumulador actualizado
suma = &%{acc: &1.acc + &2}

# Mostramos solamente el valor que deseamos
conversor = & &1.acc

0
|> constructor.()
|> suma.(1)
|> suma.(3)
|> suma.(4)
|> conversor.()
8

Ejemplo de CRC

Vamos a ejemplificar un poco utilizando un mapa del tesoro. En este mapa vamos a dar una serie de direcciones norte, sur, este y oeste. Se puede ver como tenemos una funci贸n de creaci贸n que retorna una tupla {x, y}, la cual ser谩 nuestra estructura de datos del acumulador. Luego tenemos una serie de reductores que modifican la tupla y devuelven una nueva tupla con los valores apropiados. Finalmente tenemos nuestro conversor que retorna un String con un mensaje final.

defmodule Tesoro do
  # Creador
  def nuevo(x, y), do: {x, y}
  def nuevo, do: nuevo(0, 0)

  # Reductores
  def norte({x, y}), do: {x, y - 1}
  def sur({x, y}), do: {x, y + 1}

  def este({x, y}), do: {x - 1, y}
  def oeste({x, y}), do: {x + 1, y}

  # Conversor
  def mostrar({x, y}), do: "El tesoro se encuentra en las coordenadas #{x},#{y}"
end
{:module, Tesoro, <<70, 79, 82, 49, 0, 0, 11, ...>>, {:mostrar, 1}}
import Tesoro

nuevo()
|> norte()
|> este()
|> este()
|> oeste()
|> oeste()
|> sur()
|> sur()
|> sur()
|> este()
|> este()
|> este()
|> mostrar()
"El tesoro se encuentra en las coordenadas -3,2"

Como se puede apreciar logramos generar nuestro mapa del tesoro utilizando un pipeline de funciones que aceptan una estructura de datos en com煤n como primer par谩metro (acumulador). Se realizan las operaciones a esta estructura para finalmente crear una salida con un formato espec铆fico.

Hemos logrado algo genial. Tomamos una idea compleja de distintas funciones y las unificamos como si fueran eslabones de una misma cadena. De esta forma puedes ver como las operaciones forman una cascada enviando valores. Hemos creado una composici贸n utilizando reductores.

Las ideas de CRC (Crear, Reducir y Convertir) pueden ser encontradas a lo largo de todo el ecosistema de Elixir, por ejemplo con OTP y el estado de un GenServer o Phoenix para tomar un dato y convertir ese dato a HTML, SVG, JSON o similares.

El ver las operaciones como una cadena simplifica c贸mo construir, leer y probar todo el software en el ecosistema de Elixir, gracias a una excelente consistencia y decisiones de dise帽o.

驴Por qu茅 usar Elixir frente a las alternativas?

Algunas razones:

  • Un lenguaje funcional moderno y preciosamente consistente. Permitiendo un nivel m谩s desacoplado y consistente que con otras tecnolog铆as.
  • Se ejecuta en la BEAM con m谩s de 30 a帽os de herramientas para sistemas robustos, escalables y concurrentes. Logrando una mayor confiabilidad y resiliencia a fallos que con otras soluciones.
  • Gran ecosistema de herramientas para IOT, Rob贸tica, IA, Web, Mobile, entre otros.
  • Un mercado atractivo para profesionales con ofertas laborales novedosas y frescas, sin tanta competencia como en otras tecnolog铆as. Mercado laboral en auge en USA, Alemania, Jap贸n, entre otros.
  • Multitud de capacitaciones y certificaciones disponibles (Grox.io, Erlang Solutions).
  • Utilizado por empresas de alto calibre como Facebook, Whatsapp, Discord, Pepsico, Walmart, entre otros.

驴Qu茅 nos depara la industria en el futuro?

La industria inform谩tica est谩 llena de cambios. Podemos mencionar la tecnolog铆a de los 80's con las primeras computadoras personales como el Commondore o Atari. Luego en los 90's vimos los inicios de internet con HTML, JS y CSS. En los 2000's vimos el auge de las redes sociales como Facebook y plataformas como Youtube. En los 2010's se masificaron las aplicaciones y tel茅fonos m贸viles "inteligentes". 驴Qu茅 se viene en la d茅cada del 2020's?. Lo m谩s probable es la masificaci贸n de la IA y sistemas concurrentes. Casos donde Elixir y Erlang son id贸neos, superando a otras alternativas como Python y Ruby. Los profesionales que aprendan lenguajes de la BEAM estar谩n preparados para las pr贸ximas d茅cadas de la industria, debido a que cada d铆a la concurrencia y robustez de los sistemas ser谩 mucho m谩s necesaria.

Los lenguajes de programaci贸n que son "dif铆ciles de contratar" siempre ser谩n un problema. Desafortunadamente, este no es un problema que realmente puedas resolver, porque nadie conoce el futuro. Si por ejemplo elijes un lenguaje de programaci贸n popular hoy, luego a la gente deja de gustarle y no ser谩 f谩cil encontrar personal (VB6, Action Script Flash). 驴Qu茅 pasa con casos como Python, donde la migraci贸n de 2.x a 3.x fue tan tortuosa, que pr谩cticamente fue un cambio a un lenguaje distinto?. Las organizaciones que no se casan con un solo lenguaje o tecnolog铆a y adoptan estrat茅gicamente nuevas herramientas, estar谩n mejor capacitadas para las variaciones del mercado e industria.

Featured ones: