Polymorphismus mit Protokollen in Elixier

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!

Kurze Einführung in die Protokolle

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.

Protokoll definieren

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.  

Ein Protokoll implementieren

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

Ein Hinweis zu Strukturen

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.

Beispiel: String.Chars-Protokoll

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!

Beispiel: Inspect Protocol

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.

Beispiel: Aufzählungsprotokoll

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:

  • count / 1 gibt die Größe des Aufzählers zurück.
  • member? / 2 prüft, ob das Enumerable ein Element enthält.
  • verkleinern / 3 wendet eine Funktion auf jedes Element der Aufzählung an.

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!

Implementieren der Count-Funktion

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!

Umsetzungsmitglied? Funktion

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!

Implementierung der Reduzierungsfunktion

Mit dem wird es etwas komplexer / 3 reduzieren Funktion. Es akzeptiert die folgenden Argumente:

  • eine Zahl, auf die die Funktion angewendet werden soll
  • ein Akkumulator zum Speichern des Ergebnisses
  • die tatsächlich zu verwendende Reduzierfunktion

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

Fazit

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!