09/05/2023 | Consejos tecnológicos,General,Tecnologías

Por qué Elixir importa y cómo puede mejorar tu código en el Backend

Introducción

Para la programación funcional no existe el término medio, o la odias con toda tu alma por aquella asignatura de la universidad en la que lo pasaste mal por Haskell, o la amas porque sientes el gran potencial que existe tras ella.

Si eres de los primeros, déjame decirte que el mundo funcional no solo es Haskell, que existen alternativas modernas que permiten entender muy fácilmente el código, y que fuera del mundo académico, no vas a tener que romperte la cabeza sin ayuda de Internet para hacer una función recursiva que haga algo sumamente complejo.

En este post me gustaría hablaros de Elixir, un lenguaje funcional basado en Erlang y muy parecido a Ruby en cuanto a sintaxis, usado por grandes empresas como Cabify, Discord, WhatsApp o Spotify para ofrecer sus servicios.

La filosofía funcional

Como su nombre lo indica, la programación funcional pone su foco en las funciones. En este paradigma estas se parecen más a la definición matemática que vemos en el instituto. De forma simplificada, la idea es que para un valor de entrada, siempre se obtenga el mismo valor de salida.

Esto es lo que se conoce como una función pura, la idea dentro del mundo funcional es que todas las funciones sean puras para lograr una mayor seguridad en el código. De hecho, todos los mecanismos y decisiones tomadas en los lenguajes funcionales, vienen propiciados para conseguir este fin.

En los lenguajes orientados a objetos, el concepto de clase es el que marca el rumbo. Las clases surgen de la idea de combinar datos con operaciones sobre estos datos, es decir, propiedades y métodos. Esta combinación en una misma unidad (clase), genera incondicionalmente un estado dentro de ella.

Lo peligroso aquí (desde los estándares funcionales), es que ese estado puede provocar que el resultado devuelto por los métodos de clase varíe. En el momento en que un método hace uso de una propiedad de clase que puede ser accedida y modificada por cualquiera, se crea la posibilidad de que el resultado que devuelve ese método no sea el esperado.

Estaríamos hablando entonces de una función impura, cosa que la programación funcional trata de combatir a toda costa. Esto de que un factor externo a la función pueda hacer que obtengamos un resultado inesperado es lo que se conoce como side effect.

En el mundo funcional los side effects están reducidos al máximo, ya que no combinamos los datos con las operaciones que realizamos sobre ellos. En su lugar, trabajamos realizando transformaciones sobre los datos. Estos entran a una función como valores de entradas, se opera con ellos, y son devueltos por la función como valores de salida.

Me gustaría remarcar la palabra transformar del párrafo anterior, ya que podría haber empleado otra como “modificar”. He seleccionado la palabra transformar y no otra debido a que en la programación funcional, no modificamos los datos, sino que los transformamos en otros datos, dejando los datos originales tal y como estaban. No me quiero enrollar mucho más con esto, solo diré que esto se hace así debido a que otro de los pilares fundamentales del paradigma funcional es la inmutabilidad.

Tan solo estas dos características favorecen y facilitan mucho el testing, debido a que para una entrada podemos esperar siempre una misma salida y debido a que al estar obligados a trabajar con datos inmutables es más sencillo depurar errores.

Me encantaría seguir explicándote los pilares y la filosofía de la programación funcional, pero si estás aquí probablemente sea por Elixir, así que me dejo ya de historias y vamos al grano.

¿Por qué Elixir?

Aun siendo un gran fan de la programación funcional, he de admitir que los lenguajes funcionales no han puesto mucho de su parte por atraer a nuevos usuarios y levantar el interés de la industria para ser elegidos como los lenguajes usados por defecto en las empresas.

Haskell se siente críptico, intimidante y complejo. Scala trató de ser moderno, pero no terminó de encajar y el estar construido sobre la JVM le aportó más problemas que ventajas. Y si hablamos de lenguajes como LISP, ML o Erlang… Os invito a ver sus respectivas landing page, creo que sobran los comentarios.

Sin embargo, por primera vez parece que existe un lenguaje que sí se preocupa por ofrecer una sintaxis amigable y cómoda para el desarrollador. Que trata de coger todas esas ideas y conceptos útiles del paradigma funcional para tratar de hacer la vida más fácil a los desarrolladores. Parece que por fin existe un lenguaje que pone el foco en los sistemas distribuidos, ofreciendo un modelo de concurrencia simple pero con unos benchmarks brutales, que permite escalar de forma muy sencilla los servicios web de una empresa.

Parece que, por fin, un lenguaje funcional, como Elixir, quiere dar un golpe sobre la mesa y diferenciarse del resto para dejar de ser un lenguaje académico y empezar a aportar valor al mundo real.

Con este post no pretendo que aprendas Elixir, solo quiero que veas el potencial que puede ofrecerte. Quiero que veas las herramientas y mecanismos que te ofrece la programación funcional. Quiero que veas que con este lenguaje la programación funcional “no da miedo” como con Haskell. Con este post quiero enseñarte cómo puedes crear un código más limpio, sostenible y escalable gracias a este prometedor y joven lenguaje.

El título de este post menciona que Elixir puede ayudarte a mejorar tu código en el Backend. Esto no deja de ser una opinión, pero creo firmemente en esto por dos motivos. El primero es la sintaxis y las herramientas que ofrece Elixir, y el segundo es su Framework web Phoenix. Si hablase de los dos este post sería gigantesco, así que en este solo me centraré en la sintaxis y las herramientas.

¿Qué tienen de especial las funciones en Elixir?

Si la orientación a objetos va de objetos… La programación funcional irá de funciones ¿No? Esta es la sintaxis para definir una función en Elixir.

La palabra reservada def es como la palabra reservada function en otros lenguajes, a continuación va el nombre de la función, y las palabras do y end definen el scope de la función. Tan solo acabamos de empezar y ya podemos ver algo interesante, no hay un return al final de la función para devolver el resultado.

Esto es algo intencional, todas las funciones devuelven como resultado la última expresión que se evalúe dentro de ellas. Esto es una decisión de diseño de Elixir, y viene dada por dos motivos.

  1. Que la definición de función se cumpla (todo valor de entrada tiene un valor de salida).
  2. Que solo exista un punto de retorno en la función, para asegurar que se construyen funciones pequeñas que tienen una única responsabilidad.
  3. Además de estos dos motivos, el azúcar sintáctico de obviar el return nunca viene mal para simplificar todavía más las cosas.

Hablando de simplificar las cosas, Elixir es un lenguaje que toma este punto como uno de sus principales fuertes dentro de su filosofía. Si la idea es hacer funciones pequeñas que tengan un único objetivo ¿Por qué no ofrecer al desarrollador facilidades para hacer funciones pequeñas?

Si el cuerpo de tu función solo ocupa una línea, Elixir te permite simplificarlo de esta manera.

Quizás no te convenza el tema de que una función solo pueda tener un único return. A mí tampoco, me gusta colocar sentencias if al principio de mi función para evitar que el flujo de ejecución continúe en los casos base o cuando los parámetros de entrada no son los esperados.

No obstante, Elixir tiene una solución para esto. Si quisiéramos implementar una función para dividir dos números, no sería descabellado querer poner un sentencia if al principio de la función para comprobar que el denominador es mayor que 0.

¿Qué es esto? Te preguntarás. Antes de nada te voy a pedir que ignores que es eso de :infinite, es un átomo, pero después hablaremos de ellos. En el mundo funcional existe una cosa llamada pattern matching o encaje de patrones, y es una de las cosas más potentes que ofrecen este tipo de lenguajes. A bajo nivel tiene que ver con cómo funciona el sistema de asignación de variables, pero eso es un tema demasiado denso, el cual te invito a investigar por tu cuenta (Spoiler: en los lenguajes funcionales no hay asignaciones, si no “igualaciones”).

Cuando nosotros llamemos a la función con divide(8,5), por ejemplo, lo que trata de hacer Elixir es ver donde “encajan” mejor esos parámetros. Elixir tratará de ejecutar de arriba a abajo la primera función, que tiene como cabecera divide(x,0), ahora Elixir se hará una serie de preguntas, como ¿x == 8? ¿0 == 5?.

La primera pregunta sera respondida con un “si”, ya que x puede ser igual a 8, sin embargo la segunda será respondida con un “no”, ya que en ninguna caso 0 puede ser igual a 5.

Por lo que nuevamente, Elixir tratará de “encajar” esa llamada con la siguiente cabecera de la función, en este caso, x si puede tomar el valor 8, e y si puede tomar el valor 5.

El pattern matching está por todos lados en Elixir, de hecho no existe la asignación, todo dato trata de encajar dentro de una variable. Si sabes utilizar el destructuring de Javascipt, esto es prácticamente un destructuring con esteroides, más adelante hablaremos más sobre el pattern matching, pero antes me gustaría contarte un par de cosas más sobre las funciones.

Imaginemos ahora que solo queremos permitir la división de números positivos. En otros lenguajes, lanzaríamos una excepción manualmente dentro de la función si alguno de los parámetros fuera menor que 0. En Elixir tenemos otro mecanismo para sobrellevar eso, las guardas.

Creo que ya sabéis por dónde van los tiros. Las guardas son expresiones adicionales que nos permiten determinar si una función debe evaluarse dependiendo de las condiciones indicadas.

Si has llegado hasta aquí, quizá te preocupe el no ver ninguna definición de tipos de datos en las cabeceras de las funciones. Elixir es un lenguaje con un tipado dinámico y fuerte, como Python o Ruby. Y al igual que Python, Elixir permite tipar las cabeceras de las funciones mediante anotaciones.

También se pueden poner alias a los tipos para que sean más descriptivos, pero por no alargar demasiado el post, te invito a que le eches un ojo a la documentación oficial sobre esta parte.

Por ir cerrando con las cosas chulas que se pueden hacer con las funciones, en el siguiente ejemplo te muestro rápidamente como se escriben las funciones lambda.

Y el detalle de que si una función devuelve un booleano, se puede añadir un ? al final de la función para hacerla más expresiva.

Para dejar ya en paz a las funciones, solo decir que estas se organizan por módulos. Para que te hagas a la idea, un módulo es como una clase, pero sin atributos, su utilidad es agrupar funciones que realicen tareas similares.

Átomos

¿Recuerdas el valor :infinite que devolvía la función divide(x,0)? Antes lo mencioné, pero se trata de un átomo. Los átomos son construcciones que sirven para representar valores constantes, siendo su nombre el propio valor del átomo.

Los booleanos en Elixir son considerados como átomos :true y :false, aunque por comodidad el lenguaje permite escribirlos como true y false a secas. Esto es así porque realmente cumplen con esta definición, son valores constantes cuyo valor es indicado por el nombre.

Definir un átomo es tan sencillo como escribir dos puntos y el nombre queramos darle. Son útiles y elegantes cuando tenemos que comparar strings, por ejemplo.

Estructuras de datos y Pattern Matching

Hablemos ahora sobre estructuras de datos. Las 4 principales estructuras de datos que existen en Elixir son las listas, las tuplas, los mapas y los structs.

Este tema da para mucho, y donde realmente se ve el potencial de todo esto es juntando estas estructuras con el pattern matching. Pero vayamos por partes.

Las tuplas son estructuras de tamaño fijo que nos permiten almacenar valores de cualquier tipo dentro de ellas. Tienen la particularidad de que los valores de las tuplas están almacenados consecutivamente en memoria, por lo que de alguna forma sirven para indicar que estos están relacionados y las operaciones sobre ellas son muy rápidas.

Los mapas de Elixir son como los mapas de Java. Tienen la particularidad de que si todas las claves del mapa son átomos (lo cual es recomendable), se puede utilizar una sintaxis especial para simplificarlos.

Los structs de Elixir son como los structs de C. No son más que mapas que deben de contar con una serie de claves predefinidas. Se pueden crear con valores por defecto y no permiten añadir nuevas claves en tiempo de ejecución.

Me estoy dejando las listas para el final, adrede, ya que son las estructuras de datos con más potencial dentro de Elixir (y de cualquier lenguaje funcional en realidad).

Una lista consiste en lo que todos ya sabemos, almacenar valores entre corchetes separados por comas.

La particularidad que tienen es como están implementadas por debajo. Aunque nosotros la veamos como 4 valores, en realidad en esa y en cualquier otra lista solo hay 2 valores, la cabeza y la cola (salvo en la lista vacía que solo hay 1 valor, la propia lista vacía).

No me quiero poner a hablar de recursividad para no asustar a nadie, aunque esta definición sea puramente recursiva, así que mejor vamos a ver unos ejemplos para entenderlo mejor.

La cabeza de la lista, es decir, su primer elemento, siempre será el primer elemento que nos encontremos en esta, y la cola, su segundo elemento, será una lista con el resto de valores.

¿Por qué esto es tan potente? Primero porque nos permite crear nuevas listas de forma totalmente inmutable utilizando composición.

Y segundo y más importante, porque nos permite realizar extracciones muy útiles mediante pattern matching.

Como he mencionado antes, esto del pattern matching es un destructuring con esteroides. Pero no es solo un destructuring, recuerda que también podemos hacer esto en las cabeceras de las funciones.

Y también con el operador case (que es un switch con pattern matching).

Como puedes ver, las guardas también se pueden aplicar a los casos del operador case. Y respecto a la barra baja, se utiliza para indicarle al compilador que esa variable no es de ninguna importancia para nuestro “match”.

Todo esto está muy chulo, pero de alguna forma debemos poder operar con listas. Elixir tiene las típicas funciones que todos los lenguajes de programación tienen para manejar listas (add, remove, concat…), no me quiero centrar en ellas, pero recuerda que por ser los datos inmutables siempre te devolverán una nueva lista.

Solo te mostraré dos operadores que me parecen increíbles para concatenar y eliminar elementos de listas.

Pero si hay funciones que han calado fuerte en los lenguajes modernos, son map, filter y reduce. Son las que ya conoces y funcionan igual que en el resto de lenguajes, pero son la excusa perfecta para enseñarte el operador pipe, que sin lugar a dudas es una de las cosas que más me gusta de Elixir.

Aquí todo es inmutable, así que no podemos concatenar estas funciones como hacemos en Javascript. Lo que sí podemos hacer, es utilizar el operador pipe, que lo que hará será tomar como entrada lo que esté a la izquierda y pasárselo como primer parámetro a la función de la derecha. No solo funciona con listas, también funciona con mapas, tuplas, etc.

Otra herramienta que nos da Elixir para trabajar con listas son las list comprehensions. Las list comprehensions son estructuras que juntan las funciones map y filter en una sola mediante azúcar sintáctico debido a que es usual usarlas juntas.

Funcionan de la siguiente manera, la lista se recorre de izquierda a derecha, y cada elemento trata de hacer pattern matching con lo que hay a la izquierda de la flecha (en este caso la variable n). Si hace match, esta variable ejecutará la función que se define en la sentencia do: n*n , y si no, se pasará al siguiente valor.

En este caso este mecanismo funciona como un match, pero las list comprehensions también permiten decidir qué valores nos sirven y cuáles no.

Para ello, nos permite definir filtros a la derecha de la lista (llegados a este punto, quizás sea mejor llamarlo enumerables) y la izquierda del do.

Conclusiones y mi opinión

Me he dejado muchas cosas interesantes por el camino, como el gestor de paquetes mix, el modelo de concurrencia tan potente que tiene Elixir, el framework de testing ExUnit, y muchas otras construcciones del lenguaje que pueden resolver situaciones de una forma más legible que en otros lenguajes.

He de admitir que este post es bastante subjetivo, me encanta Elixir por su sintaxis y por lo limpio que queda tu código cuando programas con él, pero como digo, esto es solo una opinión personal que no tienes por qué compartir.

Sin embargo, si llegados a este punto estás tan enamorado de la sintaxis de Elixir como yo, me gustaría plantearte unas preguntas para que reflexiones. Cuando construyes tus aplicaciones (supongamos que web porque es a lo que me dedico) ¿Utilizas las clases de forma consciente o las utilizas porque es el mecanismo que te ofrece tu lenguaje para agruparlas?

Quiero decir, el concepto de clase surgió para ofrecer una herramienta que permitiera juntar datos y operaciones para que estos trabajasen en conjunto, es decir, atributos y métodos. Sin embargo, construcciones tan habituales como controllers, resolvers o services, no necesitan de atributos de clase para funcionar, de hecho lo ideal es que los métodos de estos no dependan de los valores de los atributos de su clase para no generar condiciones de carrera al interactuar la aplicación con muchos usuarios.

De hecho, está muy extendida la idea de tratar de escribir funciones puras en la medida de lo posible, aun trabajando con lenguajes orientados a objetos, lo cual me parece genial, ya que combinar ambos paradigmas trae muchos beneficios y nos aporta muchas facilidades a los desarrolladores.

Pero mi punto es, si solo utilizas las clases para encapsular o “agrupar” tus funciones dentro de una entidad que las relacione contextualmente… ¿Por qué no usar módulos directamente? Si te ha gustado lo que has visto de Elixir ¿Por qué no darle una oportunidad? Si utilizas las clases de esta manera, te será exactamente igual poner class o module.

Y para terminar, si crees que lo que te he enseñado te facilitaría mucho la vida al picar código, y que este sería mucho más limpio y legible… Yo que tú estaría atento a las siguientes publicaciones del blog, porque su framework web Phoenix (del que tengo planes de escribir), mejora un montón la developer experience, y no lo digo yo, lo dicen las encuestas de satisfacción de Stackoverflow, que califican a Phoenix como el framework más amado por los desarrolladores.

¿Qué me dices? ¿Te pasas al lado oscuro de la programación funcional?

Compartir en:

Relacionados