Referencia y funciones para trabajar con protocolos.

Un protocolo especifica una API que debe ser definida por sus implementaciones. Un protocolo se define con Kernel.defprotocol/2 y sus implementaciones con Kernel.defimpl/3.

Un caso real

En Elixir, tenemos dos sustantivos para verificar cuántos elementos hay en una estructura de datos: length y size. length significa que la información debe calcularse. Por ejemplo, length(list) necesita recorrer toda la lista para calcular su longitud. Por otra parte, tuple_size(tuple) y byte_size(binary) no dependa de la tupla y el tamaño binario, ya que la información de tamaño se calcula previamente en la estructura de datos.

Aunque Elixir incluye funciones específicas como tuple_size, binary_size y map_size, a veces queremos poder recuperar el tamaño de una estructura de datos independientemente de su tipo. En Elixir podemos escribir código polimórfico, es decir, código que trabaja con diferentes formas / tipos, usando protocolos. Se podría implementar un protocolo de tamaño de la siguiente manera:

defprotocolSizedo@doc"Calculates the size (and not the length!) of a data structure"defsize(data)end

Ahora que el protocolo se puede implementar para cada estructura de datos, el protocolo puede tener una implementación compatible para:

defimplSize,for:BitStringdodefsize(binary),do:byte_size(binary)enddefimplSize,for:Mapdodefsize(map),do:map_size(map)enddefimplSize,for:Tupledodefsize(tuple),do:tuple_size(tuple)end

Tenga en cuenta que no lo implementamos para las listas, ya que no tenemos el size información en listas, más bien su valor debe calcularse con length.

La estructura de datos para la que está implementando el protocolo debe ser el primer argumento de todas las funciones definidas en el protocolo.

Es posible implementar protocolos para todos los tipos de Elixir:

  • Estructuras (consulte la sección “Protocolos y estructuras” a continuación)
  • Tuple
  • Atom
  • List
  • BitString
  • Integer
  • Float
  • Function
  • PID
  • Map
  • Port
  • Reference
  • Any (consulte el “Retorno a Any“sección siguiente)

Protocolos y estructuras

El beneficio real de los protocolos se produce cuando se mezclan con estructuras. Por ejemplo, Elixir se envía con muchos tipos de datos implementados como estructuras, como MapSet. Podemos implementar el Size protocolo para esos tipos también:

defimplSize,for:MapSetdodefsize(map_set),do:MapSet.size(map_set)end

Al implementar un protocolo para una estructura, el :for La opción se puede omitir si la defimpl/3 La llamada está dentro del módulo que define la estructura:

defmoduleUserdodefstruct[:email,:name]defimplSizedo# two fieldsdefsize(%User),do:2endend

Si no se encuentra una implementación de protocolo para un tipo determinado, la invocación del protocolo aumentará a menos que esté configurado para volver a Any. También están disponibles las conveniencias para construir implementaciones sobre las existentes, consulte defstruct/1 para obtener más información sobre cómo derivar protocolos.

Volver a Any

En algunos casos, puede ser conveniente proporcionar una implementación predeterminada para todos los tipos. Esto se puede lograr configurando el @fallback_to_any atribuir a true en la definición del protocolo:

defprotocolSizedo@fallback_to_anytruedefsize(data)end

los Size ahora se puede implementar el protocolo para Any:

defimplSize,for:Anydodefsize(_),do:0end

Aunque podría decirse que la implementación anterior no es razonable. Por ejemplo, no tiene sentido decir que un PID o un entero tienen un tamaño de 0. Esa es una de las razones por las que @fallback_to_any es un comportamiento opt-in. Para la mayoría de los protocolos, generar un error cuando no se implementa un protocolo es el comportamiento adecuado.

Varias implementaciones

Los protocolos también se pueden implementar para varios tipos a la vez:

defprotocolReversibledodefreverse(term)enddefimplReversible,for:[Map,List]dodefreverse(term),do:Enum.reverse(term)end

Dentro defimpl/3, puedes usar @protocol para acceder al protocolo que se está implementando y @for para acceder al módulo para el que se está definiendo.

Tipos

La definición de un protocolo define automáticamente un tipo de aridad cero llamado t, que se puede utilizar de la siguiente manera:

@specprint_size(Size.t()):::okdefprint_size(data)do
  result =caseSize.size(data)do0->"data has no items"1->"data has one item"
      n ->"data has #n items"endIO.puts(result)end

los @spec arriba expresa que todos los tipos permitidos para implementar el protocolo dado son tipos de argumentos válidos para la función dada.

Reflexión

Cualquier módulo de protocolo contiene tres funciones adicionales:

  • __protocol__/1 – devuelve la información del protocolo. La función toma uno de los siguientes átomos:

    • :consolidated? – devuelve si el protocolo está consolidado
    • :functions – devuelve una lista de palabras clave de funciones de protocolo y sus aridades
    • :impls – si consolidado, devoluciones :consolidated, modules con la lista de módulos que implementan el protocolo, de lo contrario :not_consolidated
    • :module – el nombre del átomo del módulo de protocolo
  • impl_for/1 – devuelve el módulo que implementa el protocolo para el argumento dado, nil de lo contrario

  • impl_for!/1 – igual que arriba pero sube Protocol.UndefinedError si no se encuentra una implementación

Por ejemplo, para el Enumerable protocolo que tenemos:

iex>Enumerable.__protocol__(:functions)[count:1,member?:2,reduce:3,slice:1]

iex>Enumerable.impl_for([])Enumerable.List

iex>Enumerable.impl_for(42)nil

Además, cada módulo de implementación de protocolo contiene el __impl__/1 función. La función toma uno de los siguientes átomos:

  • :for – devuelve el módulo responsable de la estructura de datos de la implementación del protocolo

  • :protocol – devuelve el módulo de protocolo para el que se proporciona esta implementación

Por ejemplo, el módulo que implementa el Enumerable protocolo para listas es Enumerable.List. Por lo tanto, podemos invocar __impl__/1 en este módulo:

iex(1)>Enumerable.List.__impl__(:for)Listiex(2)>Enumerable.List.__impl__(:protocol)Enumerable

Consolidación

Para acelerar el despacho de protocolos, siempre que todas las implementaciones de protocolos se conocen de antemano, normalmente después de que se compila todo el código de Elixir en un proyecto, Elixir proporciona una función llamada consolidación de protocolo. La consolidación vincula directamente los protocolos a sus implementaciones de manera que invocar una función de un protocolo consolidado equivale a invocar dos funciones remotas.

La consolidación de protocolos se aplica de forma predeterminada a todos los proyectos Mix durante la compilación. Esto puede ser un problema durante la prueba. Por ejemplo, si desea implementar un protocolo durante la prueba, la implementación no tendrá ningún efecto, ya que el protocolo ya se ha consolidado. Una posible solución es incluir directorios de compilación que sean específicos de su entorno de prueba en su mix.exs:

def project do...elixirc_paths:elixirc_paths(Mix.env())...enddefpelixirc_paths(:test),do:["lib","test/support"]defpelixirc_paths(_),do:["lib"]

Y luego puede definir las implementaciones específicas para el entorno de prueba dentro test/support/some_file.ex.

Otro enfoque es deshabilitar la consolidación de protocolos durante las pruebas en su mix.exs:

def project do...consolidate_protocols:Mix.env()!=:test...end

Aunque no se recomienda hacerlo, ya que puede afectar el rendimiento de su suite de pruebas.

Finalmente, tenga en cuenta que todos los protocolos se compilan con debug_info ajustado a true, independientemente de la opción establecida por el elixirc compilador. La información de depuración se utiliza para la consolidación y se elimina después de la consolidación, a menos que se establezca globalmente.

Funciones

asert_impl! (protocolo, base)

Comprueba si el módulo dado está cargado y es una implementación del protocolo dado.

asert_protocol! (módulo)

Comprueba si el módulo dado está cargado y es protocolo.

consolidar (protocolo, tipos)

Recibe un protocolo y una lista de implementaciones y consolida el protocolo dado.

consolidado? (protocolo)

Devoluciones true si el protocolo se consolidó.

derivar (protocolo, módulo, opciones \ [])

Deriva el protocol por module con las opciones dadas.

extract_impls (protocolo, rutas)

Extrae todos los tipos implementados para el protocolo dado de las rutas dadas.

extract_protocols (rutas)

Extrae todos los protocolos de las rutas dadas.

asert_impl! (protocolo, base)Fuente

Especificaciones

assert_impl!(module(),module()):::ok

Comprueba si el módulo dado está cargado y es una implementación del protocolo dado.

Devoluciones :ok si es así, de lo contrario aumenta ArgumentError.

asert_protocol! (módulo)Fuente

Especificaciones

assert_protocol!(module()):::ok

Comprueba si el módulo dado está cargado y es protocolo.

Devoluciones :ok si es así, de lo contrario aumenta ArgumentError.

consolidar (protocolo, tipos)Fuente

Especificaciones

consolidate(module(),[module()]):::ok,binary()|:error,:not_a_protocol|:error,:no_beam_info

Recibe un protocolo y una lista de implementaciones y consolida el protocolo dado.

La consolidación ocurre cambiando el protocolo impl_for en formato abstracto para tener reglas de búsqueda rápida. Por lo general, la lista de implementaciones que se utilizarán durante la consolidación se recupera con la ayuda de extract_impls/2.

Devuelve la versión actualizada del código de bytes del protocolo. Si el primer elemento de la tupla es :ok, significa que el protocolo se consolidó.

Se puede verificar que un código de bytes o implementación de protocolo dado esté consolidado o no mediante el análisis del atributo del protocolo:

Protocol.consolidated?(Enumerable)

Esta función no carga el protocolo en ningún momento ni carga el nuevo bytecode para el módulo compilado. Sin embargo, cada implementación debe estar disponible y se cargará.

consolidado? (protocolo)Fuente

Especificaciones

consolidated?(module())::boolean()

Devoluciones true si el protocolo se consolidó.

derivar (protocolo, módulo, opciones \ [])Fuente

Deriva el protocol por module con las opciones dadas.

Si su implementación pasa las opciones o si está generando código personalizado basado en la estructura, también necesitará implementar una macro definida como __deriving__(module, struct, options) para obtener las opciones que se aprobaron.

Ejemplos de

defprotocolDerivabledodefok(arg)enddefimplDerivable,for:Anydo
  defmacro __deriving__(module, struct, options)do
    quote dodefimplDerivable,for:unquote(module)dodefok(arg)do:ok, arg,unquote(Macro.escape(struct)),unquote(options)endendendenddefok(arg)do:ok, argendenddefmoduleImplStructdo@derive[Derivable]defstructa:0,b:0endDerivable.ok(%ImplStruct)#=> :ok, %ImplStructa: 0, b: 0, %ImplStructa: 0, b: 0, []

Las derivaciones explícitas ahora se pueden llamar a través de __deriving__/3:

# Explicitly derived via `__deriving__/3`Derivable.ok(%ImplStructa:1,b:1)#=> :ok, %ImplStructa: 1, b: 1, %ImplStructa: 0, b: 0, []# Explicitly derived by API via `__deriving__/3`requireProtocolProtocol.derive(Derivable,ImplStruct,:oops)Derivable.ok(%ImplStructa:1,b:1)#=> :ok, %ImplStructa: 1, b: 1, %ImplStructa: 0, b: 0, :oops

extract_impls (protocolo, rutas)Fuente

Especificaciones

extract_impls(module(),[charlist()|String.t()])::[atom()]

Extrae todos los tipos implementados para el protocolo dado de las rutas dadas.

Las rutas pueden ser una lista de amigos o una cadena. Internamente se trabaja en ellos como charlists, por lo que pasarlos como listas evita conversiones adicionales.

No carga ninguna de las implementaciones.

Ejemplos de

# Get Elixir's ebin directory path and retrieve all protocols
iex> path =:code.lib_dir(:elixir,:ebin)
iex> mods =Protocol.extract_impls(Enumerable,[path])
iex>Listin mods
true

extract_protocols (rutas)Fuente

Especificaciones

extract_protocols([charlist()|String.t()])::[atom()]

Extrae todos los protocolos de las rutas dadas.

Las rutas pueden ser una lista de amigos o una cadena. Internamente se trabaja en ellos como charlists, por lo que pasarlos como listas evita conversiones adicionales.

No carga ninguno de los protocolos.

Ejemplos de

# Get Elixir's ebin directory path and retrieve all protocols
iex> path =:code.lib_dir(:elixir,:ebin)
iex> mods =Protocol.extract_protocols([path])
iex>Enumerablein mods
true