Polymorfisme met protocollen in elixer

Polymorphism is een belangrijk concept in programmering, en de beginnende programmeurs leren gewoonlijk over het tijdens de eerste maanden van het bestuderen. Polymorfisme betekent in feite dat u een vergelijkbare bewerking kunt toepassen op entiteiten van verschillende typen. De telling / 1-functie kan bijvoorbeeld zowel op een bereik als op een lijst worden toegepast:

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

Hoe is dat mogelijk? In Elixir wordt polymorfisme bereikt door gebruik te maken van een interessante functie, een protocol genaamd, dat werkt als een contract. Voor elk gegevenstype dat u wilt ondersteunen, moet dit protocol worden geïmplementeerd.

Al met al is deze aanpak niet revolutionair, omdat deze in andere talen voorkomt (bijvoorbeeld Ruby). Toch zijn protocollen echt handig, dus in dit artikel zullen we bespreken hoe we ze kunnen definiëren, implementeren en ermee werken terwijl we enkele voorbeelden verkennen. Laten we beginnen!

Korte introductie tot protocollen

Dus, zoals hierboven al vermeld, een protocol heeft een generieke code en vertrouwt op het specifieke gegevenstype om de logica te implementeren. Dit is redelijk, omdat verschillende gegevenstypes verschillende implementaties kunnen vereisen. Een gegevenstype kan dan verzending op een protocol zonder zich zorgen te hoeven maken over de interne onderdelen.

Elixir heeft een heleboel ingebouwde protocollen, waaronder enumerable, Collectable, Inspecteren, List.Chars, en String.Chars. Sommigen van hen zullen later in dit artikel worden besproken. U kunt een van deze protocollen implementeren in uw aangepaste module en een aantal functies gratis krijgen. Na het implementeren van Enumerable krijgt u bijvoorbeeld toegang tot alle functies die zijn gedefinieerd in de Enum-module, wat best cool is.

Als je afkomstig bent uit de wondere Ruby-wereld vol objecten, klassen, feeën en draken, zul je een zeer vergelijkbaar concept van mixins. Als u bijvoorbeeld uw objecten ooit vergelijkbaar wilt maken, mengt u eenvoudig een module met de overeenkomstige naam in de klas. Implementeer dan gewoon een ruimteschip <=> methode en alle instanties van de klasse krijgen alle methoden zoals > en < gratis. Dit mechanisme lijkt enigszins op protocollen in Elixir. Zelfs als je dit concept nog nooit hebt ontmoet, geloof me, het is niet zo ingewikkeld. 

Oké, dus de eerste dingen eerst: het protocol moet worden gedefinieerd, dus laten we kijken hoe het in de volgende sectie kan worden gedaan.

Een protocol definiëren

Het definiëren van een protocol vereist geen zwarte magie, sterker nog, het lijkt sterk op het definiëren van modules. Gebruik defprotocol / 2 om het te doen:

defprotocol MyProtocol do end

Binnen de definitie van het protocol plaats je functies, net als bij modules. Het enige verschil is dat deze functies geen lichaam hebben. Dit betekent dat het protocol alleen een interface definieert, een blauwdruk die moet worden geïmplementeerd door alle gegevenstypen die op dit protocol willen verzenden:

Defprotocol MyProtocol do def my_func (arg) end

In dit voorbeeld moet een programmeur het mijn_func / 1 functie om succesvol te gebruiken MyProtocol.

Als het protocol niet wordt geïmplementeerd, zal er een fout worden gemaakt. Laten we teruggaan naar het voorbeeld met de tellen / 1 functie gedefinieerd in de Enum module. Als u de volgende code uitvoert, krijgt u een foutmelding:

Enum.count 1 # ** (Protocol.UndefinedError) protocol Enumerable niet geïmplementeerd voor 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

Het betekent dat de Geheel getal implementeert het enumerable protocol (wat een verrassing) en daarom kunnen we geen gehele getallen tellen. Maar het protocol eigenlijk kan geïmplementeerd, en dit is eenvoudig te bereiken.  

Een protocol implementeren

Protocollen worden geïmplementeerd met behulp van de defimpl / 3-macro. U geeft aan welk protocol moet worden geïmplementeerd en voor welk type:

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

Nu kunt u uw gehele getallen aftelbaar maken door de enumerable protocol:

defimpl Opsommingstabel, voor: Integer do def count (_arg) do : ok, 1 # integers bevatten altijd één element end end Enum.count (100) |> IO.puts # => 1

We zullen het bespreken enumerable protocol in meer detail later in het artikel en ook de andere functie implementeren.

Wat betreft het type (doorgegeven aan de voor), kunt u elk ingebouwd type, uw eigen alias of een lijst met aliassen opgeven:

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

 Bovendien zou je kunnen zeggen Ieder:

defimpl MyProtocol, voor: Elke def my_func (_) do IO.puts "Niet geïmplementeerd!" einde

Dit zal fungeren als een fallback-implementatie en er zal geen fout worden gemaakt als het protocol voor een bepaald type niet is geïmplementeerd. Om dit te laten werken, stelt u de @fallback_to_any attribuut aan waar in uw protocol (anders wordt de fout nog steeds verholpen):

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

U kunt nu het protocol gebruiken voor elk ondersteund type:

MyProtocol.my_func (5) # print gewoon 5 MyProtocol.my_func ("test") # prints "Niet geïmplementeerd!"

Een opmerking over structuren

De implementatie voor een protocol kan in een module worden genest. Als deze module een struct definieert, hoeft u niet eens op te geven voor bij het bellen defimpl:

defmodule Product defstructie van titel: "", prijs: 0 defimpl MyProtocol do def my_func (% Product title: title, price: price) do IO.puts "Title # title, price # price" end end end

In dit voorbeeld definiëren we een nieuwe struct genaamd Artikel en implementeer ons demo-protocol. Binnenin past u eenvoudig de titel en de prijs aan elkaar en geeft u een tekenreeks af.

Vergeet echter niet dat een implementatie genest moet worden in een module - dit betekent dat u elke module gemakkelijk kunt uitbreiden zonder de broncode te gebruiken.

Voorbeeld: String.Chars-protocol

Oké, genoeg met abstracte theorie: laten we eens een paar voorbeelden bekijken. Ik ben er zeker van dat je de IO.puts / 2-functie vrij uitgebreid hebt gebruikt om foutopsporingsinformatie naar de console te sturen tijdens het spelen met Elixir. Natuurlijk kunnen we verschillende ingebouwde typen eenvoudig uitvoeren:

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

Maar wat gebeurt er als we onze output proberen uit te voeren? Artikel struct gemaakt in de vorige sectie? Ik zal de bijbehorende code in de Hoofd module omdat anders een foutmelding wordt gegeven dat de struct niet is gedefinieerd of binnen hetzelfde bereik is benaderd:

defmodule Product defstructie van titel: "", prijs: 0 einde defmodule Main do def run do% Product title: "Test", prijs: 5 |> IO.puts end end Main.run

Na het uitvoeren van deze code, krijgt u een foutmelding:

 (Protocol.UndefinedError) -protocol String.Chars niet geïmplementeerd voor% Product prijs: 5, titel: "Test"

Aha! Het betekent dat de puts functie is afhankelijk van het ingebouwde String.Chars-protocol. Zolang het niet is geïmplementeerd voor onze Artikel, de fout wordt opgeworpen.

String.Chars is verantwoordelijk voor het converteren van verschillende structuren naar binaire bestanden en de enige functie die u moet implementeren is to_string / 1, zoals vermeld in de documentatie. Waarom implementeren we het nu niet??

defmodule Product defstructie van titel: "", prijs: 0 defimpl String.Chars do def to_string (% Product title: title, price: price) do "# title, $ # price" end end end

Als deze code aanwezig is, geeft het programma de volgende tekenreeks weer:

Test, $ 5

Wat betekent dat alles prima werkt!

Voorbeeld: Inspecteer Protocol

Een andere veel voorkomende functie is IO.inspect / 2 om informatie over een constructie te krijgen. Er is ook een inspect / 2-functie gedefinieerd in de pit module-it voert inspectie uit volgens het ingebouwde protocol van Inspect.

Onze Artikel struct kan meteen worden geïnspecteerd, en je krijgt er wat korte informatie over:

% Product title: "Test", prijs: 5 |> IO.inspect # of:% Product title: "Test", prijs: 5 |> inspect |> IO.puts

Het zal terugkeren % Product prijs: 5, titel: "Test". Maar nogmaals, we kunnen de. Gemakkelijk implementeren Inspecteren protocol dat alleen de inspect / 2-functie vereist die moet worden gecodeerd:

defmodule Product defstructie van titel: "", prijs: 0 defimpl Inspecteer do def inspect (% Product title: title, price: price, _) do "Dat is een Product struct. Het heeft een titel van # title en een prijs van # prijs. Yay! " einde einde 

Het tweede argument dat aan deze functie wordt doorgegeven, is de lijst met opties, maar we zijn niet geïnteresseerd in deze opties.

Voorbeeld: Enumerable Protocol

Laten we nu een iets complexer voorbeeld bekijken terwijl we praten over het Enumerable-protocol. Dit protocol wordt gebruikt door de Enum-module, die ons zulke handige functies biedt als elke / 2 en count / 1 (zonder dat, zou je het moeten doen met gewone oude recursie).

Enumerable definieert drie functies die u moet invullen om het protocol te implementeren:

  • count / 1 geeft de grootte van de enumerabele.
  • lid? / 2 controleert of de opsomcode een element bevat.
  • reduc / 3 past een functie toe op elk element van de opsom.

Met al die functies op zijn plaats, krijgt u toegang tot alle goodies die door de Enum module, wat echt een goede deal is.

Laten we als voorbeeld een nieuwe struct maken genaamd Dierentuin. Het krijgt een titel en een lijst met dieren:

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

Elk dier zal ook worden vertegenwoordigd door een struct:

defmodule Dieren doen soorten ontwijken: "", naam: "", leeftijd: 0 einde

Laten we nu een nieuwe dierentuin starten:

defmodule Main do def run do my_zoo =% Zoo title: "Demo Zoo", dieren: [% Animal species: "tiger", naam: "Tigga", leeftijd: 5,% Animal species: "horse", name: "Amazing", age: 3,% Animal species: "deer", name: "Bambi", age: 2] end-end Main.run

Dus we hebben een "Demo Zoo" met drie dieren: een tijger, een paard en een hert. Wat ik nu zou willen doen, is ondersteuning toevoegen voor de functie count / 1, die als volgt wordt gebruikt:

Enum.count (my_zoo) |> IO.inspect

Laten we deze functionaliteit nu implementeren!

De tellingfunctie implementeren

Wat bedoelen we met "tel mijn dierentuin"? Het klinkt een beetje vreemd, maar waarschijnlijk betekent het tellen van alle dieren die daar leven, dus de implementatie van de onderliggende functie zal vrij eenvoudig zijn:

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

Alles wat we hier doen is vertrouwen op de telling / 1-functie terwijl we een lijst met dieren doorgeven (omdat deze functie lijsten uit de doos ondersteunt). Een heel belangrijk ding om te vermelden is dat het tellen / 1 functie moet het resultaat teruggeven in de vorm van een tuple : ok, resultaat zoals voorgeschreven door de documenten. Als u alleen een cijfer retourneert, is er een fout opgetreden  ** (CaseClauseError) geen aanpassing van hoofdletters / kleine letters zal worden verhoogd.

Dat is het eigenlijk wel. Je kunt nu zeggen Enum.count (my_zoo) binnen in de Main.run, en het zou moeten terugkeren 3 als gevolg. Goed gedaan!

Uitvoerend lid? Functie

De volgende functie die het protocol definieert is de lid? / 2. Het zou een tuple moeten teruggeven : ok, boolean als een resultaat dat zegt of een opsombare (doorgegeven als het eerste argument) een element bevat (het tweede argument).

Ik wil dat deze nieuwe functie zegt of een bepaald dier in de dierentuin leeft of niet. Daarom is de implementatie ook vrij eenvoudig:

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

Merk nogmaals op dat de functie twee argumenten accepteert: een opsommingsteken en een element. Binnen vertrouwen we eenvoudig op de lid? / 2 functie om te zoeken naar een dier in de lijst met alle dieren.

Dus nu lopen we:

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

En dit zou moeten terugkeren waar zoals we inderdaad zo'n dier in de lijst hebben!

De reductiefunctie implementeren

Dingen worden een beetje ingewikkelder met de vermindering / 3 functie. Het accepteert de volgende argumenten:

  • een opsommingsteken om de functie toe te passen op
  • een accumulator om het resultaat op te slaan
  • de feitelijke verloopfunctie om toe te passen

Wat interessant is, is dat de accumulator eigenlijk een tupel bevat met twee waarden: a werkwoord en een waarde: werkwoord, waarde. Het werkwoord is een atoom en kan een van de volgende drie waarden hebben:

  • : cont (doorgaan met)
  • : halt (beëindigen)
  • : op te schorten (tijdelijk opschorten)

De resulterende waarde geretourneerd door de vermindering / 3 functie is ook een tuple die de status en een resultaat bevat. De staat is ook een atoom en kan de volgende waarden hebben: 

  • :gedaan (verwerking is voltooid, dat is het eindresultaat)
  • : gestopt (verwerking werd gestopt omdat de accu de : halt werkwoord)
  • :geschorst (verwerking was opgeschort)

Als de verwerking is opgeschort, moeten we een functie retourneren die de huidige status van de verwerking vertegenwoordigt.

Al deze eisen worden mooi gedemonstreerd door de implementatie van de vermindering / 3 functie voor de lijsten (overgenomen uit de documenten):

def reduce (_, : stop, acc, _fun), do: : stopgezet, acc def reduceer (list, : suspend, acc, fun), do: : suspended, acc, & reduce (list, & 1, fun) def reduce ([], : cont, acc, _fun), do: : done, acc def reduce ([h | t], : cont, acc, fun), do: verminderen (t, leuk. (h, acc), plezier)

We kunnen deze code als een voorbeeld gebruiken en onze eigen implementatie coderen voor de Dierentuin struct:

defmodule Zoo do defstruct title: "", animals: [] defimpl Enumerable do def reduce (_, : stop, acc, _fun), do: : stop, acc def reduce (% Zoo animals: animals, : suspend, acc, fun) do : suspended, acc, & 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) do reduce (% Zoo animals: tail, fun. ( hoofd, acc), plezier) einde end end

In de laatste functieclausule nemen we de kop van de lijst met alle dieren, passen we de functie toe en voeren we uit verminderen tegen de staart. Als er geen dieren meer zijn (de derde zin), geven we een tuple terug met de staat van :gedaan en het eindresultaat. De eerste clausule retourneert een resultaat als de verwerking is gestopt. De tweede clausule retourneert een functie als de : op te schorten werkwoord werd doorgegeven.

Nu kunnen we bijvoorbeeld de totale leeftijd van al onze dieren eenvoudig berekenen:

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

Kortom, nu hebben we toegang tot alle functies die door de Enum module. Laten we proberen join / 2 te gebruiken:

Enum.join (my_zoo) |> IO.inspect

U krijgt echter een foutmelding dat het String.Chars protocol is niet geïmplementeerd voor de Dier struct. Dit gebeurt omdat toetreden probeert elk element naar een tekenreeks te converteren, maar kan het niet voor de tekenreeks doen Dier. Laten we daarom ook het String.Chars protocol nu:

Defmodule Dieren doen soorten ontwijken: "", naam: "", leeftijd: 0 defimpl String.Chars do def to_string (% Dier soort: soort, naam: naam, leeftijd: leeftijd) do "# name (#  soort), leeftijd # age "einde einde

Nu zou alles prima moeten werken. U kunt ook proberen om elke / 2 uit te voeren en afzonderlijke dieren weer te geven:

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

Nogmaals, dit werkt omdat we twee protocollen hebben geïmplementeerd: enumerable (voor de Dierentuin) en String.Chars (voor de Dier).

Conclusie

In dit artikel hebben we besproken hoe polymorfisme wordt geïmplementeerd in Elixir met behulp van protocollen. U hebt geleerd protocollen te definiëren en te implementeren en ingebouwde protocollen te gebruiken: enumerable, Inspecteren, en String.Chars.

Als oefening kun je proberen onze kracht te vergroten Dierentuin module met het Collectable-protocol, zodat de Enum.into / 2-functie op de juiste manier kan worden gebruikt. Dit protocol vereist de implementatie van slechts één functie: in / 2, die waarden verzamelt en het resultaat retourneert (merk op dat het ook de :gedaan, : halt en : cont werkwoorden; de staat moet niet worden gerapporteerd). Deel uw oplossing in de comments!

Ik hoop dat je het leuk vond om dit artikel te lezen. Als u nog vragen heeft, aarzel dan niet om contact met mij op te nemen. Bedankt voor het geduld en tot snel!