Polimorfismo Con Protocolos En Elixir

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

Enum.count (1… 3) Enum.count ([1,2,3])

¿Cómo es eso posible? En Elixir, el polimorfismo se logra mediante el uso de una característica interesante llamada protocolo, que actúa como un contrato. Para cada tipo de datos que desee admitir, este protocolo debe implementarse.

Con todo, este enfoque no es revolucionario, ya que se encuentra en otros idiomas (como Ruby, por ejemplo). Aún así, los protocolos son realmente convenientes, por lo que en este artículo analizaremos cómo definir, implementar y trabajar con ellos mientras exploramos algunos ejemplos. Empecemos!

Breve introducción a los protocolos

Entonces, como ya se mencionó anteriormente, un protocolo tiene algún código genérico y se basa en el tipo de datos específico para implementar la lógica. Esto es razonable, porque los diferentes tipos de datos pueden requerir diferentes implementaciones. Un tipo de datos puede entonces envío en un protocolo sin preocuparse por sus aspectos internos.

Elixir tiene un montón de protocolos incorporados, incluyendo Enumerable, Coleccionable, Inspeccionar, List.Chars, y String.Chars. Algunos de ellos serán discutidos más adelante en este artículo. Puede implementar cualquiera de estos protocolos en su módulo personalizado y obtener un montón de funciones de forma gratuita. Por ejemplo, una vez implementado Enumerable, tendrá acceso a todas las funciones definidas en el módulo Enum, lo cual es bastante bueno..

Si has venido del maravilloso mundo Ruby lleno de objetos, clases, hadas y dragones, habrás conocido un concepto muy similar de mixins. Por ejemplo, si alguna vez necesita hacer que sus objetos sean comparables, simplemente mezcle un módulo con el nombre correspondiente en la clase. Entonces solo implementa una nave espacial <=> Método y todas las instancias de la clase obtendrán todos los métodos como > y < gratis. Este mecanismo es algo similar a los protocolos en Elixir. Incluso si nunca antes has conocido este concepto, créeme, no es tan complejo. 

Bien, lo primero es lo primero: el protocolo debe estar definido, así que veamos cómo se puede hacer en la siguiente sección..

Definiendo un protocolo

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

defprotocol MyProtocol do final

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 solo define una interfaz, un plano que deben implementar todos los tipos de datos que deseen enviar en este protocolo:

defprotocol MyProtocol do def my_func (arg) end

En este ejemplo, un programador necesita implementar el my_func / 1 funciona para utilizar con éxito MyProtocol.

Si el protocolo no está implementado, se generará un error. Volvamos al ejemplo con la contar / 1 función definida dentro de la Enumerar módulo. Ejecutar el siguiente código terminará con un error:

Enum.count 1 # ** (Protocol.UndefinedError) protocol Enumerable no implementado para 1 # (elixir) lib / enum.ex: 1: Enumerable.impl_for! / 1 # (elixir) lib / enum.ex: 146: Enumerable. count / 1 # (elixir) lib / enum.ex: 467: Enum.count / 1

Significa que el Entero no implementa el Enumerable Protocolo (qué sorpresa) y, por lo tanto, no podemos contar los enteros. Pero el protocolo en realidad puede ser implementado, y esto es fácil de lograr.  

Implementando un protocolo

Los protocolos se implementan utilizando la macro defimpl / 3. Usted especifica qué protocolo implementar y para qué tipo:

defimpl MyProtocol, para: Integer def my_func (arg) do IO.puts (arg) end end end

Ahora puede hacer que sus enteros sean contables implementando parcialmente Enumerable protocolo:

defimpl Enumerable, para: Integer do def count (_arg) do : ok, 1 # los enteros siempre contienen un elemento extremo final Enum.count (100) |> IO.puts # => 1

Discutiremos la Enumerable protocolo con más detalle más adelante en el artículo e implementar su otra función, así.

En cuanto al tipo (pasado al para), puede especificar cualquier tipo incorporado, su propio alias o una lista de alias:

defimpl MyProtocol, para: [Integer, List] do end

 Además de eso, puedes decir Alguna:

defimpl MyProtocol, para: Cualquier def my_func (_) do IO.puts "No implementado!" final fin

Esto actuará como una implementación alternativa, y no se generará un error si el protocolo no se implementa para algún tipo. Para que esto funcione, configure el @fallback_to_any atribuir a cierto Dentro de su protocolo (de lo contrario, el error seguirá apareciendo):

defprotocol MyProtocol do @fallback_to_any true def my_func (arg) end

Ahora puede utilizar el protocolo para cualquier tipo compatible:

MyProtocol.my_func (5) # simplemente imprime 5 MyProtocol.my_func ("prueba") # imprime "¡No implementado!"

Una nota sobre estructuras

La implementación de un protocolo se puede anidar dentro de un módulo. Si este módulo define una estructura, ni siquiera necesita especificar para al llamar defimpl:

defmodule El producto defstruye el título: "", precio: 0 defimpl MyProtocol do def my_func (% Product title: title, price: price) do IO.puts "Title # title, price # price" end end end end

En este ejemplo, definimos una nueva estructura llamada Producto e implementar nuestro protocolo demo. En el interior, simplemente ajuste el título y el precio y luego genere una cadena.

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

Ejemplo: String.Chars Protocol

De acuerdo, basta con la teoría abstracta: echemos un vistazo a algunos ejemplos. Estoy seguro de que ha empleado la función IO.puts / 2 de forma bastante extensa para enviar información de depuración a la consola cuando juega con Elixir. Seguramente, podemos generar varios tipos incorporados fácilmente:

IO.puts 5 IO.puts "prueba" IO.puts: my_atom

Pero, ¿qué pasa si intentamos dar salida a nuestra Producto estructura creada en la sección anterior? Pondré el código correspondiente dentro del Principal módulo porque de lo contrario obtendrá un error que dice que la estructura no está definida o que se accede en el mismo ámbito:

defmodule Producto do defstruct title: "", price: 0 end defmodule Main do def run do% Product title: "Test", price: 5 |> IO.puts end end Main.run

Habiendo ejecutado este código, obtendrás un error:

 (Protocol.UndefinedError) protocolo String.Chars no implementado para% Producto precio: 5, título: "Prueba"

Jajaja Significa que el pone La función se basa en el protocolo incorporado String.Chars. Mientras no esté implementado para nuestro Producto, el error esta siendo levantado.

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

defmodule El producto defstruye el título: "", precio: 0 defimpl String.Chars do def to_string (% Product title: title, price: price) do "# title, $ # price" end end end end

Teniendo este código en su lugar, el programa emitirá la siguiente cadena:

Prueba, $ 5

Lo que significa que todo está funcionando bien.!

Ejemplo: inspeccionar protocolo

Otra función muy común es IO.inspect / 2 para obtener información sobre una construcción. También hay una función inspeccionar / 2 definida dentro del Núcleo módulo-realiza la inspección de acuerdo con el protocolo incorporado Inspeccionar.

Nuestro Producto La estructura se puede inspeccionar de inmediato, y obtendrá información breve al respecto:

% Producto título: "Prueba", precio: 5 |> IO.inspect # o:% Producto título: "Prueba", precio: 5 |> inspeccionar |> IO.puts

Volverá % Producto precio: 5, título: "Prueba". Pero, una vez más, podemos implementar fácilmente el Inspeccionar Protocolo que requiere solo la función inspeccionar / 2 para ser codificado:

Defmodule Product do defstruct title: "", price: 0 defimpl Inspect do def inspect (% Product title: title, price: price, _) do "Esa es una estructura del producto. Tiene un título de # title y un precio de # precio. ¡Yay! " fin extremo fin 

El segundo argumento que se pasa a esta función es la lista de opciones, pero no estamos interesados ​​en ellas..

Ejemplo: Protocolo Enumerable

Ahora veamos 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 prácticas como cada / 2 y count / 1 (sin él, tendrías que seguir con la recursión antigua).

Enumerable define tres funciones que debe completar para implementar el protocolo:

  • contar / 1 devuelve el tamaño del enumerable.
  • miembro? / 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 todos los beneficios proporcionados por el Enumerar módulo, que es una buena oferta.

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

Defmodule Zoo do defstruct title: "", animals: [] end

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

Defmodule Animal do defstruct species: "", name: "", age: 0 end

Ahora vamos a crear una instancia de un nuevo zoológico:

defmodule Main do def run do my_zoo =% Zoo title: "Demo Zoo", animales: [% Animal especies: "tigre", nombre: "Tigga", edad: 5,% Animal especies: "caballo", nombre: "Asombroso", edad: 3,% Animal especie: "ciervo", nombre: "Bambi", edad: 2] final final Main.run

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

Enum.count (my_zoo) |> IO.inspect

Vamos a implementar esta funcionalidad ahora!

Implementando la función de conteo

¿Qué queremos decir cuando decimos "cuenta mi zoológico"? 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 simple:

defmodule Zoo do defstruct title: "", animals: [] defimpl Enumerable do def count (% Zoo animals: animals) do : ok, Enum.count (animals) end end end end

Todo lo que hacemos aquí es confiar en la función contar / 1 mientras le pasamos una lista de animales (porque esta función admite listas fuera de la caja). Una cosa muy importante a mencionar es que la contar / 1 La función debe devolver su resultado en forma de tupla. : ok, resultado Según lo dictado por los documentos. Si devuelves solo un número, un error.  ** (CaseClauseError) sin coincidencia de cláusula de caso será levantado.

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

Miembro implementador? Función

La siguiente función que define el protocolo es la miembro? / 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 animal en particular vive en el zoológico o no. Por lo tanto, la implementación es bastante simple también:

defmodule Zoo do defstruct title: "", animals: [] defimpl Enumerable do # ... def member? (% Zoo title: _, animals: animals, animal) do : ok, Enum.member? (animales, animal)  fin fin fin

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

Así que ahora corremos:

Enum.member? (My_zoo,% Animal especie: "tigre", nombre: "Tigga", edad: 5) |> IO.inspect

Y esto debería volver. cierto Como de hecho tenemos un animal en la lista!

Implementando la Función Reducir

Las cosas se ponen un poco más complejas con el reducir / 3 función. Acepta los siguientes argumentos:

  • un enumerable para aplicar la función a
  • Un acumulador para almacenar el resultado.
  • la función reductora real para aplicar

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

  • : cont (continuar)
  • :detener (Terminar)
  • :suspender (suspender temporalmente)

El valor resultante devuelto por el reducir / 3 La función 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: 

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

Si se suspendió el procesamiento, deberíamos devolver una función que represente el estado actual del procesamiento..

Todos estos requisitos están bien demostrados por la implementación del reducir / 3 Función para las listas (tomadas de los documentos):

def reduce (_, : detener, acc, _fun), hacer: : detenido, acc def reducir (lista, : suspender, acc, diversión), hacer: : suspendido, acc, y reducir (lista, & 1, diversión) def reduce ([], : cont, acc, _fun), haz: : hecho, acc def reduce ([h | t], : cont, acc, diversión), haz: reducir (t, diversión. (h, acc), diversión)

Podemos usar este código como ejemplo y codificar nuestra propia implementación para el zoo estructura:

defmodule Zoo do defstruct title: "", animals: [] defimpl Enumerable do def reduce (_, : halt, acc, _fun), do: : halted, acc def reduce (% Zoo animals: animals, : suspender, acc, diversión) do : suspendido, acc y reducir (% Zoo animals: animals, & 1, fun) end def reduce (% Zoo animals: [], : cont, acc , _fun), do: : done, acc def reduce (% Zoo animals: [head | tail], : cont, acc, fun) do reduce (% Zoo animals: tail, fun. ( cabeza, acc), diversión) end end end end

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

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

Enum.reduce (my_zoo, 0, fn (animal, total_age) -> animal.age + total_age end) |> IO.puts

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

Enum.join (my_zoo) |> IO.inspect

Sin embargo, obtendrá un error diciendo que el String.Chars protocolo no está implementado para el Animal estructura Esto esta pasando porque unirse intenta convertir cada elemento en una cadena, pero no puede hacerlo para el Animal. Por lo tanto, también implementemos el String.Chars protocolo ahora

defmodule Animal do defstruct species: "", name: "", age: 0 defimpl String.Chars do def to_string (% Animal species: species, name: name, age: age) do "# name (#  especie), envejecido # age "end end end end

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

Enum.each (my_zoo, & (IO.puts (& 1)))

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 utilizando protocolos. Aprendió a definir e implementar protocolos, así como a utilizar protocolos integrados: Enumerable, Inspeccionar, y String.Chars.

Como ejercicio, puedes intentar potenciar nuestra zoo módulo con el protocolo coleccionable para que la función Enum.into / 2 pueda ser utilizada correctamente. Este protocolo requiere la implementación de una sola función: en / 2, que recopila valores y devuelve el resultado (tenga en cuenta que también debe admitir la :hecho, :detener y : cont verbos el estado no debe ser reportado). Comparte tu solución en los comentarios.!

Espero que hayas disfrutado leyendo este artículo. Si tiene alguna pregunta, no dude en ponerse en contacto conmigo. Gracias por la paciencia y hasta pronto.!