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!
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.
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.
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!"
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.
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!
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.
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:
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à!
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!
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!
Le cose diventano un po 'più complesse con il ridurre / 3
funzione. Accetta i seguenti argomenti:
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
).
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!