Polimorfismo con protocolli in elisir

Il polimorfismo è un concetto importante nella programmazione e di solito i programmatori alle prime armi ne apprendono durante i primi mesi di studio. Il polimorfismo in pratica significa che è possibile applicare un'operazione simile a entità di tipi diversi. Ad esempio, la funzione count / 1 può essere applicata sia a un intervallo che a un elenco:

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

Come è possibile? In Elixir, il polimorfismo si ottiene utilizzando una caratteristica interessante chiamata protocollo, che agisce come un contrarre. Per ogni tipo di dati che si desidera supportare, è necessario implementare questo protocollo.

Tutto sommato, questo approccio non è rivoluzionario, in quanto si trova in altre lingue (come Ruby, per esempio). Tuttavia, i protocolli sono davvero convenienti, quindi in questo articolo discuteremo come definire, implementare e lavorare con loro mentre esploriamo alcuni esempi. Iniziamo!

Breve introduzione ai protocolli

Quindi, come già menzionato sopra, un protocollo ha un codice generico e si affida al tipo di dati specifico per implementare la logica. Questo è ragionevole, perché tipi di dati diversi possono richiedere implementazioni diverse. Un tipo di dati può quindi spedizione su un protocollo senza preoccuparsi dei suoi interni.

Elixir ha un sacco di protocolli integrati, inclusi Enumerable, collezione, Ispezionare, List.Chars, e String.Chars. Alcuni di loro saranno discussi più avanti in questo articolo. È possibile implementare uno qualsiasi di questi protocolli nel modulo personalizzato e ottenere un sacco di funzioni gratuitamente. Ad esempio, dopo aver implementato Enumerable, avrai accesso a tutte le funzioni definite nel modulo Enum, che è abbastanza interessante.

Se vieni dal meraviglioso mondo Ruby pieno di oggetti, classi, fate e draghi, avrai incontrato un concetto molto simile di mixins. Ad esempio, se hai mai bisogno di rendere i tuoi oggetti confrontabili, basta mescolare un modulo con il nome corrispondente nella classe. Quindi basta implementare un'astronave <=> metodo e tutte le istanze della classe avranno tutti i metodi come > e < gratuito. Questo meccanismo è in qualche modo simile ai protocolli in elisir. Anche se non hai mai incontrato prima questo concetto, credimi, non è così complesso. 

Ok, quindi prima le cose: il protocollo deve essere definito, quindi vediamo come può essere fatto nella prossima sezione.

Definizione di un protocollo

Definire un protocollo non implica alcuna magia nera, infatti è molto simile alla definizione dei moduli. Usa defprotocol / 2 per farlo:

defProtocol MyProtocol do end

All'interno della definizione del protocollo si posizionano le funzioni, proprio come con i moduli. L'unica differenza è che queste funzioni non hanno corpo. Significa che il protocollo definisce solo un'interfaccia, un progetto che dovrebbe essere implementato da tutti i tipi di dati che desiderano inviare su questo protocollo:

defprotocol MyProtocol do def my_func (arg) end

In questo esempio, un programmatore deve implementare il my_func / 1 funzione per utilizzare con successo MyProtocol.

Se il protocollo non è implementato, verrà generato un errore. Torniamo all'esempio con il contare / 1 funzione definita all'interno del enum modulo. L'esecuzione del seguente codice causerà un errore:

Enum.count Protocollo 1 # ** (Protocol.UndefinedError) Enumerabile non implementato per 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 che il Numero intero non implementa il Enumerable protocollo (che sorpresa) e, quindi, non possiamo contare gli interi. Ma il protocollo in realtà può essere implementato, e questo è facile da raggiungere.  

Implementazione di un protocollo

I protocolli sono implementati usando la macro defimpl / 3. Specificare quale protocollo implementare e per quale tipo:

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

Ora puoi rendere i tuoi interi numerabili implementando parzialmente il Enumerable protocollo:

defimpl Enumerable, per: Integer do def count (_arg) do : ok, 1 # gli interi contengono sempre un elemento end end Enum.count (100) |> IO.puts # => 1

Discuteremo del Enumerable protocollo più in dettaglio più avanti nell'articolo e implementare anche la sua altra funzione.

Per quanto riguarda il tipo (passato al per), puoi specificare qualsiasi tipo predefinito, il tuo alias o un elenco di alias:

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

 Oltre a ciò, puoi dire Qualunque:

defimpl MyProtocol, per: Qualsiasi def my_func (_) do IO.puts "Non implementato!" fine fine

Ciò si comporterà come un'implementazione fallback e non verrà generato un errore se il protocollo non è implementato per alcun tipo. Affinché questo funzioni, imposta il @fallback_to_any attribuire a vero all'interno del tuo protocollo (altrimenti l'errore verrà comunque generato):

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

Ora puoi utilizzare il protocollo per qualsiasi tipo supportato:

MyProtocol.my_func (5) # stampa semplicemente 5 MyProtocol.my_func ("test") # prints "Non implementato!"

Una nota sulle strutture

L'implementazione per un protocollo può essere annidata all'interno di un modulo. Se questo modulo definisce una struttura, non è nemmeno necessario specificare per quando si chiama defimpl:

defmodule Product do defstruct title: "", prezzo: 0 defimpl MyProtocol do def my_func (% Product titolo: title, price: price) do IO.puts "Title # title, price # price" end end end

In questo esempio, definiamo una nuova struttura chiamata Prodotto e implementare il nostro protocollo demo. All'interno, è sufficiente associare il modello al titolo e al prezzo e quindi generare una stringa.

Ricorda, tuttavia, che un'implementazione deve essere annidata all'interno di un modulo, significa che puoi facilmente estendere qualsiasi modulo senza accedere al suo codice sorgente.

Esempio: protocollo String.Chars

Ok, basta con la teoria astratta: diamo un'occhiata ad alcuni esempi. Sono sicuro che hai utilizzato la funzione IO.puts / 2 in modo abbastanza estensivo per inviare informazioni di debug alla console quando giochi con Elixir. Sicuramente, possiamo generare facilmente vari tipi built-in:

IO.puts 5 IO.puts "test" IO.puts: my_atom

Ma cosa succede se proviamo a produrre i nostri Prodotto struct creata nella sezione precedente? Inserirò il codice corrispondente all'interno del Principale modulo perché altrimenti si otterrà un errore che dice che la struttura non è definita o acceduta nello stesso ambito:

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

Dopo aver eseguito questo codice, riceverai un errore:

 (Protocol.UndefinedError) protocollo String.Chars non implementato per% Product prezzo: 5, titolo: "Test"

Aha! Significa che il mette la funzione si basa sul protocollo String.Chars incorporato. Finché non è implementato per il nostro Prodotto, l'errore è stato sollevato.

String.Chars è responsabile della conversione di varie strutture in binari e l'unica funzione che è necessario implementare è to_string / 1, come indicato dalla documentazione. Perché non lo implementiamo ora?

defmodule Product do defstruct title: "", prezzo: 0 defimpl String.Chars do def to_string (% Product titolo: titolo, prezzo: prezzo) fai "# title, $ # price" end end end

Avendo questo codice in atto, il programma produrrà la seguente stringa:

Prova, $ 5

Il che significa che tutto funziona perfettamente!

Esempio: ispezionare il protocollo

Un'altra funzione molto comune è IO.inspect / 2 per ottenere informazioni su un costrutto. C'è anche una funzione di ispezione / 2 definita all'interno del nocciolo modulo: esegue l'ispezione secondo il protocollo integrato Inspect.

Nostro Prodotto struct può essere ispezionato subito e riceverai alcune brevi informazioni a riguardo:

% Product title: "Test", prezzo: 5 |> IO.inspect # oppure:% Product title: "Test", prezzo: 5 |> inspect |> IO.puts

Ritornerà % Prodotto prezzo: 5, titolo: "Test". Ma, ancora una volta, possiamo facilmente implementare il Ispezionare protocollo che richiede solo la funzione inspect / 2 da codificare:

defmodule Product do defstruct title: "", prezzo: 0 defimpl Inspect do def inspect (% Product titolo: title, price: price, _) do "Questa è una struct Product. Ha un titolo di # title e un prezzo di # prezzo. Yay! " fine fine 

Il secondo argomento passato a questa funzione è l'elenco di opzioni, ma non ci interessa.

Esempio: protocollo enumerabile

Ora vediamo un esempio leggermente più complesso mentre parliamo del protocollo Enumerable. Questo protocollo è impiegato dal modulo Enum, che ci presenta funzioni così convenienti come each / 2 e count / 1 (senza di esso, si dovrebbe rimanere con una semplice vecchia ricorsione).

Enumerable definisce tre funzioni che devi implementare per implementare il protocollo:

  • count / 1 restituisce la dimensione dell'enumerabile.
  • member? / 2 controlla se l'enumerable contiene un elemento.
  • riduci / 3 applica una funzione a ciascun elemento dell'enumerabile.

Avendo tutte quelle funzioni in atto, avrai accesso a tutte le chicche fornite da enum modulo, che è davvero un buon affare.

Ad esempio, creiamo una nuova struttura chiamata Zoo. Avrà un titolo e un elenco di animali:

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

Ogni animale sarà anche rappresentato da una struttura:

defmodule Animal do defstruct species: "", nome: "", età: 0 fine

Ora inizializziamo un nuovo zoo:

defmodule Main do def run do my_zoo =% Zoo titolo: "Demo Zoo", animali: [% Animale specie: "tigre", nome: "Tigga", età: 5,% Animale specie: "cavallo", nome: "Amazing", età: 3,% Animal species: "deer", nome: "Bambi", età: 2] end end Main.run

Quindi abbiamo uno "Zoo Demo" con tre animali: una tigre, un cavallo e un cervo. Quello che mi piacerebbe fare ora è aggiungere il supporto per la funzione count / 1, che verrà utilizzata in questo modo:

Enum.count (my_zoo) |> IO.inspect

Implementiamo ora questa funzionalità!

Implementazione della funzione di conteggio

Cosa intendiamo quando dici "conta il mio zoo"? Sembra un po 'strano, ma probabilmente significa contare tutti gli animali che vivono lì, quindi l'implementazione della funzione sottostante sarà abbastanza semplice:

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

Tutto ciò che facciamo qui è fare affidamento sulla funzione count / 1 mentre si passa ad una lista di animali (perché questa funzione supporta gli elenchi fuori dalla scatola). Una cosa molto importante da ricordare è che il contare / 1 la funzione deve restituire il risultato sotto forma di una tupla : ok, risultato come dettato dai documenti. Se si restituisce solo un numero, un errore  ** (CaseClauseError) nessuna corrispondenza della clausola sarà sollevato.

Questo è praticamente tutto. Adesso puoi dire Enum.count (my_zoo) dentro il Main.run, e dovrebbe tornare 3 di conseguenza. Buon lavoro!

Membro di attuazione? Funzione

La prossima funzione che il protocollo definisce è la membro? / 2. Dovrebbe restituire una tupla : ok, booleano come risultato che dice se un enumerable (passato come primo argomento) contiene un elemento (il secondo argomento).

Voglio questa nuova funzione per dire se un particolare animale vive nello zoo o no. Pertanto, l'implementazione è abbastanza semplice:

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

Ancora una volta, si noti che la funzione accetta due argomenti: un enumerabile e un elemento. Dentro facciamo semplicemente affidamento sul membro? / 2 funzione per cercare un animale nella lista di tutti gli animali.

Quindi ora eseguiamo:

Enum.member? (My_zoo,% Animal species: "tiger", nome: "Tigga", età: 5) |> IO.inspect

E questo dovrebbe tornare vero come abbiamo davvero un tale animale nella lista!

Implementazione della funzione di riduzione

Le cose diventano un po 'più complesse con il ridurre / 3 funzione. Accetta i seguenti argomenti:

  • un enumerabile per applicare la funzione a
  • un accumulatore per memorizzare il risultato
  • la funzione effettiva del riduttore da applicare

La cosa interessante è che l'accumulatore contiene effettivamente una tupla con due valori: a verbo e un valore: verbo, valore. Il verbo è un atomo e può avere uno dei seguenti tre valori:

  • : cont (Continua)
  • : battuta d'arresto (terminare)
  • :sospendere (sospendere temporaneamente)

Il valore risultante restituito da ridurre / 3 la funzione è anche una tupla contenente lo stato e un risultato. Lo stato è anche un atomo e può avere i seguenti valori: 

  • :fatto (l'elaborazione è terminata, questo è il risultato finale)
  • : arrestato (l'elaborazione è stata interrotta perché l'accumulatore conteneva il file : battuta d'arresto verbo)
  • :sospeso (l'elaborazione è stata sospesa)

Se l'elaborazione è stata sospesa, dovremmo restituire una funzione che rappresenta lo stato corrente dell'elaborazione.

Tutti questi requisiti sono ben dimostrati dall'implementazione del ridurre / 3 funzione per gli elenchi (presi dai documenti):

def reduce (_, : halt, acc, _fun), do: : halted, acc def reduce (list, : suspend, acc, fun), do: : sospeso, acc, e reduce (lista, & 1, fun) def reduce ([], : cont, acc, _fun), do: : done, acc def reduce ([h | t], : cont, acc, fun), do: ridurre (t, divertimento. (h, acc), divertimento)

Possiamo usare questo codice come esempio e codificare la nostra implementazione per il Zoo struct:

defmodule Zoo do defstruct title: "", animals: [] defimpl Enumerable do def reduce (_, : halt, acc, _fun), do: : halted, acc def reduce (% Zoo animals: animals, : suspend, acc, fun) do : sospeso, acc, e reduce (% 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) riduci (% Zoo animals: tail, fun. ( testa, acc), divertimento) fine fine

Nell'ultima clausola di funzione, prendiamo la testa dell'elenco contenente tutti gli animali, applichiamo la funzione ad esso, quindi eseguiamo ridurre contro la coda. Quando non ci sono più animali rimasti (la terza clausola), restituiamo una tupla con lo stato di :fatto e il risultato finale. La prima clausola restituisce un risultato se l'elaborazione è stata interrotta. La seconda clausola restituisce una funzione se il :sospendere è stato passato un verbo.

Ora, ad esempio, possiamo calcolare facilmente l'età totale di tutti i nostri animali:

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

Fondamentalmente, ora abbiamo accesso a tutte le funzioni fornite dal enum modulo. Proviamo a utilizzare join / 2:

Enum.join (my_zoo) |> IO.inspect

Tuttavia, riceverai un errore dicendo che il String.Chars il protocollo non è implementato per il Animale struct. Questo sta accadendo perché aderire prova a convertire ogni elemento in una stringa, ma non può farlo per il Animale. Pertanto, implementiamo anche il String.Chars protocollo ora:

defmodule Animal do defstruct species: "", nome: "", età: 0 defimpl String.Chars do def to_string (% Animal specie: specie, nome: nome, età: età) fai "# nome (#  specie), invecchiato # età "end end end

Ora tutto dovrebbe funzionare bene. Inoltre, puoi provare a eseguire ogni / 2 e visualizzare i singoli animali:

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

Ancora una volta, funziona perché abbiamo implementato due protocolli: Enumerable (per il Zoo) e String.Chars (per il Animale).

Conclusione

In questo articolo, abbiamo discusso di come il polimorfismo è implementato in elisir usando i protocolli. Hai imparato come definire e implementare i protocolli, oltre a utilizzare i protocolli integrati: Enumerable, Ispezionare, e String.Chars.

Come esercizio, puoi provare a potenziare il nostro Zoo modulo con il protocollo Collectable in modo che la funzione Enum.into / 2 possa essere utilizzata correttamente. Questo protocollo richiede l'implementazione di una sola funzione: in / 2, che raccoglie i valori e restituisce il risultato (si noti che deve anche supportare :fatto, : battuta d'arresto e : cont verbi; lo stato non dovrebbe essere segnalato). Condividi la tua soluzione nei commenti!

Spero ti sia piaciuto leggere questo articolo. Se hai qualche domanda, non esitare a contattarmi. Grazie per la pazienza e a presto!