Polimorfismul este un concept important în programare, iar programatorii începători învață de obicei despre asta în primele luni de studiu. Polimorfismul înseamnă în principiu că puteți aplica o operație similară entităților de diferite tipuri. De exemplu, funcția count / 1 poate fi aplicată atât la un interval, cât și la o listă:
Număr de cont (1 ... 3) Număr de cont ([1,2,3])
Cum este posibil? În Elixir, polimorfismul este obținut prin folosirea unei trăsături interesante numite protocol, care acționează ca a contracta. Pentru fiecare tip de date pe care doriți să îl sprijiniți, acest protocol trebuie implementat.
În ansamblu, această abordare nu este revoluționară, deoarece se găsește în alte limbi (de exemplu, Ruby, de exemplu). Totuși, protocoalele sunt foarte convenabile, așa că în acest articol vom discuta cum să definim, să implementăm și să lucrăm cu ele, explorând câteva exemple. Să începem!
Deci, așa cum am menționat deja mai sus, un protocol are un cod generic și se bazează pe tipul de date specific pentru implementarea logicii. Acest lucru este rezonabil, deoarece diferite tipuri de date pot necesita implementări diferite. Un tip de date poate apoi expediere pe un protocol fără să-ți faci griji în privința internelor sale.
Elixir are o grămadă de protocoale încorporate, inclusiv enumerabile
, perceptibil
, Inspecta
, List.Chars
, și String.Chars
. Unele dintre ele vor fi discutate mai târziu în acest articol. Puteți implementa oricare dintre aceste protocoale în modulul dvs. personalizat și puteți obține gratuit o grămadă de funcții. De exemplu, dacă ați implementat Enumerable, veți avea acces la toate funcțiile definite în modulul Enum, ceea ce este destul de răcoros.
Dacă ați venit de la minunata lume Ruby plină de obiecte, clase, zane și dragoni, veți fi întâlnit un concept foarte asemănător de mixins. De exemplu, dacă vreți să faceți obiectele comparabile, amestecați pur și simplu un modul cu numele corespunzător în clasă. Apoi, implementați o navă spațială <=>
metoda și toate instanțele clasei vor primi toate metodele cum ar fi >
și <
gratuit. Acest mecanism este oarecum similar cu protocoalele din Elixir. Chiar dacă nu ați întâlnit niciodată acest concept, credeți-mă, nu este așa de complex.
Bine, deci primele lucruri sunt mai întâi: protocolul trebuie definit, așa că să vedem cum se poate face în secțiunea următoare.
Definirea unui protocol nu implică nici o magie neagră - de fapt, este foarte asemănătoare cu definirea modulelor. Utilizați defprotocol / 2 pentru a face acest lucru:
defrotocolul MyProtocol se termină
În interiorul definiției protocolului localizați funcții, la fel ca în cazul modulelor. Singura diferență este că aceste funcții nu au corp. Aceasta înseamnă că protocolul definește doar o interfață, un model care ar trebui implementat de toate tipurile de date care doresc să expedieze pe acest protocol:
defrotocolul MyProtocol def def. my_func (arg) end
În acest exemplu, un programator trebuie să implementeze my_func / 1
pentru a utiliza cu succes MyProtocol
.
Dacă protocolul nu este implementat, va apărea o eroare. Să revenim la exemplul cu Numar / 1
funcția definită în interiorul enum
modul. Rularea următorului cod se va încheia cu o eroare:
Protocolul Enum.count 1 # ** (Protocol.UndefinedError) Enumerable nu este implementat pentru 1 # (elixir) lib / enum.ex: 1: Enumerable.impl_for! / 1 # (elixir) lib / enum.ex: 146: Enumerable. număr / 1 # (elixir) lib / enum.ex: 467: Enum.count / 1
Aceasta înseamnă că Întreg
nu implementează enumerabile
protocol (ceea ce este o surpriză) și, prin urmare, nu putem număra numere întregi. Dar protocolul de fapt poate sa să fie implementată, iar acest lucru este ușor de realizat.
Protocoalele sunt implementate folosind macrocomanda defimpl / 3. Specificați ce protocol să implementați și pentru ce tip:
defimpl MyProtocol, pentru: Integer def my_func (arg) face IO.puts (arg) end end
Acum puteți să vă numărați numerele întregi prin implementarea parțială enumerabile
protocol:
defimpl Enumerable, pentru: Integer do def count (_arg) do : ok, 1 # numere întregi conțin întotdeauna un sfârșit de element Enum.count (100) |> IO.puts # => 1
Vom discuta despre enumerabile
protocol mai detaliat mai târziu în articol și implementarea celeilalte funcții.
În ceea ce privește tipul (transmis către pentru
), puteți specifica orice tip de built-in, alias propriu sau o listă de pseudonime:
defimpl MyProtocol, pentru că: [Integer, List] se termină
În afară de asta, puteți spune Orice
:
defimpl MyProtocol, pentru: Orice def my_func (_) face IO.puts "Nu este implementat!" sfârșitul final
Acest lucru va acționa ca o implementare alternativă și o eroare nu va fi ridicată dacă protocolul nu este implementat pentru un anumit tip. Pentru ca acest lucru să funcționeze, setați @fallback_to_any
atribuit lui Adevărat
în interiorul protocolului dvs. (în caz contrar, eroarea va fi încă ridicată):
defrotocolul MyProtocol nu @fallback_to_any true def my_func (arg) end
Acum puteți utiliza protocolul pentru orice tip acceptat:
MyProtocol.my_func (5) # imprimă pur și simplu 5 MyProtocol.my_func ("test") # imprimă "Nu este implementat!"
Implementarea unui protocol poate fi imbricată în interiorul unui modul. Dacă acest modul definește un struct, nici măcar nu trebuie să specificați pentru
când sunați defimpl
:
defmodule Produsul nu defolctează titlul: "", preț: 0 defimpl MyProtocol do def my_func (% Produs title: title, price: price) do IO.puts "Title # price
În acest exemplu, definim un nou struct numit Produs
și să pună în aplicare protocolul demo. În interior, simpla potrivire a titlului și a prețului și apoi ieșirea unui șir.
Rețineți însă că o implementare trebuie să fie imbricată în interiorul unui modul - înseamnă că puteți extinde cu ușurință orice modul fără a avea acces la codul sursă.
Bine, suficient cu teoria abstractă: să aruncăm o privire la câteva exemple. Sunt sigur că ați folosit funcția IO.puts / 2 destul de extensiv pentru a trimite informații de depanare la consola când jucați cu Elixir. Sigur, putem produce diferite tipuri încorporate:
IO.puts 5 IO.puts "test" IO.puts: my_atom
Dar ce se întâmplă dacă încercăm să ne scoatem Produs
struct creat în secțiunea anterioară? Voi plasa codul corespunzător în interiorul lui Principal
deoarece altfel veți primi o eroare spunând că structul nu este definit sau accesat în același domeniu:
defmodule Produsul defstructeaza titlul: "", pret: 0 sfarsit defmodule Principal def execute do Produs title: "Test", pret: 5 |> IO.puts end end Main.run
După ce executați acest cod, veți primi o eroare:
(Protocol.UndefinedError) Protocolul String.Chars nu este implementat pentru% Product price: 5, title: "Test"
Aha! Aceasta înseamnă că puts
funcția se bazează pe protocolul încorporat String.Chars. Atâta timp cât nu este implementat pentru noi Produs
, eroarea este ridicată.
String.Chars
este responsabil pentru conversia diferitelor structuri în binare, iar singura funcție pe care trebuie să o implementați este to_string / 1, după cum se precizează în documentație. De ce nu o implementăm acum?
defmodule Produsul nu defrijează titlul: "", prețul: 0 defimpl String.Chars do def to_string (% Produs title: title, price: price)
Având acest cod în loc, programul va emite următorul șir:
Testează, 5 $
Ceea ce înseamnă că totul funcționează foarte bine!
O altă funcție foarte comună este IO.inspect / 2 pentru a obține informații despre un construct. Există, de asemenea, o funcție de inspecție / 2 definită în interiorul Nucleu
modul - acesta efectuează inspecția conform protocolului Inspect încorporat.
Al nostru Produs
struct pot fi inspectate imediat și vei primi câteva informații despre el:
Produs title: "Test", pret: 5 |> IO.inspect # sau:% Produs title: "Test", pret: 5 |
Se va întoarce Produsul preț: 5, titlu: "Test"
. Dar, încă o dată, putem implementa cu ușurință Inspecta
protocol care necesită doar codificarea funcției inspect / 2:
defmodule Produsul nu defreeaza titlul: "", pret: 0 defimpl Inspect do def inspect (% Product title: title, price: price, do) prețul de # price. Yay! " sfârșitul capătului final
Cel de-al doilea argument transmis acestei funcții este lista de opțiuni, dar nu ne interesează.
Acum, să vedem un exemplu ușor mai complex în timp ce vorbim despre protocolul Enumerable. Acest protocol este folosit de modulul Enum, care ne prezintă funcții atât de convenabile ca fiecare / 2 și numărătoarea / 1 (fără acesta, ar trebui să rămânem cu o recursivitate veche).
Enumerable definește trei funcții pe care trebuie să le îndepliniți pentru implementarea protocolului:
Având toate aceste funcții în loc, veți avea acces la toate bunatațile oferite de enum
modul, care este o afacere foarte bună.
Ca exemplu, să creăm un nou struct numit Grădină zoologică
. Va avea un titlu și o listă de animale:
Defmodule Zoo face defstruct titlu: "", animale: [] sfârșitul
Fiecare animal va fi de asemenea reprezentat de un struct:
Defmodule Animal do defstruct specie: "", nume: "", varsta: 0 sfarsit
Acum, permiteți instanțierii unei noi grădini zoologice:
Defmodule Principal face def alergare my_zoo =% Zoo title: "Demo Zoo", animale: [% animal specie: "tiger", nume: "Tigga", varsta: nume: "Amazing", vârsta: 3,% Animal specie: "cerb", nume: "Bambi", vârstă: 2]
Deci, avem un "Demo Zoo" cu trei animale: un tigru, un cal și un cerb. Ceea ce aș vrea să fac acum este să adaug suport pentru funcția count / 1, care va fi folosită astfel:
Contul meu (my_zoo) |> IO.inspect
Să implementăm această funcție acum!
Ce inseamna atunci cand spuneti "numara gradina mea zoologica"? Suna cam ciudat, dar probabil înseamnă a număra toate animalele care trăiesc acolo, astfel că implementarea funcției de bază va fi destul de simplă:
Defmodule Zoo face defragmentarea titlului: "", animale: [] defimpl Numar de numarare def (% Zoo animals: animals) ok, Enum.count (animals)
Tot ceea ce facem aici se bazează pe funcția count / 1 în timp ce trece o listă de animale către ea (deoarece această funcție susține liste din cutie). Un lucru foarte important de menționat este faptul că Numar / 1
funcția trebuie să returneze rezultatul său sub forma unei tuple : ok, rezultat
așa cum este dictat de docs. Dacă returnați doar un număr, o eroare ** (CaseClauseError) nici o potrivire a clauzelor de caz
va fi ridicat.
Cam asta e tot. Acum puteți spune Enum.count (my_zoo)
în interiorul Main.run
, și ar trebui să se întoarcă 3
ca rezultat. Bună treabă!
Următoarea funcție definită de protocol este membru? / 2
. Ar trebui să returneze o tuplă : ok, boolean
ca rezultat, care spune dacă un enumerable (trecut ca primul argument) conține un element (al doilea argument).
Vreau ca această nouă funcție să spună dacă un anumit animal trăiește în grădina zoologică sau nu. Prin urmare, implementarea este destul de simplă:
Defmodule Zoo face defragmentare titlu: "", animale: [] defimpl Enumerate nu # ... def membru (% Zoo title: _, animals: animals, animal) end end end
Încă o dată, rețineți că funcția acceptă două argumente: un element enumerabil și un element. În interior, ne bazăm pur și simplu pe membru? / 2
funcția de a căuta un animal pe lista tuturor animalelor.
Deci, acum fugim:
Membru din grupul meu (my_zoo,% animal specie: "tigru", nume: "Tigga", varsta: 5)> IO.inspect
Și asta ar trebui să se întoarcă Adevărat
deoarece într-adevăr avem un astfel de animal pe listă!
Lucrurile devin un pic mai complexe cu reducerea / 3
funcţie. Acceptă următoarele argumente:
Interesant este faptul că acumulatorul conține de fapt o tuplă cu două valori: a verb și o valoare: verb, value
. Verbul este un atom și poate avea una din următoarele trei valori:
: cont
(continua):oprire
(Termina):suspenda
(suspendați temporar)Valoarea rezultată returnată de reducerea / 3
funcția este de asemenea o tuplă care conține starea și un rezultat. Statul este, de asemenea, un atom și poate avea următoarele valori:
:Terminat
(procesarea se face, acesta este rezultatul final): oprit
(procesarea a fost oprită deoarece acumulatorul conținea :oprire
verb):suspendat
(procesarea a fost suspendată)Dacă procesul de procesare a fost suspendat, ar trebui să returnați o funcție reprezentând starea curentă a procesării.
Toate aceste cerințe sunt demonstrate frumos prin punerea în aplicare a directivei reducerea / 3
pentru listele (preluate din documente):
def: : suspend, acc, & reduce (list, : suspend, acc, fun) & 1, distracție) def reduce ([, cont, acc], _fun), face: : done, acc def reduce ([h | t] reduceți (t, distracție (h, acc), distracție)
Putem folosi acest cod ca exemplu și să codificăm propria noastră implementare pentru Grădină zoologică
struct:
Defmodule Zoo face defragmentarea titlului: "", animale: [] defimpl enumerable do def reduce (_, : halt, acc, _fun), do: : stop, : suspend, acc, distracție) suspend, acc, & reduce (% Zoo animals: animals, & 1, fun) , _fun), face: : done, acc def reduce (% Zoo animals: [head | tail], : cont, acc, distractiv). cap, acc), distracție) sfârșitul capătului final
În ultima clauză de funcții, luăm capul listei care conține toate animalele, aplicăm funcția și apoi efectuăm reduce
împotriva coastei. Când nu mai rămân animale (a treia clauză), vom întoarce o tuplă cu starea lui :Terminat
și rezultatul final. Prima clauză returnează un rezultat dacă procesarea a fost oprită. A doua clauză returnează o funcție dacă :suspenda
a fost trecută verbul.
Acum, de exemplu, putem calcula cu ușurință vârsta totală a tuturor animalelor noastre:
Enum.reduce (my_zoo, 0, fn (animal, total_age) -> animal.age + capăt total_age) |> IO.puts
Practic, acum avem acces la toate funcțiile oferite de enum
modul. Să încercăm să utilizăm join / 2:
Enum.join (my_zoo) |> IO.inspect
Cu toate acestea, veți primi o eroare spunând că String.Chars
protocol nu este implementat pentru Animal
struct. Acest lucru se întâmplă pentru că a adera
încearcă să convertească fiecare element la un șir, dar nu poate face acest lucru pentru Animal
. Prin urmare, să implementăm și String.Chars
protocol acum:
defmodule Animal do defstrat specii: "", nume: "", vârstă: 0 defimpl String.Chars do def to_string (% Animal specie: species, name: name, age: age specie), în vârstă de # age "end end end
Acum totul ar trebui să funcționeze foarte bine. De asemenea, puteți încerca să rulați fiecare / 2 și să afișați animale individuale:
Enum.each (my_zoo, & (IO.puts (& 1)))
Încă o dată, acest lucru funcționează deoarece am implementat două protocoale: enumerabile
(pentru Grădină zoologică
) și String.Chars
(pentru Animal
).
În acest articol, am discutat despre modul în care polimorfismul este implementat în Elixir folosind protocoale. Ați învățat cum să definiți și să implementați protocoale, precum și să utilizați protocoale integrate: enumerabile
, Inspecta
, și String.Chars
.
Ca exercițiu, puteți încerca să ne împuterniciți Grădină zoologică
modul cu protocolul Collectable, astfel încât funcția Enum.into / 2 să poată fi utilizată în mod corespunzător. Acest protocol necesită implementarea unei singure funcții: în / 2, care colectează valori și returnează rezultatul (rețineți că trebuie, de asemenea, să susțină :Terminat
, :oprire
și : cont
verbe; statul nu trebuie raportat). Trimiteți-vă soluția în comentarii!
Sper că v-ați bucurat să citiți acest articol. Dacă aveți întrebări, nu ezitați să mă contactați. Vă mulțumim pentru răbdare și vă vom vedea în curând!