Unlimited Plugins, WordPress themes, videos & courses! Unlimited asset downloads! From $16.50/m
Advertisement
  1. Code
  2. Elixir

Polimorfismo con protocolos en Elixir

by
Length:LongLanguages:

Spanish (Español) translation by Manuel (you can also view the original English article)

El polimorfismo es un concepto importante en la programación, y los programadores novatos suelen aprenderlo durante los primeros meses de estudio. El polimorfismo significa básicamente que se puede aplicar una operación similar a entidades de distintos tipos. Por ejemplo, la función count/1 puede aplicarse tanto a un rango como a una lista:

¿Cómo es posible? En Elixir, el polimorfismo se consigue utilizando una interesante característica llamada protocolo, que actúa como un contrato. Para cada tipo de datos que desees soportar, este protocolo debe ser implementado.

Con todo, este enfoque no es revolucionario, ya que se encuentra en otros lenguajes (como Ruby, por ejemplo). Aun así, los protocolos son realmente convenientes, así que en este artículo hablaremos de cómo definirlos, implementarlos y trabajar con ellos mientras exploramos algunos ejemplos. ¡Empecemos!

Breve introducción a los protocolos

Así, como ya se ha mencionado anteriormente, un protocolo tiene un código genérico y se basa en el tipo de datos específico para implementar la lógica. Esto es razonable, porque diferentes tipos de datos pueden requerir diferentes implementaciones. Un tipo de datos puede entonces despachar en un protocolo sin preocuparse de sus aspectos internos.

Elixir tiene un montón de protocolos incorporados, incluyendo Enumerable, Collectable, Inspect, List.Chars, y String.Chars. Algunos de ellos se discutirán más adelante en este artículo. Puedes implementar cualquiera de estos protocolos en tu módulo personalizado y obtener un montón de funciones gratis. Por ejemplo, habiendo implementado Enumerable, obtendrás acceso a todas las funciones definidas en el módulo Enum, lo cual es bastante genial.

Si vienes del maravilloso mundo de Ruby, lleno de objetos, clases, hadas y dragones, habrás conocido un concepto muy parecido al de los mixins. Por ejemplo, si alguna vez necesitas que sus objetos sean comparables, simplemente mezcla un módulo con el nombre correspondiente en la clase. Entonces sólo hay que implementar un método de nave espacial <=> y todas las instancias de la clase obtendrán todos los métodos como > y < de forma gratuita. Este mecanismo es algo similar a los protocolos en Elixir. Incluso si nunca has conocido este concepto, créeme, no es tan complejo.

Bien, lo primero es lo primero: hay que definir el protocolo, así que vamos a ver cómo hacerlo en la siguiente sección.

Definir un protocolo

Definir un protocolo no implica ninguna magia negra, de hecho, es muy similar a la definición de módulos. Utiliza defprotocolo/2 para hacerlo:

Dentro de la definición del protocolo se colocan funciones, al igual que con los módulos. La única diferencia es que estas funciones no tienen cuerpo. Significa que el protocolo sólo define una interfaz, un plano que debe ser implementado por todos los tipos de datos que deseen despachar en este protocolo:

En este ejemplo, un programador necesita implementar la función my_func/1 para utilizar con éxito MyProtocol.

Si el protocolo no está implementado, se producirá un error. Volvamos al ejemplo con la función count/1 definida dentro del módulo Enum. Al ejecutar el siguiente código se producirá un error:

Significa que el Entero no implementa el protocolo Enumerable (qué sorpresa) y, por tanto, no podemos contar enteros. Pero el protocolo sí se puede implementar, y esto es fácil de conseguir.

Implementando un protocolo 

Los protocolos se implementan mediante la macro defimpl/3. Tu específicas qué protocolo implementar y para qué tipo:

Ahora puedes hacer que tus enteros sean contables implementando parcialmente el protocolo Enumerable:

Más adelante hablaremos del protocolo Enumerable con más detalle y también implementaremos su otra función.

En cuanto al tipo (pasado al for), puedes especificar cualquier tipo incorporado, tu propio alias o una lista de alias:

Además de eso, puedes decir Any:

Esto actuará como una implementación de reserva, y no se producirá un error si el protocolo no está implementado para algún tipo. Para que esto funcione, establece el atributo @fallback_to_any a true dentro de tu protocolo (de lo contrario, el error seguirá apareciendo):

Ahora puedes utilizar el protocolo para cualquier tipo de soporte:

Nota sobre las estructuras

La implementación de un protocolo puede estar anidada dentro de un módulo. Si este módulo define una estructura, ni siquiera necesitas especificar for cuando se llama a defimpl:

En este ejemplo, definimos una nueva estructura llamada Producto e implementamos nuestro protocolo de demostración. En su interior, simplemente se hace una coincidencia de patrones con el título y el precio y luego se emite una cadena.

Recuerda, sin embargo, que una implementación tiene que estar anidada dentro de un módulo, lo que significa que puedes extender fácilmente cualquier módulo sin acceder a su código fuente.

Ejemplo: Protocolo String.Chars

Bien, basta de teoría abstracta: veamos algunos ejemplos. Estoy seguro de que has empleado la función IO.puts/2 muy a menudo para dar salida a la información de depuración en la consola cuando juegas con Elixir. Seguramente, podemos dar salida a varios tipos incorporados fácilmente:

¿Pero qué ocurre si intentamos dar salida a nuestra estructura Product creada en el apartado anterior? Colocaré el código correspondiente dentro del módulo Main porque de lo contrario obtendrá un error diciendo que la estructura no está definida o no se accede a ella en el mismo ámbito:

Corriendo este código, obtendrás un error:

¡Aja!  Significa que la función puts se basa en el protocolo String.Chars incorporado. Mientras no esté implementado para nuestro Product, el error se está levantando.

String.Chars se encarga de convertir varias estructuras en binarios, y la única función que hay que implementar es to_string/1, como indica la documentación. ¿Por qué no la implementamos ahora?

Con este código colocado, el programa mostrará la siguiente cadena:

¡Lo que significa que todo está funcionando bien! 

Ejemplo: Protocolo de inspección 

Otra función muy común es IO.inspect/2 para obtener información sobre una construcción. También hay una función inspect/2 definida dentro del módulo Kernel , que realiza la inspección según el protocolo incorporado Inspect.

Nuestra estructura de Product puede ser inspeccionada de inmediato, y obtendrá una breve información sobre ella:

Devolverá %Product{price: 5, title: "Test"}. Pero, una vez más, podemos implementar fácilmente el protocolo Inspect que sólo requiere codificar la función inspect/2:

El segundo argumento que se pasa a esta función es la lista de opciones, pero no nos interesa. 

Ejemplo: Protocolo Enumerable

Veamos ahora un ejemplo un poco más complejo al hablar del protocolo Enumerable. Este protocolo es empleado por el módulo Enum, que nos presenta funciones tan convenientes como each/2 y count/1 (sin él, tendrías que quedarte con la simple recursión). 

Enumerable define tres funciones que tienes que desarrollar para implementar el protocolo:

  • count/1 devuelve el tamaño del enumerable.
  • member?/2 comprueba si el enumerable contiene un elemento.
  • reduce/3 aplica una función a cada elemento del enumerable.

Teniendo todas esas funciones en su lugar, tendrás acceso a todas las bondades proporcionadas por el módulo Enum, lo cual es un muy buen trato.

Como ejemplo, vamos a crear una nueva estructura llamada Zoo. Tendrá un título y una lista de animales:

Cada animal también estará representado por una estructura:

Ahora vamos a instanciar un nuevo zoo:

Así que tenemos un "Zoo de demostración" con tres animales: un tigre, un caballo y un ciervo. Lo que me gustaría hacer ahora es añadir soporte para la función count/1, que se utilizará así:

¡Implementemos esta funcionalidad ahora!

Implementación de la función de recuento

¿A qué nos referimos cuando decimos "contar mi zoo"? Suena un poco extraño, pero probablemente significa contar todos los animales que viven allí, por lo que la implementación de la función subyacente será bastante sencilla:

Todo lo que hacemos aquí es confiar en la función count/1 mientras le pasamos una lista de animales (porque esta función soporta listas fuera de la caja). Una cosa muy importante a mencionar es que la función count/1 debe devolver su resultado en forma de tupla {:ok, results} como dictan los docs. Si devuelve solo un número, se producirá un error ** (CaseClauseError) no case clause matching.

Eso es todo. Ahora puedes poner Enum.count(my_zoo) dentro de Main.run, y debería devolver 3 como resultado. ¡Buen trabajo!

¿Implementación de la función Member? Función

La siguiente función que define el protocolo es el member?/2. Debe devolver una tupla {:ok, booleano} como resultado que dice si un enumerable (pasado como primer argumento) contiene un elemento (el segundo argumento).

Quiero que esta nueva función diga si un determinado animal vive en el zoo o no. Por lo tanto, la implementación es bastante simple también:

Una vez más, observa que la función acepta dos argumentos: un enumerable y un elemento. En el interior simplemente nos apoyamos en la función member?/2 para buscar un animal en la lista de todos los animales.

Así que ahora corremos:

Y esto debería devolver true ya que efectivamente ¡tenemos un animal así en la lista!

Implementación de la función Reduce

Las cosas se vuelven un poco más complejas con la función reduce/3. Acepta los siguientes argumentos:

  • un enumerable al que aplicar la función
  • un acumulador para almacenar el resultado
  • la función reductora verdadera a aplicar

Lo interesante es que el acumulador contiene en realidad una tupla con dos valores: un verbo y un valor: {verb, value}. El verbo es un átomo y puede tener uno de los tres valores siguientes:

  • :cont (continuar)
  • :halt (terminar)
  • :suspend (suspender temporalmente)

El valor resultante devuelto por la función reduce/3 es también una tupla que contiene el estado y un resultado. El estado también es un átomo y puede tener los siguientes valores:

  • :done (el procesamiento está hecho, ese es el resultado final)
  • :halted (el procesamiento se detuvo porque el acumulador contenía el verbo :halt)
  • :suspended (el procesamiento fue suspendido)

Si el procesamiento fue suspendido, debemos devolver una función que represente el estado actual del procesamiento.

Todos estos requisitos están bien demostrados por la implementación de la función reduce/3 para las listas (tomada de los docs):

Podemos utilizar este código como ejemplo y codificar nuestra propia implementación para la estructura Zoo:

En la última cláusula de la función, tomamos la cabeza de la lista que contiene todos los animales, le aplicamos la función y luego ejecutamos reduce contra la cola. Cuando no quedan más animales (la tercera cláusula), devolvemos una tupla con el estado de :done y el resultado final. La primera cláusula devuelve un resultado si se suspendió el procesamiento. La segunda cláusula devuelve una función si se pasó el verbo :suspend.

Ahora, por ejemplo, podemos calcular fácilmente la edad total de todos nuestros animales:

Básicamente, ahora tenemos acceso a todas las funciones proporcionadas por el módulo Enum. Vamos a intentar utilizar join/2:

Sin embargo, obtendrás un error diciendo que el protocolo String.Chars no está implementado para la estructura Animal. Esto sucede porque join intenta convertir cada elemento en una cadena, pero no puede hacerlo para el Animal. Por lo tanto, vamos a implementar también el protocolo String.Chars ahora:

Ahora todo debería funcionar bien. Además, puedes intentar ejecutar each/2 y mostrar animales individuales:

Una vez más, esto funciona porque hemos implementado dos protocolos: Enumerable (para el Zoo) y String.Chars (para el Animal).

Conclusión

En este artículo, hemos discutido cómo se implementa el polimorfismo en Elixir usando protocolos. Has aprendido a definir e implementar protocolos, así como a utilizar los protocolos incorporados: Enumerable, Inspect y String.Chars.

Como ejercicio, puedes intentar potenciar nuestro módulo Zoo con el protocolo Collectable para poder utilizar adecuadamente la función Enum.into/2. Este protocolo requiere la implementación de una sola función: into/2, que recoge valores y devuelve el resultado (ten en cuenta que también tiene que soportar los verbos :done, :halt y :cont; el estado no debe ser reportado). ¡Comparte tu solución en los comentarios!

Espero que hayas disfrutado de la lectura de este artículo. Si te queda alguna pregunta, no dudes en ponerte en contacto conmigo. Gracias por la paciencia, ¡y hasta pronto!

Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.