Polymorfisme er et viktig konsept i programmering, og nybegynnere programmerer vanligvis lære om det i de første månedene av studiet. Polymorfisme betyr i utgangspunktet at du kan bruke en lignende operasjon til enheter av forskjellige typer. For eksempel kan count / 1-funksjonen brukes både til et område og til en liste:
Enum.count (1 ... 3) Enum.count ([1,2,3])
Hvordan er det mulig? I Elixir oppnås polymorfisme ved å bruke en interessant funksjon som kalles en protokoll, som virker som a kontrakt. For hver datatype du ønsker å støtte, må denne protokollen implementeres.
Alt i alt er denne tilnærmingen ikke revolusjonerende, som den finnes på andre språk (for eksempel Ruby, for eksempel). Likevel er protokoller veldig praktiske, så i denne artikkelen diskuterer vi hvordan du definerer, implementerer og jobber med dem mens du utforsker noen eksempler. La oss komme i gang!
Så, som allerede nevnt ovenfor, har en protokoll noen generisk kode og er avhengig av den spesifikke datatypen for å implementere logikken. Dette er rimelig, fordi ulike datatyper kan kreve forskjellige implementeringer. En datatype kan da utsendelse på en protokoll uten å bekymre seg for sine internals.
Elixir har en rekke innebygde protokoller, inkludert enumerable
, collectable
, Undersøke
, List.Chars
, og String.Chars
. Noen av dem vil bli diskutert senere i denne artikkelen. Du kan implementere noen av disse protokollene i din egendefinerte modul og få en masse funksjoner gratis. For eksempel, etter å ha implementert Enumerable, får du tilgang til alle funksjonene som er definert i Enum-modulen, noe som er ganske kult.
Hvis du er kommet fra den vidunderlige Ruby-verdenen full av objekter, klasser, feer og drager, har du møtt et meget lignende konsept av mixins. For eksempel, hvis du noen gang trenger å gjøre objektene dine sammenlignbare, må du bare blande en modul med det tilhørende navnet inn i klassen. Deretter bare implementere et romskip <=>
metode og alle forekomster av klassen vil få alle metoder som >
og <
gratis. Denne mekanismen er noe lik protokollene i Elixir. Selv om du aldri har møtt dette konseptet før, tro meg, det er ikke så komplisert.
Ok, så første ting først: protokollen må defineres, så la oss se hvordan det kan gjøres i neste avsnitt.
Definisjon av en protokoll involverer ikke noe svart magi-faktisk, det ligner veldig på å definere moduler. Bruk defprotocol / 2 for å gjøre det:
defprotocol MyProtocol slutter
I protokollens definisjon plasserer du funksjoner, akkurat som med moduler. Den eneste forskjellen er at disse funksjonene ikke har noen kropp. Det betyr at protokollen bare definerer et grensesnitt, en tegning som bør implementeres av alle datatyper som ønsker å sende på denne protokollen:
defprotocol MyProtocol gjør def my_func (arg) slutten
I dette eksemplet må en programmerer implementere my_func / 1
funksjon for å kunne utnytte MyProtocol
.
Hvis protokollen ikke er implementert, vil en feil bli hevet. La oss gå tilbake til eksemplet med teller / 1
funksjon definert inne i enum
modul. Kjører følgende kode vil ende opp med en feil:
Enum.count 1 # ** (Protocol.UndefinedError) protokoll Enumerable ikke implementert for 1 # (elixir) lib / enum.ex: 1: Enumerable.impl_for! / 1 # (elixir) lib / enum.ex: 146: Enumerable. telle / 1 # (elixir) lib / enum.ex: 467: Enum.count / 1
Det betyr at Integer
implementerer ikke enumerable
protokoll (hva en overraskelse), og derfor kan vi ikke telle heltall. Men protokollen faktisk kan implementeres, og dette er lett å oppnå.
Protokoller implementeres ved hjelp av defimpl / 3 makroen. Du angir hvilken protokoll som skal implementeres og for hvilken type:
defimpl MyProtocol, for: Integer def my_func (arg) gjør IO.puts (arg) ende ende
Nå kan du gjøre integerene dine telle ved å delvis implementere enumerable
protokoll:
defimpl Enumerable, for: Integer gjør def count (_arg) gjør : ok, 1 # heltall inneholder alltid ett element endeend Enum.count (100) | IO.puts # => 1
Vi vil diskutere enumerable
protokollen mer detaljert senere i artikkelen og implementere sin andre funksjon også.
Som for typen (sendt til til
), kan du angi hvilken som helst innebygd type, ditt eget alias eller en liste over aliaser:
defimpl MyProtocol, for: [Integer, List] do end
På toppen av det kan du si Noen
:
defimpl MyProtocol, for: Enhver def my_func (_) gjør IO.puts "Ikke implementert!" slutten
Dette vil fungere som en tilbakekallingsimplementering, og en feil vil ikke bli hevet dersom protokollen ikke er implementert for noen type. For at dette skal fungere, setter du inn @fallback_to_any
tilskrive ekte
inne i protokollen din (ellers vil feilen fortsatt bli hevet):
defprotocol MyProtocol gjør @ fallback_to_any true def my_func (arg) slutten
Du kan nå bruke protokollen for alle støttede typer:
MyProtocol.my_func (5) # skriver bare ut 5 MyProtocol.my_func ("test") # utskrifter "Ikke implementert!"
Implementeringen av en protokoll kan nestes inne i en modul. Hvis denne modulen definerer en struktur, trenger du ikke engang å spesifisere til
når du ringer defimpl
:
defmodule Produktet defraplerer tittelen: "", pris: 0 defimpl MyProtocol gjør def my_func (% Produkt tittel: tittel, pris: pris) gjør IO.puts "Tittel # title, pris # pris" slutten ende
I dette eksemplet definerer vi en ny struktur som heter Produkt
og implementere vår demo-protokoll. Innsiden, bare mønster-match tittelen og prisen og deretter utføre en streng.
Husk imidlertid at en implementering må være nestet inne i en modul - det betyr at du enkelt kan utvide en modul uten å få tilgang til kildekoden.
Ok nok med abstrakt teori: La oss ta en titt på noen eksempler. Jeg er sikker på at du har brukt IO.puts / 2-funksjonen ganske mye for å utføre feilsøkingsinformasjon til konsollen når du spiller med Elixir. Sikkert, vi kan enkelt utføre ulike innebygde typer:
IO.puts 5 IO.puts "test" IO.puts: my_atom
Men hva skjer hvis vi prøver å utføre vår Produkt
struct opprettet i forrige seksjon? Jeg vil plassere den tilsvarende koden inne i Hoved
modul fordi ellers får du en feil som sier at strukturen ikke er definert eller åpnet i samme omfang:
defmodule Produkt defray title: "", pris: 0 end defmodule Main gjør def run do% Produkt title: "Test", pris: 5 |> IO.puts endend Main.run
Etter å ha kjørt denne koden, får du en feil:
(Protocol.UndefinedError) protokoll String.Chars ikke implementert for% Produkt pris: 5, tittel: "Test"
Aha! Det betyr at puts
funksjonen er avhengig av den innebygde String.Chars-protokollen. Så lenge det ikke er implementert for vår Produkt
, feilen blir hevet.
String.Chars
er ansvarlig for å konvertere ulike strukturer til binærfiler, og den eneste funksjonen du trenger å implementere er to_string / 1, som angitt i dokumentasjonen. Hvorfor implementerer vi ikke det nå?
defmodule Produktet defekte tittelen: "", pris: 0 defimpl String.Chars gjør def to_string (% Produkt tittel: tittel, pris: pris) gjør "# title, $ # price"
Når denne koden er på plass, vil programmet sende følgende streng:
Test, $ 5
Det betyr at alt fungerer bra!
En annen svært vanlig funksjon er IO.inspect / 2 for å få informasjon om en konstruksjon. Det er også en inspeksjon / 2-funksjon definert inne i Kernel
modul-det utfører inspeksjon i henhold til Inspect-innebygd protokoll.
Våre Produkt
struct kan inspiseres med en gang, og du får litt kort informasjon om det:
% Produkt tittel: "Test", pris: 5 |> IO.inspect # eller:% Produkt title: "Test", pris: 5 |> inspisere |> IO.puts
Det kommer tilbake % Produkt pris: 5, tittel: "Test"
. Men igjen kan vi enkelt implementere Undersøke
protokoll som krever at kun inspeksjon / 2-funksjonen skal kodes:
defmodule Produktet defekte tittelen: "", pris: 0 defimpl Inspeksjon, inspeksjon ikke (% Produkt tittel: tittel, pris: pris, _) gjør "Det er en produktstruktur. Den har tittelen på # title prisen på # pris. Yay! " sluttendens ende
Det andre argumentet som går til denne funksjonen er listen over alternativer, men vi er ikke interessert i dem.
La oss nå se et litt mer komplekst eksempel mens du snakker om Enumerable-protokollen. Denne protokollen er ansatt av Enum-modulen, som presenterer oss med så praktiske funksjoner som hver / 2 og teller / 1 (uten det, må du holde fast med vanlig gammel rekursjon).
Enumerable definerer tre funksjoner som du må kutte ut for å implementere protokollen:
Å ha alle disse funksjonene på plass, får du tilgang til alle de godbiter som tilbys av enum
modul, som er en veldig god avtale.
Som et eksempel, la oss lage en ny struktur som heter dyrehage
. Det vil ha en tittel og en liste over dyr:
defmodule Zoo gjør defrukt tittel: "", dyr: [] ende
Hvert dyr vil også bli representert av en struktur:
defmodule Animal gjør defrukt arter: "", navn: "", alder: 0 ende
La oss nå instansere en ny dyrehage:
defodule Main gjør def run gjøre my_zoo =% Zoo title: "Demo Zoo", dyr: [% Animal species: "tiger", navn: "Tigga", alder: 5,% Animal species: "horse" navn: "Amazing", alder: 3,% Dyr art: "hjort", navn: "Bambi", alder: 2] slutten Main.run
Så vi har en "Demo Zoo" med tre dyr: en tiger, en hest og en hjort. Hva jeg vil gjerne gjøre nå, er å legge til støtte for telle / 1-funksjonen, som vil bli brukt som denne:
Enum.count (my_zoo) |> IO.inspect
La oss implementere denne funksjonaliteten nå!
Hva mener vi når du sier "count my zoo"? Det høres litt rart, men sannsynligvis betyr det å telle alle dyrene som bor der, så implementeringen av den underliggende funksjonen vil være ganske enkel:
defmodule Zoo gjør defekt tittel: "", dyr: [] defimpl Opptelling gjør ikke telle (% Zoo animals: animals) gjør : ok, Enum.count
Alt vi gjør her er avhengig av telle / 1-funksjonen mens du sender en liste over dyr til den (fordi denne funksjonen støtter lister ut av boksen). En veldig viktig ting å nevne er at teller / 1
funksjonen må returnere resultatet i form av en tuple : ok, resultat
som diktert av docs. Hvis du bare returnerer et nummer, en feil ** (CaseClauseError) ingen tilfelle klausul matching
vil bli hevet.
Det er ganske mye det. Nå kan du si Enum.count (my_zoo)
inne i Main.run
, og det skal komme tilbake 3
som et resultat. Godt jobbet!
Den neste funksjonen protokollen definerer, er medlem? / 2
. Det skal returnere en tuple : ok, boolean
som et resultat som sier om en tallrik (bestått som det første argumentet) inneholder et element (det andre argumentet).
Jeg vil at denne nye funksjonen skal si om et bestemt dyr bor i dyrehagen eller ikke. Derfor er implementeringen ganske enkel også:
defiment Dyr gjør defekt tittel: "", dyr: [] defimpl Opptatt gjør # ... def medlem? (% Zoo tittel: _, dyr: dyr, dyr) gjør : ok, Enum.member? slutten slutten
Igjen, merk at funksjonen aksepterer to argumenter: en tallbar og et element. Innsiden stole vi bare på medlem? / 2
Funksjon for å søke etter et dyr på listen over alle dyr.
Så nå løper vi:
Enum.member? (My_zoo,% Animal species: "tiger", navn: "Tigga", alder: 5) |> IO.inspect
Og dette skulle komme tilbake ekte
som vi faktisk har et slikt dyr i listen!
Ting blir litt mer komplekse med redusere / 3
funksjon. Den aksepterer følgende argumenter:
Det som er interessant er at akkumulatoren faktisk inneholder en tuple med to verdier: a verb og en verdi: verb, verdi
. Verbetet er et atom og kan ha en av følgende tre verdier:
: forts
(Fortsette): stans
(terminere):henge
(midlertidig suspendere)Den resulterende verdien returnert av redusere / 3
funksjon er også en tuple som inneholder staten og et resultat. Staten er også et atom og kan ha følgende verdier:
: gjort
(behandling er ferdig, det er sluttresultatet):stanset
(behandlingen ble stoppet fordi akkumulatoren inneholdt : stans
verb): suspendert
(behandlingen ble suspendert)Hvis behandlingen ble suspendert, bør vi returnere en funksjon som representerer den nåværende tilstanden for behandlingen.
Alle disse kravene er pent demonstrert av implementeringen av redusere / 3
funksjon for lister (hentet fra dokumentene):
def redusere (_, : halt, acc, _fun), gjør: : stoppet, acc def redusere (liste, : suspend, acc, morsomt) & 1, morsom) def redusere ([], : cont, acc, _fun), gjør: : done, acc def redusere ([h | t], : cont, acc, morsomt) redusere (t, morsomt. (h, acc), morsomt)
Vi kan bruke denne koden som et eksempel og kode vår egen implementering for dyrehage
struct:
defmodule Zoo gjør defekt tittel: "", dyr: [] defimpl Enumerable gjør def redusere (_, : halt, acc, _fun), gjør: : stoppet, acc def redusere (% Zoo animals: animals : suspend, acc, gøy) gjør : suspendert, acc, & redusere (% Zoo dyr: dyr, & 1, morsomt) slutt def redusere (% Zoo animals: [], : cont, acc , _fun), gjør: : done, acc def redusere (% Zoo dyr: [head | tail], : cont, acc, morsomt) gjør redusere (% Zoo animals: tail, fun. hode, acc), morsomt) endeendens ende
I den siste funksjonsklausulen tar vi lederen av listen som inneholder alle dyr, bruker funksjonen til den og utfører deretter redusere
mot halen. Når det ikke er flere dyr igjen (tredje klausulen), returnerer vi en tuple med tilstanden til : gjort
og sluttresultatet. Den første klausulen returnerer et resultat hvis behandlingen ble stoppet. Den andre klausulen returnerer en funksjon hvis :henge
verb ble bestått.
Nå kan vi for eksempel beregne total alder på alle dyrene våre lett:
Enum.reduce (my_zoo, 0, fn (animal, total_age) -> animal.age + total_age end) |> IO.puts
I utgangspunktet har vi nå tilgang til alle funksjonene som tilbys av enum
modul. La oss prøve å bruke join / 2:
Enum.join (my_zoo) |> IO.inspect
Du får imidlertid en feil som sier at String.Chars
protokollen er ikke implementert for Dyr
struct. Dette skjer fordi bli med
prøver å konvertere hvert element til en streng, men kan ikke gjøre det for Dyr
. Derfor, la oss også implementere String.Chars
protokoll nå:
defiment Dyr gjør defekt arter: "", navn: "", alder: 0 defimpl String.Chars gjør def to_string (% Animal art: art, navn: navn, alder: alder) gjør "# name arter), alderen # alder "slutten slutten
Nå skal alt fungere fint. Du kan også prøve å kjøre hver / 2 og vise individuelle dyr:
Enum.each (my_zoo, & (IO.puts (& 1)))
Igjen virker dette fordi vi har implementert to protokoller: enumerable
(for dyrehage
) og String.Chars
(for Dyr
).
I denne artikkelen har vi diskutert hvordan polymorfisme er implementert i Elixir ved hjelp av protokoller. Du har lært hvordan du definerer og implementerer protokoller, samt benytter innebygde protokoller: enumerable
, Undersøke
, og String.Chars
.
Som en øvelse kan du prøve å styrke vår dyrehage
modul med samleprotokollet slik at Enum.into / 2-funksjonen kan brukes på riktig måte. Denne protokollen krever implementering av bare en funksjon: inn i / 2, som samler verdier og returnerer resultatet (merk at det også må støtte : gjort
, : stans
og : forts
verb; Staten bør ikke rapporteres). Del løsningen i kommentarene!
Jeg håper du har hatt glede av å lese denne artikkelen. Hvis du har noen spørsmål igjen, ikke nøl med å kontakte meg. Takk for tålmodigheten, og vi ses snart!