Polymorphismus ist ein wichtiges Konzept beim Programmieren, und Programmieranfänger erfahren in den ersten Monaten des Studiums davon. Polymorphismus bedeutet im Wesentlichen, dass Sie eine ähnliche Operation auf Entitäten verschiedener Typen anwenden können. Zum Beispiel kann die Funktion count / 1 sowohl auf einen Bereich als auch auf eine Liste angewendet werden:
Aufzählungszähler (1… 3) Aufzählungssatz ([1,2,3])
Wie ist das möglich? In Elixir wird Polymorphismus durch Verwendung eines interessanten Protokolls erreicht, das als Protokoll bezeichnet wird Vertrag. Für jeden Datentyp, den Sie unterstützen möchten, muss dieses Protokoll implementiert werden.
Alles in allem ist dieser Ansatz nicht revolutionär, da er in anderen Sprachen (wie zum Beispiel Ruby) zu finden ist. Trotzdem sind Protokolle sehr praktisch. In diesem Artikel werden wir diskutieren, wie man sie definiert, implementiert und mit ihnen arbeitet, während wir einige Beispiele untersuchen. Lass uns anfangen!
Wie bereits oben erwähnt, hat ein Protokoll einen generischen Code und verlässt sich bei der Implementierung der Logik auf den spezifischen Datentyp. Dies ist sinnvoll, da unterschiedliche Datentypen unterschiedliche Implementierungen erfordern können. Ein Datentyp kann dann Versand auf einem Protokoll, ohne sich um seine internen Elemente zu kümmern.
Elixir verfügt über eine Reihe integrierter Protokolle, einschließlich Zahlreich
, Sammlerstücke
, Prüfen
, List.Chars
, und String.Chars
. Einige davon werden später in diesem Artikel beschrieben. Sie können jedes dieser Protokolle in Ihrem benutzerdefinierten Modul implementieren und eine Reihe von Funktionen kostenlos erhalten. Wenn Sie beispielsweise Enumerable implementiert haben, erhalten Sie Zugriff auf alle Funktionen, die im Enum-Modul definiert sind. Dies ist ziemlich cool.
Wenn Sie aus der wunderbaren Ruby-Welt voller Objekte, Klassen, Feen und Drachen gekommen sind, haben Sie ein sehr ähnliches Konzept getroffen Mixins. Wenn Sie beispielsweise Ihre Objekte vergleichbar machen müssen, mischen Sie einfach ein Modul mit dem entsprechenden Namen in die Klasse. Dann einfach ein Raumschiff implementieren <=>
Methode und alle Instanzen der Klasse erhalten alle Methoden wie >
und <
kostenlos. Dieser Mechanismus ähnelt den Protokollen in Elixir. Glauben Sie mir, auch wenn Sie dieses Konzept noch nie zuvor getroffen haben, ist es nicht so komplex.
Okay, als Erstes: Das Protokoll muss definiert werden, also sehen wir uns im nächsten Abschnitt an, wie es gemacht werden kann.
Das Definieren eines Protokolls beinhaltet keine schwarze Magie, es ist der Definition von Modulen sehr ähnlich. Verwenden Sie defprotocol / 2, um dies zu tun:
defprotocol MyProtocol endet
Innerhalb der Definition des Protokolls platzieren Sie Funktionen wie bei Modulen. Der einzige Unterschied ist, dass diese Funktionen keinen Körper haben. Dies bedeutet, dass das Protokoll nur eine Schnittstelle definiert, eine Blaupause, die von allen Datentypen implementiert werden sollte, die mit diesem Protokoll abgesetzt werden sollen:
defprotocol MyProtocol definiert mein my_func (arg) ende
In diesem Beispiel muss ein Programmierer das implementieren my_func / 1
Funktion erfolgreich zu nutzen MyProtocol
.
Wenn das Protokoll nicht implementiert ist, wird ein Fehler ausgelöst. Kommen wir zum Beispiel mit der zählen / 1
Funktion innerhalb der definiert Enum
Modul. Das Ausführen des folgenden Codes führt zu einem Fehler:
Enum.count 1 # ** (Protocol.UndefinedError) -Protokoll Enumerable nicht implementiert für 1 # (elixir) lib / enum.ex: 1: Enumerable.impl_for! / 1 # (elixir) lib / enum.ex: 146: Enumerable. count / 1 # (Elixier) lib / enum.ex: 467: Enum.count / 1
Es bedeutet, dass die Ganze Zahl
implementiert das nicht Zahlreich
Protokoll (was für eine Überraschung) und daher können wir keine ganzen Zahlen zählen. Aber eigentlich das Protokoll können umgesetzt werden, und dies ist leicht zu erreichen.
Protokolle werden mit dem Makro defimpl / 3 implementiert. Sie geben an, welches Protokoll implementiert werden soll und für welchen Typ:
defimpl MyProtocol, für: Integer def my_func (arg) do IO.puts (arg) Ende
Jetzt können Sie Ihre Ganzzahlen zählen lassen, indem Sie die Zahlreich
Protokoll:
defimpl Enumerable, für: Integer do def count (_arg) do : ok, 1 # Ganzzahlen enthalten immer ein Element end end Enum.count (100) |> IO.puts # => 1
Wir werden das besprechen Zahlreich
Protokoll später ausführlicher in dem Artikel und implementieren auch seine andere Funktion.
Wie für den Typ (an den übergeben zum
), können Sie einen beliebigen eingebauten Typ, Ihren eigenen Alias oder eine Liste von Aliasnamen angeben:
defimpl MyProtocol, für: [Integer, List] enden
Darüber hinaus können Sie sagen Irgendein
:
defimpl MyProtocol, für: Any def my_func (_) do IO.puts "Nicht implementiert!" Ende Ende
Dies wirkt wie eine Fallback-Implementierung, und es wird kein Fehler ausgegeben, wenn das Protokoll für einen Typ nicht implementiert ist. Damit dies funktioniert, stellen Sie das ein @fallback_to_any
zuschreiben wahr
in Ihrem Protokoll (sonst wird der Fehler immer noch ausgelöst):
defprotocol MyProtocol do @fallback_to_any true def mein_func (arg) end
Sie können das Protokoll jetzt für jeden unterstützten Typ verwenden:
MyProtocol.my_func (5) # druckt einfach 5 MyProtocol.my_func ("test") # druckt "Nicht implementiert!"
Die Implementierung eines Protokolls kann in einem Modul verschachtelt sein. Wenn dieses Modul eine Struktur definiert, müssen Sie nicht einmal angeben zum
beim anrufen defimpl
:
defmodule Produkt defstruct title: "", price: 0 defimpl MyProtocol do def my_func (% Product title: title, price: price) do IO.puts "title # title, price # price" end end end
In diesem Beispiel definieren wir eine neue Struktur namens Produkt
und implementieren Sie unser Demo-Protokoll. Innen passen Sie einfach den Titel und den Preis an und geben dann eine Zeichenfolge aus.
Beachten Sie jedoch, dass eine Implementierung in ein Modul geschachtelt werden muss. Dies bedeutet, dass Sie jedes Modul problemlos erweitern können, ohne auf den Quellcode zugreifen zu müssen.
Okay, genug mit abstrakter Theorie: Sehen wir uns einige Beispiele an. Ich bin sicher, Sie haben die IO.puts / 2-Funktion ziemlich umfangreich eingesetzt, um Debugging-Informationen an die Konsole auszugeben, wenn Sie mit Elixir herumspielen. Sicher können wir verschiedene eingebaute Typen problemlos ausgeben:
IO.puts 5 IO.puts "testen" IO.puts: mein_atom
Aber was passiert, wenn wir versuchen, unser Ergebnis auszugeben? Produkt
Struktur erstellt im vorherigen Abschnitt? Ich werde den entsprechenden Code in die Main
Modul, da Sie andernfalls eine Fehlermeldung erhalten, die besagt, dass die Struktur nicht im selben Gültigkeitsbereich definiert oder aufgerufen wird:
defmodule Produkt defstruct title: "", price: 0 end defmodule Main do def run do% Produkt title: "Test", price: 5 |> IO.puts end end Main.run
Nachdem Sie diesen Code ausgeführt haben, erhalten Sie eine Fehlermeldung:
(Protocol.UndefinedError) Protokoll String.Chars für% Product price: 5, Titel: "Test" nicht implementiert
Aha! Es bedeutet, dass die setzt
Funktion basiert auf dem integrierten String.Chars-Protokoll. Solange es nicht für unsere implementiert ist Produkt
, Der Fehler wird ausgelöst.
String.Chars
ist dafür verantwortlich, verschiedene Strukturen in Binärdateien zu konvertieren. Die einzige Funktion, die Sie implementieren müssen, ist to_string / 1, wie in der Dokumentation angegeben. Warum implementieren wir es jetzt nicht??
defmodule Produkt defstruct title: "", price: 0 defimpl String.Chars def to_string (% product title: title, price: price) do "# title, $ # price" end end end
Wenn dieser Code vorhanden ist, gibt das Programm die folgende Zeichenfolge aus:
Test 5 €
Was bedeutet, dass alles gut funktioniert!
Eine weitere sehr häufige Funktion ist IO.inspect / 2, um Informationen über ein Konstrukt zu erhalten. Es gibt auch eine inspect / 2-Funktion im Kernel
Das Modul-it führt die Inspektion gemäß dem eingebauten Inspect-Protokoll durch.
Unsere Produkt
struct kann sofort eingesehen werden, und Sie erhalten einige kurze Informationen dazu:
% Produkt title: "Test", Preis: 5 |> IO.inspect # oder:% Produkt title: "Test", Preis: 5 |> inspect |> IO.puts
Es wird wiederkommen % Produkt Preis: 5, Titel: "Test"
. Aber auch hier können wir das einfach umsetzen Prüfen
Protokoll, bei dem nur die Funktion inspect / 2 codiert werden muss:
defmodule Produkt defstruct title: "", price: 0 defimpl Inspect do def inspect (% Produkt title: title, price: price, _) do "Das ist eine Produktstruktur. Es hat den Titel # title und ein Preis von # price. Yay! " Ende Ende Ende
Das zweite Argument, das an diese Funktion übergeben wird, ist die Liste der Optionen, die wir jedoch nicht interessieren.
Lassen Sie uns nun ein etwas komplexeres Beispiel sehen, während wir über das Enumerable-Protokoll sprechen. Dieses Protokoll wird vom Enum-Modul verwendet, das uns so bequeme Funktionen wie jedes / 2 und count / 1 bietet (ohne dieses müssten Sie bei der reellen alten Rekursion bleiben).
Enumerable definiert drei Funktionen, die Sie ausarbeiten müssen, um das Protokoll zu implementieren:
Mit all diesen Funktionen haben Sie Zugriff auf alle von der Enum
Modul, das ist ein wirklich gutes Geschäft.
Als Beispiel erstellen wir eine neue Struktur namens Zoo
. Es wird einen Titel und eine Liste von Tieren haben:
defmodule Zoo defstruct Titel: "", Tiere: [] Ende
Jedes Tier wird auch durch eine Struktur dargestellt:
defmodule Animal defstruct Arten: "", Name: "", Alter: 0 Ende
Lassen Sie uns jetzt einen neuen Zoo instanziieren:
defmodule Main do def run do my_zoo =% Zoo title: "Demo Zoo", Tiere: [% Animal Arten: "Tiger", Name: "Tigga", Alter: 5,% Animal Arten: "Pferd", Name: "Amazing", Alter: 3,% Animal Arten: "Hirsch", Name: "Bambi", Alter: 2] end end Main.run
Wir haben also einen "Demo Zoo" mit drei Tieren: einem Tiger, einem Pferd und einem Hirsch. Ich möchte jetzt die Unterstützung für die count / 1-Funktion hinzufügen, die folgendermaßen verwendet wird:
Enum.count (mein_zoo) |> IO.inspect
Lassen Sie uns diese Funktionalität jetzt implementieren!
Was meinen wir mit "Zählen Sie meinen Zoo"? Das hört sich ein bisschen seltsam an, aber wahrscheinlich müssen alle Tiere gezählt werden, die dort leben. Die Implementierung der zugrunde liegenden Funktion wird also recht einfach sein:
defmodule Zoo defstruct title: "", animals: [] defimpl Enumerable def count (% Zoo animals: animals) do : ok, Enum.count (animals) end end end
Alles, was wir hier tun, ist, uns auf die count / 1-Funktion zu verlassen, während wir eine Liste von Tieren an sie übergeben (da diese Funktion Listen ohne Voreinstellung unterstützt). Eine sehr wichtige Sache zu erwähnen ist, dass die zählen / 1
Funktion muss das Ergebnis in Form eines Tupels zurückgeben : ok, Ergebnis
wie von den Dokumenten vorgegeben. Wenn Sie nur eine Zahl zurückgeben, wird ein Fehler angezeigt ** (CaseClauseError) kein Übereinstimmungsfall von case-Klauseln
wird erhöht werden.
Das wars so ziemlich. Sie können jetzt sagen Enum.count (my_zoo)
in der Hauptlauf
, und es sollte zurückkehren 3
als Ergebnis. Gut gemacht!
Die nächste Funktion, die das Protokoll definiert, ist die Mitglied? / 2
. Es sollte ein Tupel zurückgeben : ok, boolean
als Ergebnis, das angibt, ob ein Aufzählungszeichen (als erstes Argument übergeben) ein Element (das zweite Argument) enthält..
Ich möchte mit dieser neuen Funktion sagen, ob ein bestimmtes Tier im Zoo lebt oder nicht. Daher ist die Implementierung auch recht einfach:
defmodule Zoo defstruct title: "", animals: [] defimpl Enumerable do # ... def member? (% Zoo title: _, animals: animals, animal) do : ok, Enum.member? (Tiere, Tier) Ende Ende Ende
Beachten Sie erneut, dass die Funktion zwei Argumente akzeptiert: ein Aufzählungszeichen und ein Element. Innen verlassen wir uns einfach auf die Mitglied? / 2
Funktion zum Suchen eines Tieres in der Liste aller Tiere.
Also laufen wir jetzt:
Enum.member? (My_zoo,% Animal Arten: "Tiger", Name: "Tigga", Alter: 5) |> IO.inspect
Und das sollte wiederkommen wahr
denn wir haben tatsächlich so ein Tier in der Liste!
Mit dem wird es etwas komplexer / 3 reduzieren
Funktion. Es akzeptiert die folgenden Argumente:
Interessant ist, dass der Akku tatsächlich ein Tupel mit zwei Werten enthält: a Verb und einen Wert: Verb, Wert
. Das Verb ist ein Atom und kann einen der folgenden drei Werte haben:
: cont
(fortsetzen):Halt
(kündigen):aussetzen
(vorübergehend aussetzen)Der resultierende Wert, der vom zurückgegeben wird / 3 reduzieren
Funktion ist auch ein Tupel, das den Status und ein Ergebnis enthält. Der Staat ist auch ein Atom und kann folgende Werte annehmen:
:erledigt
(Die Verarbeitung ist abgeschlossen, das ist das Endergebnis): angehalten
(Die Verarbeitung wurde angehalten, weil der Akku :Halt
Verb):suspendiert
(Bearbeitung wurde abgebrochen)Wenn die Verarbeitung unterbrochen wurde, sollten Sie eine Funktion zurückgeben, die den aktuellen Status der Verarbeitung darstellt.
Alle diese Anforderungen werden durch die Umsetzung der / 3 reduzieren
Funktion für die Listen (aus den Dokumenten):
def verkleinern (_, : halt, acc, _fun), do: : halted, acc def verkleinern (list, : suspend, acc, Spaß), do: : suspend, acc, & verkleinern (list & 1, fun) def verkleinern ([], : cont, acc, _fun), do: : erledigt, acc def reduzieren ([h | t], : cont, acc, fun), do: reduzieren (t, spaß. (h, acc), spaß)
Wir können diesen Code als Beispiel verwenden und unsere eigene Implementierung für die Zoo
struct:
defmodule Zoo defstruct title: "", animals: [] defimpl Enumerable def def (_, : halt, acc, _fun), do: : angehalten, acc def verkleinern (% Zoo animals: animals, : suspend, acc, fun) do : suspend, acc, & verkleinern (% Zoo Tiere: Tiere, & 1, Spaß) end def verkleinern (% Zoo Tiere: [], : cont, acc , _fun), do: : done, acc def verkleinern (% Zoo Tiere: [Kopf | Schwanz], : cont, acc, Spaß) do reduzieren (% Zoo Tiere: Schwanz, Spaß. ( Kopf, acc), Spaß) Ende Ende Ende
In der letzten Funktionsklausel nehmen wir den Kopf der Liste mit allen Tieren, wenden die Funktion darauf an und führen sie aus reduzieren
gegen den Schwanz Wenn keine Tiere mehr vorhanden sind (dritte Klausel), wird ein Tupel mit dem Status von zurückgegeben :erledigt
und das Endergebnis. Die erste Klausel gibt ein Ergebnis zurück, wenn die Verarbeitung angehalten wurde. Die zweite Klausel gibt eine Funktion zurück, wenn die :aussetzen
Verb wurde bestanden.
Nun können wir zum Beispiel das Gesamtalter aller unserer Tiere einfach berechnen:
Enum.reduce (my_zoo, 0, fn (animal, total_age) -> animal.age + total_age end) |> IO.puts
Grundsätzlich haben wir nun Zugriff auf alle Funktionen, die von der bereitgestellt werden Enum
Modul. Versuchen wir, join / 2 zu nutzen:
Enum.join (mein_zoo) |> IO.inspect
Sie erhalten jedoch eine Fehlermeldung, dass die String.Chars
Protokoll ist für die nicht implementiert Tier
struct. Dies geschieht weil Beitreten
versucht, jedes Element in einen String zu konvertieren, kann dies jedoch nicht für Tier
. Lassen Sie uns deshalb auch die implementieren String.Chars
Protokoll jetzt:
defmodule Animal defstruct Arten: "", Name: "", Alter: 0 defimpl String.Chars def def_string (% Animal Arten: Arten, Name: Name, Alter: Alter)) do "# Name (# Spezies), Alter # Alter "Ende Ende Ende
Jetzt sollte alles gut funktionieren. Sie können auch versuchen, jedes / 2 auszuführen und einzelne Tiere anzuzeigen:
Enum.each (mein_zoo, & (IO.puts (& 1)))
Das funktioniert wieder einmal, weil wir zwei Protokolle implementiert haben: Zahlreich
(für die Zoo
) und String.Chars
(für die Tier
).
In diesem Artikel haben wir diskutiert, wie der Polymorphismus in Elixir mithilfe von Protokollen implementiert wird. Sie haben gelernt, Protokolle zu definieren und zu implementieren sowie integrierte Protokolle zu verwenden: Zahlreich
, Prüfen
, und String.Chars
.
Als Übung können Sie versuchen, unsere Fähigkeiten zu stärken Zoo
Modul mit dem Collectable-Protokoll, damit die Funktion Enum.into / 2 ordnungsgemäß verwendet werden kann. Für dieses Protokoll muss nur eine Funktion implementiert werden: in / 2, die Werte sammelt und das Ergebnis zurückgibt (beachten Sie, dass es auch die Funktion unterstützen muss :erledigt
, :Halt
und : cont
Verben; der Staat sollte nicht gemeldet werden). Teilen Sie Ihre Lösung in den Kommentaren!
Ich hoffe, Sie haben diesen Artikel gerne gelesen. Wenn Sie noch Fragen haben, können Sie sich gerne an mich wenden. Vielen Dank für die Geduld und bis bald!