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!
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.
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.
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!"
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.
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!
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.
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:
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!
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!
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!
Dingen worden een beetje ingewikkelder met de vermindering / 3
functie. Het accepteert de volgende argumenten:
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
).
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!