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 aAny
“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 subeProtocol.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
pormodule
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
Sección de Reseñas y Valoraciones
Recuerda dar difusión a este ensayo si te valió la pena.