Polimorfismul cu protocoale în Elixir

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!

Scurtă prezentare a protocoalelor

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

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.  

Implementarea unui protocol

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!"

O notă despre Structs

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ă.

Exemplu: Protocolul String.Chars

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!

Exemplu: Inspectați protocolul

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ă.

Exemplu: Protocolul enumerabil

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:

  • count / 1 returnează mărimea enumerabilului.
  • membru? / 2 verifică dacă enumerable conține un element.
  • reduce / 3 aplică o funcție pentru fiecare element al enumerabilului.

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!

Implementarea funcției Count

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ă!

Membru implementator? Funcţie

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ă!

Implementarea funcției Reduce

Lucrurile devin un pic mai complexe cu reducerea / 3 funcţie. Acceptă următoarele argumente:

  • un număr enumerabil pentru a aplica funcția
  • un acumulator pentru stocarea rezultatului
  • funcția actuală de reducere care se aplică

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).

Concluzie

Î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!

Cod