Elixier - Kurzanleitung

Elixir ist eine dynamische, funktionale Sprache zum Erstellen skalierbarer und wartbarer Anwendungen. Es nutzt die Erlang-VM, die für die Ausführung verteilter und fehlertoleranter Systeme mit geringer Latenz bekannt ist und gleichzeitig in der Webentwicklung und in der eingebetteten Softwaredomäne erfolgreich eingesetzt wird.

Elixir ist eine funktionale, dynamische Sprache, die auf Erlang und der Erlang VM aufbaut. Erlang ist eine Sprache, die ursprünglich 1986 von Ericsson geschrieben wurde, um Telefonieprobleme wie Verteilung, Fehlertoleranz und Parallelität zu lösen. Elixir, geschrieben von José Valim, erweitert Erlang und bietet eine freundlichere Syntax für die Erlang-VM. Dies geschieht unter Beibehaltung der Leistung auf dem gleichen Niveau wie bei Erlang.

Eigenschaften von Elixier

Lassen Sie uns nun einige wichtige Funktionen von Elixir diskutieren -

  • Scalability - Der gesamte Elixir-Code wird in einfachen Prozessen ausgeführt, die isoliert sind und Informationen über Nachrichten austauschen.

  • Fault Tolerance- Elixir bietet Supervisoren, die beschreiben, wie Sie Teile Ihres Systems neu starten, wenn etwas schief geht, und zu einem bekannten Ausgangszustand zurückkehren, der garantiert funktioniert. Dies stellt sicher, dass Ihre Anwendung / Plattform niemals ausfällt.

  • Functional Programming - Die funktionale Programmierung fördert einen Codierungsstil, mit dem Entwickler Code schreiben können, der kurz, schnell und wartbar ist.

  • Build tools- Elixir wird mit einer Reihe von Entwicklungswerkzeugen geliefert. Mix ist ein solches Tool, mit dem Sie problemlos Projekte erstellen, Aufgaben verwalten, Tests ausführen usw. Es verfügt auch über einen eigenen Paketmanager - Hex.

  • Erlang Compatibility - Elixir läuft auf der Erlang-VM und bietet Entwicklern vollständigen Zugriff auf das Erlang-Ökosystem.

Um Elixir ausführen zu können, müssen Sie es lokal auf Ihrem System einrichten.

Um Elixir zu installieren, benötigen Sie zunächst Erlang. Auf einigen Plattformen enthalten Elixir-Pakete Erlang.

Elixier installieren

Lassen Sie uns nun die Installation von Elixir in verschiedenen Betriebssystemen verstehen.

Windows Setup

Laden Sie das Installationsprogramm von herunter, um Elixir unter Windows zu installieren https://repo.hex.pm/elixirwebsetup.exe und einfach klicken Nextum alle Schritte durchzugehen. Sie haben es auf Ihrem lokalen System.

Wenn Sie während der Installation Probleme haben, können Sie auf dieser Seite weitere Informationen finden.

Mac-Setup

Wenn Sie Homebrew installiert haben, stellen Sie sicher, dass es sich um die neueste Version handelt. Verwenden Sie zum Aktualisieren den folgenden Befehl:

brew update

Installieren Sie nun Elixir mit dem folgenden Befehl:

brew install elixir

Ubuntu / Debian Setup

Die Schritte zum Installieren von Elixir in einem Ubuntu / Debian-Setup lauten wie folgt:

Erlang Solutions Repo hinzufügen -

wget https://packages.erlang-solutions.com/erlang-solutions_1.0_all.deb && sudo 
dpkg -i erlang-solutions_1.0_all.deb 
sudo apt-get update

Installieren Sie die Erlang / OTP-Plattform und alle ihre Anwendungen -

sudo apt-get install esl-erlang

Installieren Sie Elixir -

sudo apt-get install elixir

Andere Linux Distros

Wenn Sie eine andere Linux-Distribution haben, besuchen Sie diese Seite , um Elixier auf Ihrem lokalen System einzurichten.

Setup testen

Um das Elixir-Setup auf Ihrem System zu testen, öffnen Sie Ihr Terminal und geben Sie iex ein. Die interaktive Elixier-Shell wird wie folgt geöffnet:

Erlang/OTP 19 [erts-8.0] [source-6dc93c1] [64-bit] 
[smp:4:4] [async-threads:10] [hipe] [kernel-poll:false]  

Interactive Elixir (1.3.1) - press Ctrl+C to exit (type h() ENTER for help) 
iex(1)>

Elixir ist jetzt erfolgreich auf Ihrem System eingerichtet.

Wir beginnen mit dem üblichen 'Hello World'-Programm.

Geben Sie den folgenden Befehl ein, um die interaktive Elixir-Shell zu starten.

iex

Verwenden Sie nach dem Start der Shell die IO.putsFunktion zum "Einfügen" der Zeichenfolge in die Konsolenausgabe. Geben Sie Folgendes in Ihre Elixir-Shell ein:

IO.puts "Hello world"

In diesem Tutorial verwenden wir den Elixir-Skriptmodus, in dem wir den Elixir-Code in einer Datei mit der Erweiterung aufbewahren .ex. Lassen Sie uns nun den obigen Code in dertest.exDatei. Im folgenden Schritt werden wir es mit ausführenelixirc- -

IO.puts "Hello world"

Versuchen wir nun, das obige Programm wie folgt auszuführen:

$elixirc test.ex

Das obige Programm generiert das folgende Ergebnis:

Hello World

Hier rufen wir eine Funktion auf IO.putsum einen String für unsere Konsole als Ausgabe zu generieren. Diese Funktion kann auch wie in C, C ++, Java usw. aufgerufen werden und liefert Argumente in Klammern nach dem Funktionsnamen -

IO.puts("Hello world")

Bemerkungen

Einzeilige Kommentare beginnen mit einem '#' Symbol. Es gibt keinen mehrzeiligen Kommentar, aber Sie können mehrere Kommentare stapeln. Zum Beispiel -

#This is a comment in Elixir

Zeilenenden

Es sind keine Zeilenenden wie ';' erforderlich. in Elixier. Mit ';' können wir jedoch mehrere Anweisungen in derselben Zeile haben. Zum Beispiel,

IO.puts("Hello"); IO.puts("World!")

Das obige Programm generiert das folgende Ergebnis:

Hello 
World!

Kennungen

Bezeichner wie Variablen, Funktionsnamen werden verwendet, um eine Variable, eine Funktion usw. zu identifizieren. In Elixir können Sie Ihre Bezeichner benennen, beginnend mit einem Kleinbuchstaben mit Zahlen, Unterstrichen und Großbuchstaben. Diese Namenskonvention ist allgemein als snake_case bekannt. Im Folgenden sind beispielsweise einige gültige Bezeichner in Elixir aufgeführt:

var1       variable_2      one_M0r3_variable

Bitte beachten Sie, dass Variablen auch mit einem führenden Unterstrich benannt werden können. Ein Wert, der nicht verwendet werden soll, muss _ oder einer Variablen zugewiesen werden, die mit dem Unterstrich beginnt -

_some_random_value = 42

Elixier stützt sich auch auf Unterstriche, um Funktionen für Module privat zu machen. Wenn Sie eine Funktion mit einem führenden Unterstrich in einem Modul benennen und dieses Modul importieren, wird diese Funktion nicht importiert.

Es gibt viele weitere Feinheiten im Zusammenhang mit der Benennung von Funktionen in Elixir, die wir in den kommenden Kapiteln diskutieren werden.

Reservierte Wörter

Die folgenden Wörter sind reserviert und können nicht als Variablen-, Modul- oder Funktionsnamen verwendet werden.

after     and     catch     do     inbits     inlist     nil     else     end 
not     or     false     fn     in     rescue     true     when     xor 
__MODULE__    __FILE__    __DIR__    __ENV__    __CALLER__

Um eine Sprache verwenden zu können, müssen Sie die grundlegenden Datentypen verstehen, die die Sprache unterstützt. In diesem Kapitel werden 7 grundlegende Datentypen erläutert, die von der Elixiersprache unterstützt werden: Ganzzahlen, Gleitkommazahlen, Boolesche Werte, Atome, Zeichenfolgen, Listen und Tupel.

Numerische Typen

Elixir unterstützt wie jede andere Programmiersprache sowohl Ganzzahlen als auch Floats. Wenn Sie Ihre Elixier-Shell öffnen und eine Ganzzahl oder ein Gleitkomma als Eingabe eingeben, wird der Wert zurückgegeben. Zum Beispiel,

42

Wenn das obige Programm ausgeführt wird, wird das folgende Ergebnis erzeugt:

42

Sie können Zahlen auch in Oktal-, Hex- und Binärbasis definieren.

Oktal

Um eine Zahl in Oktalbasis zu definieren, stellen Sie ihr '0o' voran. Zum Beispiel entspricht 0o52 in Oktal 42 in Dezimal.

Hexadezimal

Um eine Zahl in Dezimalbasis zu definieren, stellen Sie ihr '0x' voran. Zum Beispiel entspricht 0xF1 in hex 241 in dezimal.

Binär

Um eine Zahl in binärer Basis zu definieren, stellen Sie ihr '0b' voran. Zum Beispiel entspricht 0b1101 in Binärform 13 in Dezimalzahl.

Elixir unterstützt 64-Bit-Doppelgenauigkeit für Gleitkommazahlen. Sie können auch mithilfe eines Exponentiationsstils definiert werden. Beispielsweise kann 10145230000 als 1.014523e10 geschrieben werden

Atome

Atome sind Konstanten, deren Name ihr Wert ist. Sie können mit dem Symbol color (:) erstellt werden. Zum Beispiel,

:hello

Boolesche Werte

Elixier unterstützt true und falseals Boolesche. Diese beiden Werte sind tatsächlich an Atome gebunden: wahr bzw. falsch.

Saiten

Zeichenfolgen in Elixir werden zwischen doppelte Anführungszeichen eingefügt und in UTF-8 codiert. Sie können mehrere Zeilen umfassen und Interpolationen enthalten. Um eine Zeichenfolge zu definieren, geben Sie sie einfach in doppelte Anführungszeichen ein.

"Hello world"

Um mehrzeilige Zeichenfolgen zu definieren, verwenden wir eine Python-ähnliche Syntax mit dreifachen doppelten Anführungszeichen.

"""
Hello
World!
"""

Im Kapitel über Zeichenfolgen erfahren Sie mehr über Zeichenfolgen, Binärdateien und Zeichenlisten (ähnlich wie Zeichenfolgen).

Binärdateien

Binärdateien sind Folgen von Bytes in << >>, die durch ein Komma getrennt sind. Zum Beispiel,

<< 65, 68, 75>>

Binärdateien werden meistens verwendet, um mit Bits und Bytes zusammenhängende Daten zu verarbeiten, falls vorhanden. Sie können standardmäßig 0 bis 255 in jedem Wert speichern. Diese Größenbeschränkung kann mithilfe der Größenfunktion erhöht werden, die angibt, wie viele Bits zum Speichern dieses Werts erforderlich sind. Zum Beispiel,

<<65, 255, 289::size(15)>>

Listen

Elixir verwendet eckige Klammern, um eine Liste von Werten anzugeben. Werte können von jedem Typ sein. Zum Beispiel,

[1, "Hello", :an_atom, true]

Listen enthalten integrierte Funktionen für Kopf und Ende der Liste mit den Namen hd und tl, die den Kopf und das Ende der Liste zurückgeben. Wenn Sie eine Liste erstellen, wird manchmal eine Zeichenliste zurückgegeben. Dies liegt daran, dass Elixier, wenn es eine Liste druckbarer ASCII-Zeichen sieht, diese als Zeichenliste druckt. Bitte beachten Sie, dass Zeichenfolgen und Zeichenlisten nicht gleich sind. Wir werden die Listen in späteren Kapiteln weiter diskutieren.

Tupel

Elixir verwendet geschweifte Klammern, um Tupel zu definieren. Tupel können wie Listen einen beliebigen Wert enthalten.

{ 1, "Hello", :an_atom, true

Hier stellt sich die Frage, warum beides vorgesehen ist lists und tupleswenn beide gleich arbeiten? Nun, sie haben unterschiedliche Implementierungen.

  • Listen werden tatsächlich als verknüpfte Listen gespeichert, sodass Einfügungen und Löschungen in Listen sehr schnell erfolgen.

  • Tupel hingegen werden in einem zusammenhängenden Speicherblock gespeichert, wodurch der Zugriff auf sie beschleunigt wird, aber zusätzliche Kosten für das Einfügen und Löschen entstehen.

Eine Variable stellt uns einen benannten Speicher zur Verfügung, den unsere Programme bearbeiten können. Jede Variable in Elixir hat einen bestimmten Typ, der die Größe und das Layout des Speichers der Variablen bestimmt. den Wertebereich, der in diesem Speicher gespeichert werden kann; und die Menge von Operationen, die auf die Variable angewendet werden können.

Arten von Variablen

Elixir unterstützt die folgenden grundlegenden Variablentypen.

Ganze Zahl

Diese werden für Ganzzahlen verwendet. Sie haben eine Größe von 32 Bit in einer 32-Bit-Architektur und 64 Bit in einer 64-Bit-Architektur. Ganzzahlen werden immer im Elixier signiert. Wenn sich die Größe einer Ganzzahl über ihre Grenze hinaus vergrößert, wandelt Elixier sie in eine große Ganzzahl um, die Speicher im Bereich von 3 bis n Wörtern belegt, je nachdem, was in den Speicher passt.

Schwimmt

Floats haben eine 64-Bit-Genauigkeit im Elixier. Sie sind auch in Bezug auf den Speicher wie ganze Zahlen. Bei der Definition eines Gleitkommas kann die Exponentialschreibweise verwendet werden.

Boolescher Wert

Sie können 2 Werte annehmen, die entweder wahr oder falsch sind.

Saiten

Strings sind utf-8 in Elixier codiert. Sie verfügen über ein Strings-Modul, das dem Programmierer viele Funktionen zum Bearbeiten von Strings bietet.

Anonyme Funktionen / Lambdas

Dies sind Funktionen, die definiert und einer Variablen zugewiesen werden können, mit denen diese Funktion aufgerufen werden kann.

Sammlungen

In Elixir sind viele Sammlungstypen verfügbar. Einige davon sind Listen, Tupel, Karten, Binärdateien usw. Diese werden in den folgenden Kapiteln erläutert.

Variable Aussage

Eine Variablendeklaration teilt dem Interpreter mit, wo und wie viel Speicher für die Variable erstellt werden soll. Mit Elixir können wir nicht nur eine Variable deklarieren. Eine Variable muss gleichzeitig deklariert und mit einem Wert versehen werden. Um beispielsweise eine Variable mit dem Namen life zu erstellen und ihr den Wert 42 zuzuweisen, gehen Sie wie folgt vor:

life = 42

Dies wird binden die Variable Leben Wert 42. Wenn wir diese Variable einen neuen Wert zuweisen wollen, müssen wir dies tun können , die gleiche Syntax wie oben unter Verwendung, das heißt,

life = "Hello world"

Variablennamen

Namensvariablen folgen a snake_caseKonvention in Elixir, dh alle Variablen müssen mit einem Kleinbuchstaben beginnen, gefolgt von 0 oder mehr Buchstaben (sowohl Groß- als auch Kleinbuchstaben), gefolgt von einem optionalen '?' ODER '!'.

Variablennamen können auch mit einem führenden Unterstrich gestartet werden. Dies darf jedoch nur verwendet werden, wenn die Variable ignoriert wird, dh diese Variable wird nicht erneut verwendet, sondern muss etwas zugewiesen werden.

Variablen drucken

In der interaktiven Shell werden Variablen gedruckt, wenn Sie nur den Variablennamen eingeben. Zum Beispiel, wenn Sie eine Variable erstellen -

life = 42

Wenn Sie "Leben" in Ihre Shell eingeben, erhalten Sie die Ausgabe als -

42

Wenn Sie jedoch eine Variable an die Konsole ausgeben möchten (wenn Sie ein externes Skript aus einer Datei ausführen), müssen Sie die Variable als Eingabe für bereitstellen IO.puts Funktion -

life = 42  
IO.puts life

oder

life = 42 
IO.puts(life)

Dies gibt Ihnen die folgende Ausgabe -

42

Ein Operator ist ein Symbol, das den Compiler anweist, bestimmte mathematische oder logische Manipulationen durchzuführen. Es gibt viele Bediener, die von elixir bereitgestellt werden. Sie sind in folgende Kategorien unterteilt:

  • Rechenzeichen
  • Vergleichsoperatoren
  • boolesche Operatoren
  • Verschiedene Operatoren

Rechenzeichen

Die folgende Tabelle zeigt alle arithmetischen Operatoren, die von der Elixir-Sprache unterstützt werden. Variable annehmenA hält 10 und variabel B hält 20, dann -

Beispiele anzeigen

Operator Beschreibung Beispiel
+ Fügt 2 Zahlen hinzu. A + B ergibt 30
- - Subtrahiert die zweite Zahl von der ersten. AB gibt -10
* * Multipliziert zwei Zahlen. A * B ergibt 200
/. Teilt die erste Zahl von der zweiten. Dies wirft die Zahlen in Floats und ergibt ein Float-Ergebnis A / B ergibt 0,5.
div Diese Funktion wird verwendet, um den Quotienten bei Division zu erhalten. div (10,20) ergibt 0
rem Diese Funktion wird verwendet, um den Rest der Division zu erhalten. rem (A, B) ergibt 10

Vergleichsoperatoren

Die Vergleichsoperatoren in Elixir sind meistens mit denen identisch, die in den meisten anderen Sprachen bereitgestellt werden. In der folgenden Tabelle sind die Vergleichsoperatoren in Elixir zusammengefasst. Variable annehmenA hält 10 und variabel B hält 20, dann -

Beispiele anzeigen

Operator Beschreibung Beispiel
== Überprüft, ob der Wert links gleich dem Wert rechts ist (Typ wandelt Werte um, wenn sie nicht vom gleichen Typ sind). A == B gibt false
! = Überprüft, ob der Wert links nicht dem Wert rechts entspricht. A! = B wird wahr geben
=== Überprüft, ob der Werttyp links dem Werttyp rechts entspricht. Wenn ja, überprüfen Sie den Wert ebenfalls. A === B gibt false
! == Wie oben, prüft jedoch, ob Ungleichheit statt Gleichheit vorliegt. A! == B wird wahr geben
> Überprüft, ob der Wert des linken Operanden größer als der Wert des rechten Operanden ist. Wenn ja, wird die Bedingung erfüllt. A> B wird falsch geben
< Überprüft, ob der Wert des linken Operanden kleiner als der Wert des rechten Operanden ist. Wenn ja, wird die Bedingung erfüllt. A <B wird wahr geben
> = Überprüft, ob der Wert des linken Operanden größer oder gleich dem Wert des rechten Operanden ist. Wenn ja, wird die Bedingung erfüllt. A> = B gibt false
<= Überprüft, ob der Wert des linken Operanden kleiner oder gleich dem Wert des rechten Operanden ist. Wenn ja, wird die Bedingung erfüllt. A <= B gibt wahr

Logische Operatoren

Elixir bietet 6 logische Operatoren: und, oder nicht, &&, || und !. Die ersten drei,and or notsind strenge Boolesche Operatoren, was bedeutet, dass sie erwarten, dass ihr erstes Argument ein Boolescher Wert ist. Ein nicht-boolesches Argument löst einen Fehler aus. Während die nächsten drei,&&, || and !sind nicht streng, verlangen nicht, dass wir den ersten Wert streng als Booleschen Wert haben. Sie arbeiten genauso wie ihre strengen Kollegen. Variable annehmenA gilt wahr und variabel B hält 20, dann -

Beispiele anzeigen

Operator Beschreibung Beispiel
und Überprüft, ob beide angegebenen Werte wahr sind. Wenn ja, wird der Wert der zweiten Variablen zurückgegeben. (Logisch und). A und B geben 20
oder Überprüft, ob einer der angegebenen Werte wahr ist. Gibt den Wert zurück, der wahr ist. Andernfalls wird false zurückgegeben. (Logisch oder). A oder B geben wahr
nicht Unärer Operator, der den Wert der angegebenen Eingabe invertiert. nicht A wird falsch geben
&& Nicht streng and. Funktioniert genauso wieand erwartet jedoch nicht, dass das erste Argument ein Boolescher Wert ist. B & & A wird 20 geben
|| Nicht streng or. Funktioniert genauso wieor erwartet jedoch nicht, dass das erste Argument ein Boolescher Wert ist. B || A wird wahr geben
! Nicht streng not. Funktioniert genauso wienot erwartet jedoch nicht, dass das Argument ein Boolescher Wert ist. ! A wird falsch geben

NOTE −und , oder , && und || || sind Kurzschlussbetreiber. Dies bedeutet, dass, wenn das erste Argument vonandist falsch, dann wird nicht weiter nach dem zweiten gesucht. Und wenn das erste Argument vonorist wahr, dann wird nicht nach dem zweiten gesucht. Zum Beispiel,

false and raise("An error")  
#This won't raise an error as raise function wont get executed because of short
#circuiting nature of and operator

Bitweise Operatoren

Bitweise Operatoren arbeiten an Bits und führen bitweise Operationen durch. Elixir bietet bitweise Module als Teil des PaketsBitwise, So um diese zu verwenden, müssen Sie verwenden das bitweise Modul. Um es zu verwenden, geben Sie den folgenden Befehl in Ihre Shell ein:

use Bitwise

Angenommen, A ist 5 und B ist 6 für die folgenden Beispiele -

Beispiele anzeigen

Operator Beschreibung Beispiel
&&& Bitweise und Operator kopiert ein Bit, um es zu erhalten, wenn es in beiden Operanden vorhanden ist. A &&& B wird 4 geben
||| Bitweise oder Operator kopiert ein Bit, um es zu erhalten, wenn es in einem der Operanden vorhanden ist. A ||| B wird 7 geben
>>> Der bitweise Rechtsverschiebungsoperator verschiebt die ersten Operandenbits um die im zweiten Operanden angegebene Zahl nach rechts. A >>> B ergibt 0
<<< Der bitweise Linksverschiebungsoperator verschiebt die ersten Operandenbits um die im zweiten Operanden angegebene Zahl nach links. A <<< B ergibt 320
^^^ Der bitweise XOR-Operator kopiert ein Bit nur dann, wenn es auf beiden Operanden unterschiedlich ist. A ^^^ B ergibt 3
~~~ Unär bitweise invertiert die Bits der angegebenen Zahl nicht. ~~~ A wird -6 geben

Verschiedene Operatoren

Neben den oben genannten Operatoren bietet Elixir auch eine Reihe anderer Operatoren wie Concatenation Operator, Match Operator, Pin Operator, Pipe Operator, String Match Operator, Code Point Operator, Capture Operator, Ternary Operator das macht es zu einer ziemlich mächtigen Sprache.

Beispiele anzeigen

Pattern Matching ist eine Technik, die Elixir von Erlang erbt. Es ist eine sehr leistungsfähige Technik, mit der wir einfachere Unterstrukturen aus komplizierten Datenstrukturen wie Listen, Tupeln, Karten usw. extrahieren können.

Ein Match besteht aus 2 Hauptteilen: a left und ein rightSeite. Die rechte Seite ist eine Datenstruktur jeglicher Art. Die linke Seite versucht, die Datenstruktur auf der rechten Seite abzugleichen und alle Variablen auf der linken Seite an die jeweilige Unterstruktur auf der rechten Seite zu binden. Wird keine Übereinstimmung gefunden, gibt der Bediener einen Fehler aus.

Die einfachste Übereinstimmung ist eine einzelne Variable links und eine beliebige Datenstruktur rechts. This variable will match anything. Zum Beispiel,

x = 12
x = "Hello"
IO.puts(x)

Sie können Variablen innerhalb einer Struktur platzieren, um eine Unterstruktur zu erfassen. Zum Beispiel,

[var_1, _unused_var, var_2] = [{"First variable"}, 25, "Second variable" ]
IO.puts(var_1)
IO.puts(var_2)

Dadurch werden die Werte gespeichert. {"First variable"}in var_1 und"Second variable"in var_2 . Es gibt auch eine besondere_ Variable (oder Variablen mit dem Präfix '_'), die genau wie andere Variablen funktioniert, aber Elixier mitteilt, "Make sure something is here, but I don't care exactly what it is.". Im vorherigen Beispiel war _unused_var eine solche Variable.

Mit dieser Technik können wir kompliziertere Muster abgleichen. Zumexample Wenn Sie eine Zahl in einem Tupel auspacken und erhalten möchten, das sich in einer Liste befindet, die sich selbst in einer Liste befindet, können Sie den folgenden Befehl verwenden:

[_, [_, {a}]] = ["Random string", [:an_atom, {24}]]
IO.puts(a)

Das obige Programm generiert das folgende Ergebnis:

24

Dies wird binden a bis 24. Andere Werte werden ignoriert, da wir '_' verwenden.

Beim Pattern Matching, wenn wir eine Variable auf dem verwenden rightwird sein Wert verwendet. Wenn Sie den Wert einer Variablen auf der linken Seite verwenden möchten, müssen Sie den Pin-Operator verwenden.

Wenn Sie beispielsweise eine Variable "a" mit dem Wert 25 haben und diese mit einer anderen Variablen "b" mit dem Wert 25 abgleichen möchten, müssen Sie Folgendes eingeben:

a = 25
b = 25
^a = b

Die letzte Zeile entspricht dem aktuellen Wert von a, anstatt es dem Wert von zuzuweisen b. Wenn wir einen nicht übereinstimmenden Satz von linker und rechter Seite haben, löst der Übereinstimmungsoperator einen Fehler aus. Wenn wir beispielsweise versuchen, ein Tupel mit einer Liste oder eine Liste der Größe 2 mit einer Liste der Größe 3 abzugleichen, wird ein Fehler angezeigt.

Entscheidungsstrukturen erfordern, dass der Programmierer eine oder mehrere Bedingungen angibt, die vom Programm bewertet oder getestet werden sollen, sowie eine Anweisung oder Anweisungen, die ausgeführt werden sollen, wenn die Bedingung bestimmt wird trueund optional andere Anweisungen, die ausgeführt werden sollen, wenn die Bedingung bestimmt wird false.

Es folgt das Allgemeine einer typischen Entscheidungsstruktur, die in den meisten Programmiersprachen zu finden ist:

Elixir bietet if / else-bedingte Konstrukte wie viele andere Programmiersprachen. Es hat auch einecondAnweisung, die den ersten gefundenen wahren Wert aufruft. Case ist eine weitere Kontrollflussanweisung, die den Musterabgleich verwendet, um den Programmfluss zu steuern. Schauen wir sie uns einmal genauer an.

Elixir bietet die folgenden Arten von Entscheidungserklärungen. Klicken Sie auf die folgenden Links, um deren Details zu überprüfen.

Sr.Nr. Aussage & Beschreibung
1 if-Anweisung

Eine if-Anweisung besteht aus einem booleschen Ausdruck, gefolgt von do, eine oder mehrere ausführbare Anweisungen und schließlich eine endStichwort. Code in der if-Anweisung wird nur ausgeführt, wenn die boolesche Bedingung true ergibt.

2 if..else Anweisung

Auf eine if-Anweisung kann eine optionale else-Anweisung (innerhalb des do..end-Blocks) folgen, die ausgeführt wird, wenn der Boolesche Ausdruck false ist.

3 es sei denn, Aussage

Eine if-Anweisung hat denselben Text wie eine if-Anweisung. Der Code in der Anweisung "Es sei denn" wird nur ausgeführt, wenn die angegebene Bedingung falsch ist.

4 es sei denn .. andere Aussage

Eine if..else-Anweisung hat denselben Text wie eine if..else-Anweisung. Der Code in der Anweisung "Es sei denn" wird nur ausgeführt, wenn die angegebene Bedingung falsch ist.

5 cond

Eine cond-Anweisung wird verwendet, wenn Code auf der Grundlage mehrerer Bedingungen ausgeführt werden soll. Es funktioniert wie ein if ... else if ... anderes Konstrukt in mehreren anderen Programmiersprachen.

6 Fall

Die case-Anweisung kann als Ersatz für die switch-Anweisung in imperativen Sprachen betrachtet werden. Fall nimmt eine Variable / ein Literal und wendet Musterabgleich mit verschiedenen Fällen an. Wenn ein Fall übereinstimmt, führt Elixir den mit diesem Fall verknüpften Code aus und beendet die case-Anweisung.

Zeichenfolgen in Elixir werden zwischen doppelte Anführungszeichen eingefügt und in UTF-8 codiert. Im Gegensatz zu C und C ++, bei denen die Standardzeichenfolgen ASCII-codiert sind und nur 256 verschiedene Zeichen möglich sind, besteht UTF-8 aus 1.112.064 Codepunkten. Dies bedeutet, dass die UTF-8-Codierung aus diesen vielen verschiedenen möglichen Zeichen besteht. Da die Zeichenfolgen utf-8 verwenden, können wir auch Symbole wie ö, ł usw. verwenden.

Erstellen Sie einen String

Um eine Zeichenfolgenvariable zu erstellen, weisen Sie einer Variablen einfach eine Zeichenfolge zu.

str = "Hello world"

Um dies auf Ihre Konsole zu drucken, rufen Sie einfach die IO.puts Funktion und übergeben Sie ihm die Variable str -

str = str = "Hello world" 
IO.puts(str)

Das obige Programm generiert das folgende Ergebnis:

Hello World

Leere Saiten

Sie können eine leere Zeichenfolge mit dem Zeichenfolgenliteral erstellen. "". Zum Beispiel,

a = ""
if String.length(a) === 0 do
   IO.puts("a is an empty string")
end

Das obige Programm generiert das folgende Ergebnis.

a is an empty string

String-Interpolation

Die String-Interpolation ist eine Möglichkeit, einen neuen String-Wert aus einer Mischung von Konstanten, Variablen, Literalen und Ausdrücken zu erstellen, indem deren Werte in ein String-Literal eingefügt werden. Elixir unterstützt die String-Interpolation, um beim Schreiben eine Variable in einem String zu verwenden, diese mit geschweiften Klammern zu umschließen und den geschweiften Klammern ein a voranzustellen'#' Zeichen.

Zum Beispiel,

x = "Apocalypse" 
y = "X-men #{x}"
IO.puts(y)

Dies nimmt den Wert von x und ersetzt ihn durch y. Der obige Code generiert das folgende Ergebnis:

X-men Apocalypse

String-Verkettung

Wir haben bereits in früheren Kapiteln die Verwendung der String-Verkettung gesehen. Der Operator '<>' wird verwendet, um Zeichenfolgen in Elixir zu verketten. Um 2 Zeichenfolgen zu verketten,

x = "Dark"
y = "Knight"
z = x <> " " <> y
IO.puts(z)

Der obige Code generiert das folgende Ergebnis:

Dark Knight

String-Länge

Um die Länge der Zeichenfolge zu ermitteln, verwenden wir die String.lengthFunktion. Übergeben Sie den String als Parameter und er zeigt Ihnen seine Größe. Zum Beispiel,

IO.puts(String.length("Hello"))

Wenn das obige Programm ausgeführt wird, wird folgendes Ergebnis erzielt:

5

String umkehren

Um einen String umzukehren, übergeben Sie ihn an die Funktion String.reverse. Zum Beispiel,

IO.puts(String.reverse("Elixir"))

Das obige Programm generiert das folgende Ergebnis:

rixilE

String-Vergleich

Um 2 Zeichenfolgen zu vergleichen, können wir die Operatoren == oder === verwenden. Zum Beispiel,

var_1 = "Hello world"
var_2 = "Hello Elixir"
if var_1 === var_2 do
   IO.puts("#{var_1} and #{var_2} are the same")
else
   IO.puts("#{var_1} and #{var_2} are not the same")
end

Das obige Programm generiert das folgende Ergebnis:

Hello world and Hello elixir are not the same.

String Matching

Wir haben bereits die Verwendung des = ~ string match-Operators gesehen. Um zu überprüfen, ob eine Zeichenfolge mit einem regulären Ausdruck übereinstimmt, können Sie auch den Zeichenfolgenübereinstimmungsoperator oder String.match? Verwenden. Funktion. Zum Beispiel,

IO.puts(String.match?("foo", ~r/foo/))
IO.puts(String.match?("bar", ~r/foo/))

Das obige Programm generiert das folgende Ergebnis:

true 
false

Dies kann auch mit dem Operator = ~ erreicht werden. Zum Beispiel,

IO.puts("foo" =~ ~r/foo/)

Das obige Programm generiert das folgende Ergebnis:

true

String-Funktionen

Elixir unterstützt eine Vielzahl von Funktionen, die sich auf Zeichenfolgen beziehen. Einige der am häufigsten verwendeten Funktionen sind in der folgenden Tabelle aufgeführt.

Sr.Nr. Funktion und Zweck
1

at(string, position)

Gibt das Graphem an der Position der angegebenen utf8-Zeichenfolge zurück. Wenn die Position größer als die Zeichenfolgenlänge ist, wird null zurückgegeben

2

capitalize(string)

Konvertiert das erste Zeichen in der angegebenen Zeichenfolge in Großbuchstaben und den Rest in Kleinbuchstaben

3

contains?(string, contents)

Überprüft, ob die Zeichenfolge einen der angegebenen Inhalte enthält

4

downcase(string)

Konvertiert alle Zeichen in der angegebenen Zeichenfolge in Kleinbuchstaben

5

ends_with?(string, suffixes)

Gibt true zurück, wenn die Zeichenfolge mit einem der angegebenen Suffixe endet

6

first(string)

Gibt das erste Graphem aus einer utf8-Zeichenfolge zurück, null, wenn die Zeichenfolge leer ist

7

last(string)

Gibt das letzte Graphem aus einer utf8-Zeichenfolge zurück, null, wenn die Zeichenfolge leer ist

8

replace(subject, pattern, replacement, options \\ [])

Gibt eine neue Zeichenfolge zurück, die durch Ersetzen von Mustervorkommen im Betreff durch Ersetzen erstellt wurde

9

slice(string, start, len)

Gibt einen Teilstring zurück, der am Versatzstart beginnt und die Länge len hat

10

split(string)

Teilt eine Zeichenfolge bei jedem Auftreten von Unicode-Leerzeichen in Teilzeichenfolgen, wobei führende und nachfolgende Leerzeichen ignoriert werden. Gruppen von Leerzeichen werden als ein einzelnes Vorkommen behandelt. Auf nicht unterbrechenden Leerzeichen treten keine Unterteilungen auf

11

upcase(string)

Konvertiert alle Zeichen in der angegebenen Zeichenfolge in Großbuchstaben

Binärdateien

Eine Binärdatei ist nur eine Folge von Bytes. Binärdateien werden mit definiert<< >>. Zum Beispiel:

<< 0, 1, 2, 3 >>

Natürlich können diese Bytes auf irgendeine Weise organisiert werden, selbst in einer Sequenz, die sie nicht zu einer gültigen Zeichenfolge macht. Zum Beispiel,

<< 239, 191, 191 >>

Strings sind auch Binärdateien. Und der String-Verkettungsoperator<> ist eigentlich ein binärer Verkettungsoperator:

IO.puts(<< 0, 1 >> <> << 2, 3 >>)

Der obige Code generiert das folgende Ergebnis:

<< 0, 1, 2, 3 >>

Beachten Sie das Zeichen ł. Da dies utf-8-codiert ist, nimmt diese Zeichendarstellung 2 Bytes ein.

Da jede in einer Binärdatei dargestellte Zahl ein Byte sein soll, wird dieser Wert abgeschnitten, wenn er von 255 aufsteigt. Um dies zu verhindern, verwenden wir den Größenmodifikator, um anzugeben, wie viele Bits diese Zahl annehmen soll. Zum Beispiel -

IO.puts(<< 256 >>) # truncated, it'll print << 0 >>
IO.puts(<< 256 :: size(16) >>) #Takes 16 bits/2 bytes, will print << 1, 0 >>

Das obige Programm generiert das folgende Ergebnis:

<< 0 >>
<< 1, 0 >>

Wir können auch den Modifikator utf8 verwenden. Wenn ein Zeichen ein Codepunkt ist, wird es in der Ausgabe erzeugt. sonst die Bytes -

IO.puts(<< 256 :: utf8 >>)

Das obige Programm generiert das folgende Ergebnis:

Ā

Wir haben auch eine Funktion namens is_binarydas prüft, ob eine gegebene Variable eine Binärdatei ist. Beachten Sie, dass nur Variablen, die als Vielfaches von 8 Bit gespeichert sind, Binärdateien sind.

Bitstrings

Wenn wir eine Binärdatei mit dem Größenmodifikator definieren und ihr einen Wert übergeben, der kein Vielfaches von 8 ist, erhalten wir eine Bitfolge anstelle einer Binärdatei. Zum Beispiel,

bs = << 1 :: size(1) >>
IO.puts(bs)
IO.puts(is_binary(bs))
IO.puts(is_bitstring(bs))

Das obige Programm generiert das folgende Ergebnis:

<< 1::size(1) >>
false
true

Dies bedeutet diese Variable bsist keine Binärdatei, sondern eine Bitfolge. Wir können auch sagen, dass eine Binärdatei eine Bitfolge ist, bei der die Anzahl der Bits durch 8 teilbar ist. Die Mustererkennung funktioniert sowohl für Binärdateien als auch für Bitfolgen auf die gleiche Weise.

Eine Zeichenliste ist nichts anderes als eine Liste von Zeichen. Betrachten Sie das folgende Programm, um dasselbe zu verstehen.

IO.puts('Hello')
IO.puts(is_list('Hello'))

Das obige Programm generiert das folgende Ergebnis:

Hello
true

Anstatt Bytes zu enthalten, enthält eine Zeichenliste die Codepunkte der Zeichen zwischen einfachen Anführungszeichen. So while the double-quotes represent a string (i.e. a binary), singlequotes represent a char list (i.e. a list). Beachten Sie, dass IEx nur Codepunkte als Ausgabe generiert, wenn eines der Zeichen außerhalb des ASCII-Bereichs liegt.

Zeichenlisten werden hauptsächlich bei der Schnittstelle mit Erlang verwendet, insbesondere bei alten Bibliotheken, die keine Binärdateien als Argumente akzeptieren. Sie können eine Zeichenliste in eine Zeichenfolge und zurück konvertieren, indem Sie die Funktionen to_string (char_list) und to_char_list (Zeichenfolge) verwenden.

IO.puts(is_list(to_char_list("hełło")))
IO.puts(is_binary(to_string ('hełło')))

Das obige Programm generiert das folgende Ergebnis:

true
true

NOTE - Die Funktionen to_string und to_char_list sind polymorph, dh sie können mehrere Arten von Eingaben wie Atome, ganze Zahlen annehmen und sie in Zeichenfolgen bzw. Zeichenlisten konvertieren.

(Verknüpfte) Listen

Eine verknüpfte Liste ist eine heterogene Liste von Elementen, die an verschiedenen Stellen im Speicher gespeichert und mithilfe von Referenzen verfolgt werden. Verknüpfte Listen sind Datenstrukturen, die insbesondere in der funktionalen Programmierung verwendet werden.

Elixir verwendet eckige Klammern, um eine Liste von Werten anzugeben. Werte können von jedem Typ sein -

[1, 2, true, 3]

Wenn Elixir eine Liste druckbarer ASCII-Nummern sieht, druckt Elixir diese als Zeichenliste (buchstäblich als Liste von Zeichen). Immer wenn Sie einen Wert in IEx sehen und nicht sicher sind, was er ist, können Sie den verwendeni Funktion zum Abrufen von Informationen darüber.

IO.puts([104, 101, 108, 108, 111])

Die obigen Zeichen in der Liste sind alle druckbar. Wenn das obige Programm ausgeführt wird, wird das folgende Ergebnis erzeugt:

hello

Sie können Listen auch umgekehrt in einfachen Anführungszeichen definieren -

IO.puts(is_list('Hello'))

Wenn das obige Programm ausgeführt wird, wird das folgende Ergebnis erzeugt:

true

Beachten Sie, dass Darstellungen in einfachen und doppelten Anführungszeichen in Elixir nicht gleichwertig sind, da sie durch verschiedene Typen dargestellt werden.

Länge einer Liste

Um die Länge einer Liste zu ermitteln, verwenden wir die Längenfunktion wie im folgenden Programm:

IO.puts(length([1, 2, :true, "str"]))

Das obige Programm generiert das folgende Ergebnis:

4

Verkettung und Subtraktion

Mit dem können zwei Listen verkettet und subtrahiert werden ++ und --Betreiber. Betrachten Sie das folgende Beispiel, um die Funktionen zu verstehen.

IO.puts([1, 2, 3] ++ [4, 5, 6])
IO.puts([1, true, 2, false, 3, true] -- [true, false])

Dadurch erhalten Sie im ersten Fall eine verkettete Zeichenfolge und im zweiten eine subtrahierte Zeichenfolge. Das obige Programm generiert das folgende Ergebnis:

[1, 2, 3, 4, 5, 6]
[1, 2, 3, true]

Kopf und Schwanz einer Liste

Der Kopf ist das erste Element einer Liste und der Schwanz ist der Rest einer Liste. Sie können mit den Funktionen abgerufen werdenhd und tl. Lassen Sie uns einer Variablen eine Liste zuweisen und ihren Kopf und Schwanz abrufen.

list = [1, 2, 3]
IO.puts(hd(list))
IO.puts(tl(list))

Dies gibt uns den Kopf und das Ende der Liste als Ausgabe. Das obige Programm generiert das folgende Ergebnis:

1
[2, 3]

Note - Das Abrufen des Kopfes oder des Endes einer leeren Liste ist ein Fehler.

Andere Listenfunktionen

Die Elixir-Standardbibliothek bietet eine Vielzahl von Funktionen für den Umgang mit Listen. Wir werden uns einige davon hier ansehen. Den Rest können Sie hier auflisten .

S.no. Funktionsname und Beschreibung
1

delete(list, item)

Löscht das angegebene Element aus der Liste. Gibt eine Liste ohne das Element zurück. Wenn das Element mehrmals in der Liste vorkommt, wird nur das erste Vorkommen entfernt.

2

delete_at(list, index)

Erstellt eine neue Liste, indem der Wert am angegebenen Index entfernt wird. Negative Indizes geben einen Versatz vom Ende der Liste an. Wenn der Index außerhalb der Grenzen liegt, wird die ursprüngliche Liste zurückgegeben.

3

first(list)

Gibt das erste Element in der Liste zurück oder nil, wenn die Liste leer ist.

4

flatten(list)

Glättet die angegebene Liste verschachtelter Listen.

5

insert_at(list, index, value)

Gibt eine Liste mit einem Wert zurück, der am angegebenen Index eingefügt wurde. Beachten Sie, dass der Index auf die Listenlänge begrenzt ist. Negative Indizes geben einen Versatz vom Ende der Liste an.

6

last(list)

Gibt das letzte Element in der Liste zurück oder nil, wenn die Liste leer ist.

Tupel

Tupel sind auch Datenstrukturen, in denen eine Reihe anderer Strukturen gespeichert sind. Im Gegensatz zu Listen speichern sie Elemente in einem zusammenhängenden Speicherblock. Dies bedeutet, dass der Zugriff auf ein Tupelelement pro Index oder das Abrufen der Tupelgröße ein schneller Vorgang ist. Indizes beginnen bei Null.

Elixir verwendet geschweifte Klammern, um Tupel zu definieren. Tupel können wie Listen jeden Wert enthalten -

{:ok, "hello"}

Länge eines Tupels

Verwenden Sie die Taste, um die Länge eines Tupels zu ermitteln tuple_size Funktion wie im folgenden Programm -

IO.puts(tuple_size({:ok, "hello"}))

Das obige Programm generiert das folgende Ergebnis:

2

Wert anhängen

Verwenden Sie die Funktion Tuple.append, um einen Wert an das Tupel anzuhängen.

tuple = {:ok, "Hello"}
Tuple.append(tuple, :world)

Dadurch wird ein neues Tupel erstellt und zurückgegeben: {: ok, "Hallo" ,: Welt}

Einfügen eines Wertes

Um einen Wert an einer bestimmten Position einzufügen, können Sie entweder die verwenden Tuple.insert_at Funktion oder die put_elemFunktion. Betrachten Sie das folgende Beispiel, um dasselbe zu verstehen:

tuple = {:bar, :baz}
new_tuple_1 = Tuple.insert_at(tuple, 0, :foo)
new_tuple_2 = put_elem(tuple, 1, :foobar)

Beachte das put_elem und insert_atgab neue Tupel zurück. Das in der Tupelvariablen gespeicherte ursprüngliche Tupel wurde nicht geändert, da die Elixir-Datentypen unveränderlich sind. Da Elixir-Code unveränderlich ist, ist es einfacher, darüber nachzudenken, da Sie sich keine Sorgen machen müssen, wenn ein bestimmter Code Ihre Datenstruktur verändert.

Tupel gegen Listen

Was ist der Unterschied zwischen Listen und Tupeln?

Listen werden als verknüpfte Listen gespeichert, dh jedes Element in einer Liste behält seinen Wert und zeigt auf das folgende Element, bis das Ende der Liste erreicht ist. Wir nennen jedes Wertepaar und jeden Zeiger eine Nachteilezelle. Dies bedeutet, dass der Zugriff auf die Länge einer Liste eine lineare Operation ist: Wir müssen die gesamte Liste durchlaufen, um ihre Größe herauszufinden. Das Aktualisieren einer Liste ist schnell, solange Elemente vorangestellt werden.

Tupel hingegen werden zusammenhängend im Speicher gespeichert. Dies bedeutet, dass das Abrufen der Tupelgröße oder der Zugriff auf ein Element über den Index schnell erfolgt. Das Aktualisieren oder Hinzufügen von Elementen zu Tupeln ist jedoch teuer, da das gesamte Tupel im Speicher kopiert werden muss.

Bisher haben wir keine assoziativen Datenstrukturen erörtert, dh Datenstrukturen, die einem Schlüssel einen bestimmten Wert (oder mehrere Werte) zuordnen können. Verschiedene Sprachen nennen diese Funktionen mit unterschiedlichen Namen wie Wörterbüchern, Hashes, assoziativen Arrays usw.

In Elixir haben wir zwei assoziative Hauptdatenstrukturen: Schlüsselwortlisten und Karten. In diesem Kapitel konzentrieren wir uns auf Keyword-Listen.

In vielen funktionalen Programmiersprachen ist es üblich, eine Liste von Tupeln mit zwei Elementen als Darstellung einer assoziativen Datenstruktur zu verwenden. Wenn wir in Elixir eine Liste von Tupeln haben und das erste Element des Tupels (dh der Schlüssel) ein Atom ist, nennen wir es eine Schlüsselwortliste. Betrachten Sie das folgende Beispiel, um dasselbe zu verstehen:

list = [{:a, 1}, {:b, 2}]

Elixir unterstützt eine spezielle Syntax zum Definieren solcher Listen. Wir können den Doppelpunkt am Ende jedes Atoms platzieren und die Tupel vollständig entfernen. Zum Beispiel,

list_1 = [{:a, 1}, {:b, 2}]
list_2 = [a: 1, b: 2]
IO.puts(list_1 == list_2)

Das obige Programm generiert das folgende Ergebnis:

true

Beide repräsentieren eine Keyword-Liste. Da Schlüsselwortlisten auch Listen sind, können wir alle Operationen verwenden, die wir für Listen auf ihnen verwendet haben.

Um den einem Atom in der Schlüsselwortliste zugeordneten Wert abzurufen, übergeben Sie das Atom als [] nach dem Namen der Liste -

list = [a: 1, b: 2]
IO.puts(list[:a])

Das obige Programm generiert das folgende Ergebnis:

1

Keyword-Listen weisen drei Besonderheiten auf:

  • Schlüssel müssen Atome sein.
  • Die Schlüssel werden gemäß den Angaben des Entwicklers bestellt.
  • Schlüssel können mehrmals vergeben werden.

Um Keyword-Listen zu bearbeiten, stellt Elixir das Keyword-Modul zur Verfügung . Denken Sie jedoch daran, dass Keyword-Listen einfach Listen sind und als solche dieselben linearen Leistungsmerkmale wie Listen bieten. Je länger die Liste ist, desto länger dauert es, einen Schlüssel zu finden, die Anzahl der Elemente zu zählen usw. Aus diesem Grund werden Keyword-Listen in Elixir hauptsächlich als Optionen verwendet. Wenn Sie viele Elemente speichern oder Mitarbeiter mit einem Schlüssel mit einem maximalen Wert von einem Wert garantieren müssen, sollten Sie stattdessen Karten verwenden.

Zugriff auf einen Schlüssel

Um auf Werte zuzugreifen, die einem bestimmten Schlüssel zugeordnet sind, verwenden wir die Keyword.getFunktion. Es gibt den ersten Wert zurück, der dem angegebenen Schlüssel zugeordnet ist. Um alle Werte abzurufen, verwenden wir die Funktion Keyword.get_values. Zum Beispiel -

kl = [a: 1, a: 2, b: 3] 
IO.puts(Keyword.get(kl, :a)) 
IO.puts(Keyword.get_values(kl))

Das obige Programm generiert das folgende Ergebnis:

1
[1, 2]

Schlüssel einstecken

Verwenden Sie, um einen neuen Wert hinzuzufügen Keyword.put_new. Wenn der Schlüssel bereits vorhanden ist, bleibt sein Wert unverändert -

kl = [a: 1, a: 2, b: 3]
kl_new = Keyword.put_new(kl, :c, 5)
IO.puts(Keyword.get(kl_new, :c))

Wenn das obige Programm ausgeführt wird, erstellt es eine neue Schlüsselwortliste mit dem zusätzlichen Schlüssel c und generiert das folgende Ergebnis:

5

Schlüssel löschen

Wenn Sie alle Einträge für einen Schlüssel löschen möchten, verwenden Sie Keyword.delete; Verwenden Sie, um nur den ersten Eintrag für einen Schlüssel zu löschen Keyword.delete_first.

kl = [a: 1, a: 2, b: 3, c: 0]
kl = Keyword.delete_first(kl, :b)
kl = Keyword.delete(kl, :a)

IO.puts(Keyword.get(kl, :a))
IO.puts(Keyword.get(kl, :b))
IO.puts(Keyword.get(kl, :c))

Dadurch wird der erste gelöscht b in der Liste und alle aIn der Liste. Wenn das obige Programm ausgeführt wird, wird das folgende Ergebnis generiert:

0

Schlüsselwortlisten sind eine bequeme Möglichkeit, Inhalte, die in Listen gespeichert sind, nach Schlüsseln zu adressieren. Darunter geht Elixir jedoch immer noch durch die Liste. Dies ist möglicherweise geeignet, wenn Sie andere Pläne für diese Liste haben, bei denen Sie alles durchgehen müssen. Es kann jedoch ein unnötiger Aufwand sein, wenn Sie Schlüssel als einzigen Ansatz für die Daten verwenden möchten.

Hier kommen Karten zu Ihrer Rettung. Wann immer Sie einen Schlüsselwertspeicher benötigen, sind Maps die Datenstruktur in Elixir.

Karte erstellen

Eine Map wird mit der% {} -Syntax erstellt -

map = %{:a => 1, 2 => :b}

Im Vergleich zu den Keyword-Listen sehen wir bereits zwei Unterschiede:

  • Karten erlauben jeden Wert als Schlüssel.
  • Die Schlüssel der Karten folgen keiner Reihenfolge.

Zugriff auf einen Schlüssel

Um auf den mit einem Schlüssel verknüpften Wert zuzugreifen, verwenden Maps dieselbe Syntax wie Keyword-Listen.

map = %{:a => 1, 2 => :b}
IO.puts(map[:a])
IO.puts(map[2])

Wenn das obige Programm ausgeführt wird, generiert es das folgende Ergebnis:

1
b

Schlüssel einstecken

Um einen Schlüssel in eine Karte einzufügen, verwenden wir die Dict.put_new Funktion, die die Karte, den neuen Schlüssel und den neuen Wert als Argumente verwendet -

map = %{:a => 1, 2 => :b}
new_map = Dict.put_new(map, :new_val, "value") 
IO.puts(new_map[:new_val])

Dadurch wird das Schlüssel-Wert-Paar eingefügt :new_val - "value"in einer neuen Karte. Wenn das obige Programm ausgeführt wird, generiert es das folgende Ergebnis:

"value"

Einen Wert aktualisieren

Um einen bereits in der Karte vorhandenen Wert zu aktualisieren, können Sie die folgende Syntax verwenden:

map = %{:a => 1, 2 => :b}
new_map = %{ map | a: 25}
IO.puts(new_map[:a])

Wenn das obige Programm ausgeführt wird, generiert es das folgende Ergebnis:

25

Mustervergleich

Im Gegensatz zu Keyword-Listen sind Karten beim Pattern Matching sehr nützlich. Wenn eine Karte in einem Muster verwendet wird, stimmt sie immer mit einer Teilmenge des angegebenen Werts überein.

%{:a => a} = %{:a => 1, 2 => :b}
IO.puts(a)

Das obige Programm generiert das folgende Ergebnis:

1

Dies wird übereinstimmen a mit 1. Und daher wird die Ausgabe als generiert1.

Wie oben gezeigt, stimmt eine Karte überein, solange die Schlüssel im Muster in der angegebenen Karte vorhanden sind. Daher stimmt eine leere Karte mit allen Karten überein.

Variablen können beim Zugreifen auf, Abgleichen und Hinzufügen von Kartenschlüsseln verwendet werden -

n = 1
map = %{n => :one}
%{^n => :one} = %{1 => :one, 2 => :two, 3 => :three}

Das Kartenmodul bietet eine dem Schlüsselwortmodul sehr ähnliche API mit praktischen Funktionen zum Bearbeiten von Karten. Sie können Funktionen wie die verwendenMap.get, Map.delete, um Karten zu manipulieren.

Karten mit Atomtasten

Karten haben einige interessante Eigenschaften. Wenn alle Schlüssel in einer Karte Atome sind, können Sie der Einfachheit halber die Schlüsselwortsyntax verwenden.

map = %{:a => 1, 2 => :b} 
IO.puts(map.a)

Eine weitere interessante Eigenschaft von Maps ist, dass sie eine eigene Syntax für die Aktualisierung und den Zugriff auf Atomschlüssel bereitstellen.

map = %{:a => 1, 2 => :b}
IO.puts(map.a)

Das obige Programm generiert das folgende Ergebnis:

1

Beachten Sie, dass der Zugriff auf Atomschlüssel auf diese Weise vorhanden sein sollte, da das Programm sonst nicht funktioniert.

In Elixir gruppieren wir mehrere Funktionen in Module. In den vorherigen Kapiteln haben wir bereits verschiedene Module verwendet, z. B. das String-Modul, das Bitwise-Modul, das Tuple-Modul usw.

Um unsere eigenen Module in Elixir zu erstellen, verwenden wir die defmoduleMakro. Wir nehmen dasdef Makro zum Definieren von Funktionen in diesem Modul -

defmodule Math do
   def sum(a, b) do
      a + b
   end
end

In den folgenden Abschnitten werden unsere Beispiele länger und es kann schwierig sein, sie alle in die Shell einzugeben. Wir müssen lernen, wie man Elixir-Code kompiliert und wie man Elixir-Skripte ausführt.

Zusammenstellung

Es ist immer bequem, Module in Dateien zu schreiben, damit sie kompiliert und wiederverwendet werden können. Nehmen wir an, wir haben eine Datei namens math.ex mit folgendem Inhalt:

defmodule Math do
   def sum(a, b) do
      a + b
   end
end

Wir können die Dateien mit dem Befehl kompilieren -elixirc ::

$ elixirc math.ex

Dadurch wird eine Datei mit dem Namen generiert Elixir.Math.beamenthält den Bytecode für das definierte Modul. Wenn wir anfangeniexAuch hier ist unsere Moduldefinition verfügbar (vorausgesetzt, iex wird in demselben Verzeichnis gestartet, in dem sich die Bytecode-Datei befindet). Zum Beispiel,

IO.puts(Math.sum(1, 2))

Das obige Programm generiert das folgende Ergebnis:

3

Skriptmodus

Zusätzlich zur Elixir-Dateierweiterung .exElixir unterstützt auch .exsDateien für die Skripterstellung. Elixir behandelt beide Dateien genauso, der einzige Unterschied liegt im Ziel..ex Dateien sollen kompiliert werden, während .exs-Dateien für verwendet werden scripting. Bei der Ausführung kompilieren beide Erweiterungen ihre Module und laden sie in den Speicher, allerdings nur.ex Dateien schreiben ihren Bytecode im Format von .beam-Dateien auf die Festplatte.

Zum Beispiel, wenn wir das ausführen wollten Math.sum In derselben Datei können wir die EXE-Datei folgendermaßen verwenden:

Math.exs

defmodule Math do
   def sum(a, b) do
      a + b
   end
end
IO.puts(Math.sum(1, 2))

Wir können es mit dem Elixir-Befehl ausführen -

$ elixir math.exs

Das obige Programm generiert das folgende Ergebnis:

3

Die Datei wird im Speicher kompiliert und ausgeführt, wobei als Ergebnis „3“ gedruckt wird. Es wird keine Bytecode-Datei erstellt.

Modulverschachtelung

Module können in Elixir verschachtelt werden. Diese Funktion der Sprache hilft uns, unseren Code besser zu organisieren. Um verschachtelte Module zu erstellen, verwenden wir die folgende Syntax:

defmodule Foo do
   #Foo module code here
   defmodule Bar do
      #Bar module code here
   end
end

Das oben angegebene Beispiel definiert zwei Module: Foo und Foo.Bar. Auf die zweite kann als zugegriffen werdenBar Innerhalb Foosolange sie sich im selben lexikalischen Bereich befinden. Wenn später dieBar Das Modul wird außerhalb der Foo-Moduldefinition verschoben. Es muss mit seinem vollständigen Namen (Foo.Bar) referenziert werden, oder ein Alias ​​muss mithilfe der im Alias-Kapitel beschriebenen Alias-Direktive festgelegt werden.

Note- In Elixir muss das Foo-Modul nicht definiert werden, um das Foo.Bar-Modul zu definieren, da die Sprache alle Modulnamen in Atome übersetzt. Sie können beliebige Module definieren, ohne ein Modul in der Kette zu definieren. Zum Beispiel können Sie definierenFoo.Bar.Baz ohne zu definieren Foo oder Foo.Bar.

Um die Wiederverwendung von Software zu erleichtern, bietet Elixir drei Anweisungen: alias, require und import. Es bietet auch ein Makro namens use, das unten zusammengefasst ist -

# Alias the module so it can be called as Bar instead of Foo.Bar
alias Foo.Bar, as: Bar

# Ensure the module is compiled and available (usually for macros)
require Foo

# Import functions from Foo so they can be called without the `Foo.` prefix
import Foo

# Invokes the custom code defined in Foo as an extension point
use Foo

Lassen Sie uns nun jede Richtlinie im Detail verstehen.

alias

Mit der Alias-Direktive können Sie Aliase für einen bestimmten Modulnamen einrichten. Zum Beispiel, wenn Sie einen Alias ​​angeben möchten'Str' In das String-Modul können Sie einfach schreiben -

alias String, as: Str
IO.puts(Str.length("Hello"))

Das obige Programm generiert das folgende Ergebnis:

5

Ein Alias ​​wird dem gegeben String Modul als Str. Wenn wir nun eine Funktion mit dem Str-Literal aufrufen, verweist sie tatsächlich auf dasStringModul. Dies ist sehr hilfreich, wenn wir sehr lange Modulnamen verwenden und diese im aktuellen Bereich durch kürzere ersetzen möchten.

NOTE - Aliase MUST Beginnen Sie mit einem Großbuchstaben.

Aliase sind nur innerhalb der gültig lexical scope Sie werden aufgerufen. Wenn Sie beispielsweise zwei Module in einer Datei haben und einen Alias ​​in einem der Module erstellen, kann auf diesen Alias ​​im zweiten Modul nicht zugegriffen werden.

Wenn Sie den Namen eines eingebauten Moduls wie String oder Tuple als Alias ​​für ein anderes Modul angeben, um auf das eingebaute Modul zuzugreifen, müssen Sie ihm ein vorangestelltes Modul hinzufügen "Elixir.". Zum Beispiel,

alias List, as: String
#Now when we use String we are actually using List.
#To use the string module: 
IO.puts(Elixir.String.length("Hello"))

Wenn das obige Programm ausgeführt wird, generiert es das folgende Ergebnis:

5

benötigen

Elixir bietet Makros als Mechanismus für die Metaprogrammierung (Schreiben von Code, der Code generiert).

Makros sind Codestücke, die zur Kompilierungszeit ausgeführt und erweitert werden. Das heißt, um ein Makro verwenden zu können, müssen wir sicherstellen, dass sein Modul und seine Implementierung während der Kompilierung verfügbar sind. Dies geschieht mit demrequire Richtlinie.

Integer.is_odd(3)

Wenn das obige Programm ausgeführt wird, wird das folgende Ergebnis generiert:

** (CompileError) iex:1: you must require Integer before invoking the macro Integer.is_odd/1

In Elixir, Integer.is_odd ist definiert als macro. Dieses Makro kann als Schutz verwendet werden. Dies bedeutet, dass, um aufzurufenInteger.is_oddbenötigen wir das Integer-Modul.

Verwenden Sie die require Integer Funktion und führen Sie das Programm wie unten gezeigt aus.

require Integer
Integer.is_odd(3)

Dieses Mal wird das Programm ausgeführt und die Ausgabe wie folgt erzeugt: true.

Im Allgemeinen ist ein Modul vor der Verwendung nicht erforderlich, es sei denn, wir möchten die in diesem Modul verfügbaren Makros verwenden. Ein Versuch, ein Makro aufzurufen, das nicht geladen wurde, führt zu einem Fehler. Beachten Sie, dass require wie die Alias-Direktive auch lexikalisch ist . Wir werden in einem späteren Kapitel mehr über Makros sprechen.

importieren

Wir nehmen das importAnweisung zum einfachen Zugriff auf Funktionen oder Makros von anderen Modulen ohne Verwendung des vollständig qualifizierten Namens. Zum Beispiel, wenn wir die verwenden möchtenduplicate Funktion aus dem List-Modul mehrmals, können wir es einfach importieren.

import List, only: [duplicate: 2]

In diesem Fall importieren wir nur das Funktionsduplikat (mit Argumentlistenlänge 2) aus List. Obwohl:only ist optional, wird die Verwendung empfohlen, um zu vermeiden, dass alle Funktionen eines bestimmten Moduls in den Namespace importiert werden. :except kann auch als Option angegeben werden, um alles in ein Modul außer einer Liste von Funktionen zu importieren.

Das import Richtlinie unterstützt auch :macros und :functions gegeben werden :only. Um beispielsweise alle Makros zu importieren, kann ein Benutzer schreiben:

import Integer, only: :macros

Beachten Sie, dass auch der Import ist Lexically scopedgenau wie die Direktiven require und alias. Beachten Sie auch das'import'ing a module also 'require's it.

verwenden

Obwohl keine Richtlinie, use ist ein Makro, das eng mit verwandt ist requireDamit können Sie ein Modul im aktuellen Kontext verwenden. Das Use-Makro wird häufig von Entwicklern verwendet, um externe Funktionen in den aktuellen lexikalischen Bereich zu integrieren, häufig Module. Lassen Sie uns die Verwendungsrichtlinie anhand eines Beispiels verstehen -

defmodule Example do 
   use Feature, option: :value 
end

Use ist ein Makro, das das Obige in - umwandelt.

defmodule Example do
   require Feature
   Feature.__using__(option: :value)
end

Das use Module benötigt zuerst das Modul und ruft dann das auf __using__Makro auf Modul. Elixir verfügt über hervorragende Metaprogrammierungsfunktionen und Makros zum Generieren von Code zur Kompilierungszeit. Das Makro _ _using__ wird in der obigen Instanz aufgerufen, und der Code wird in unseren lokalen Kontext eingefügt . Im lokalen Kontext wurde das Verwendungsmakro zum Zeitpunkt der Kompilierung aufgerufen.

Eine Funktion ist eine Reihe von Anweisungen, die zusammen organisiert sind, um eine bestimmte Aufgabe auszuführen. Funktionen in der Programmierung funktionieren meistens wie Funktionen in Mathe. Sie geben Funktionen eine Eingabe, sie generieren eine Ausgabe basierend auf der bereitgestellten Eingabe.

Es gibt 2 Arten von Funktionen in Elixir -

Anonyme Funktion

Mit dem definierte Funktionen fn..end constructsind anonyme Funktionen. Diese Funktionen werden manchmal auch als Lambdas bezeichnet. Sie werden verwendet, indem sie Variablennamen zugewiesen werden.

Benannte Funktion

Mit dem definierte Funktionen def keywordsind benannte Funktionen. Dies sind native Funktionen, die in Elixir bereitgestellt werden.

Anonyme Funktionen

Wie der Name schon sagt, hat eine anonyme Funktion keinen Namen. Diese werden häufig an andere Funktionen übergeben. Um eine anonyme Funktion in Elixir zu definieren, benötigen wir diefn und endSchlüsselwörter. Innerhalb dieser können wir eine beliebige Anzahl von Parametern und Funktionskörpern definieren, die durch getrennt sind->. Zum Beispiel,

sum = fn (a, b) -> a + b end
IO.puts(sum.(1, 5))

Wenn das oben genannte Programm ausgeführt wird, wird das folgende Ergebnis generiert:

6

Beachten Sie, dass diese Funktionen nicht wie die genannten Funktionen aufgerufen werden. Wir haben ein '.'zwischen dem Funktionsnamen und seinen Argumenten.

Verwenden des Capture-Operators

Wir können diese Funktionen auch mit dem Erfassungsoperator definieren. Dies ist eine einfachere Methode zum Erstellen von Funktionen. Wir werden nun die obige Summenfunktion mit dem Erfassungsoperator definieren.

sum = &(&1 + &2) 
IO.puts(sum.(1, 2))

Wenn das obige Programm ausgeführt wird, generiert es das folgende Ergebnis:

3

In der Kurzversion werden unsere Parameter nicht benannt, sondern stehen uns als & 1, & 2, & 3 usw. zur Verfügung.

Pattern Matching-Funktionen

Der Mustervergleich ist nicht nur auf Variablen und Datenstrukturen beschränkt. Wir können Pattern Matching verwenden, um unsere Funktionen polymorph zu machen. Zum Beispiel werden wir eine Funktion deklarieren, die entweder 1 oder 2 Eingaben (innerhalb eines Tupels) annehmen und diese auf der Konsole drucken kann.

handle_result = fn
   {var1} -> IO.puts("#{var1} found in a tuple!")
   {var_2, var_3} -> IO.puts("#{var_2} and #{var_3} found!")
end
handle_result.({"Hey people"})
handle_result.({"Hello", "World"})

Wenn das obige Programm ausgeführt wird, wird das folgende Ergebnis erzeugt:

Hey people found in a tuple!
Hello and World found!

Benannte Funktionen

Wir können Funktionen mit Namen definieren, damit wir später leicht darauf verweisen können. Benannte Funktionen werden innerhalb eines Moduls mit dem Schlüsselwort def definiert. Benannte Funktionen werden immer in einem Modul definiert. Um benannte Funktionen aufzurufen, müssen wir sie mit ihrem Modulnamen referenzieren.

Das Folgende ist die Syntax für benannte Funktionen -

def function_name(argument_1, argument_2) do
   #code to be executed when function is called
end

Definieren wir nun unsere benannte Funktionssumme im Math-Modul.

defmodule Math do
   def sum(a, b) do
      a + b
   end
end

IO.puts(Math.sum(5, 6))

Wenn das obige Programm ausgeführt wird, wird folgendes Ergebnis erzielt:

11

Für 1-Liner-Funktionen gibt es eine Kurzschreibweise, um diese Funktionen mithilfe von zu definieren do:. Zum Beispiel -

defmodule Math do
   def sum(a, b), do: a + b
end
IO.puts(Math.sum(5, 6))

Wenn das obige Programm ausgeführt wird, wird folgendes Ergebnis erzielt:

11

Private Funktionen

Elixir bietet uns die Möglichkeit, private Funktionen zu definieren, auf die innerhalb des Moduls zugegriffen werden kann, in dem sie definiert sind. Verwenden Sie zum Definieren einer privaten Funktiondefp Anstatt von def. Zum Beispiel,

defmodule Greeter do
   def hello(name), do: phrase <> name
   defp phrase, do: "Hello "
end

Greeter.hello("world")

Wenn das obige Programm ausgeführt wird, wird das folgende Ergebnis erzeugt:

Hello world

Aber wenn wir nur versuchen, die Phrasenfunktion explizit aufzurufen, verwenden Sie die Greeter.phrase() Funktion wird ein Fehler ausgelöst.

Standardargumente

Wenn wir einen Standardwert für ein Argument wünschen, verwenden wir das argument \\ value Syntax -

defmodule Greeter do
   def hello(name, country \\ "en") do
      phrase(country) <> name
   end

   defp phrase("en"), do: "Hello, "
   defp phrase("es"), do: "Hola, "
end

Greeter.hello("Ayush", "en")
Greeter.hello("Ayush")
Greeter.hello("Ayush", "es")

Wenn das obige Programm ausgeführt wird, wird das folgende Ergebnis erzeugt:

Hello, Ayush
Hello, Ayush
Hola, Ayush

Rekursion ist eine Methode, bei der die Lösung eines Problems von den Lösungen für kleinere Instanzen desselben Problems abhängt. Die meisten Computerprogrammiersprachen unterstützen die Rekursion, indem sie einer Funktion erlauben, sich innerhalb des Programmtextes aufzurufen.

Idealerweise haben rekursive Funktionen eine Endbedingung. Diese Endbedingung, auch als Basisfall bezeichnet, beendet die erneute Eingabe der Funktion und das Hinzufügen von Funktionsaufrufen zum Stapel. Hier stoppt der rekursive Funktionsaufruf. Betrachten wir das folgende Beispiel, um die rekursive Funktion besser zu verstehen.

defmodule Math do
   def fact(res, num) do
   if num === 1 do
      res
   else
      new_res = res * num
      fact(new_res, num-1)
      end
   end
end

IO.puts(Math.fact(1,5))

Wenn das obige Programm ausgeführt wird, generiert es das folgende Ergebnis:

120

Also in der obigen Funktion, Math.factberechnen wir die Fakultät einer Zahl. Beachten Sie, dass wir die Funktion in sich selbst aufrufen. Lassen Sie uns jetzt verstehen, wie das funktioniert.

Wir haben es mit 1 und der Zahl versehen, deren Fakultät wir berechnen möchten. Die Funktion prüft, ob die Zahl 1 ist oder nicht und gibt res zurück, wenn sie 1 ist(Ending condition). Wenn nicht, erstellt es eine Variable new_res und weist ihr den Wert der vorherigen res * current num zu. Es gibt den Wert zurück, der von unserem Funktionsaufruf fact (new_res, num-1) zurückgegeben wird . Dies wiederholt sich, bis wir num als 1 erhalten. Sobald dies geschieht, erhalten wir das Ergebnis.

Betrachten wir ein anderes Beispiel, bei dem jedes Element der Liste einzeln gedruckt wird. Dazu verwenden wir diehd und tl Funktionen von Listen und Mustervergleich in Funktionen -

a = ["Hey", 100, 452, :true, "People"]
defmodule ListPrint do
   def print([]) do
   end
   def print([head | tail]) do 
      IO.puts(head)
      print(tail)
   end
end

ListPrint.print(a)

Die erste Druckfunktion wird aufgerufen, wenn eine leere Liste vorliegt(ending condition). Wenn nicht, wird die zweite Druckfunktion aufgerufen, die die Liste in 2 teilt und das erste Element der Liste dem Kopf und das verbleibende Element der Liste dem Ende zuweist. Der Kopf wird dann gedruckt und wir rufen die Druckfunktion mit dem Rest der Liste, dh dem Schwanz, erneut auf. Wenn das obige Programm ausgeführt wird, wird das folgende Ergebnis erzeugt:

Hey
100
452
true
People

Aufgrund der Unveränderlichkeit werden Schleifen in Elixir (wie in jeder funktionalen Programmiersprache) anders geschrieben als imperative Sprachen. Zum Beispiel werden Sie in einer imperativen Sprache wie C schreiben -

for(i = 0; i < 10; i++) {
   printf("%d", array[i]);
}

In dem oben angegebenen Beispiel mutieren wir sowohl das Array als auch die Variable i. Eine Mutation ist in Elixir nicht möglich. Stattdessen basieren funktionale Sprachen auf Rekursion: Eine Funktion wird rekursiv aufgerufen, bis eine Bedingung erreicht ist, die die Fortsetzung der rekursiven Aktion verhindert. Dabei werden keine Daten mutiert.

Schreiben wir nun eine einfache Schleife mit Rekursion, die Hallo druckt n mal.

defmodule Loop do
   def print_multiple_times(msg, n) when n <= 1 do
      IO.puts msg
   end

   def print_multiple_times(msg, n) do
      IO.puts msg
      print_multiple_times(msg, n - 1)
   end
end

Loop.print_multiple_times("Hello", 10)

Wenn das obige Programm ausgeführt wird, wird das folgende Ergebnis erzeugt:

Hello
Hello
Hello
Hello
Hello
Hello
Hello
Hello
Hello
Hello

Wir haben die Musteranpassungstechniken und die Rekursion der Funktion verwendet, um eine Schleife erfolgreich zu implementieren. Rekursive Definitionen sind schwer zu verstehen, aber das Konvertieren von Schleifen in Rekursion ist einfach.

Elixier bietet uns die Enum module. Dieses Modul wird für die iterativsten Schleifenaufrufe verwendet, da es viel einfacher ist, diese zu verwenden, als zu versuchen, rekursive Definitionen für dieselben herauszufinden. Wir werden diese im nächsten Kapitel diskutieren. Ihre eigenen rekursiven Definitionen sollten nur verwendet werden, wenn Sie mit diesem Modul keine Lösung finden. Diese Funktionen sind Tail Call-optimiert und recht schnell.

Eine Aufzählung ist ein Objekt, das aufgezählt werden kann. "Aufgezählt" bedeutet, die Mitglieder einer Menge / Sammlung / Kategorie einzeln zu zählen (normalerweise in der Reihenfolge, normalerweise nach Namen).

Elixir bietet das Konzept der Aufzählungen und das Enum-Modul , um mit ihnen zu arbeiten. Die Funktionen im Enum-Modul beschränken sich, wie der Name schon sagt, auf die Aufzählung von Werten in Datenstrukturen. Ein Beispiel für eine aufzählbare Datenstruktur ist eine Liste, ein Tupel, eine Karte usw. Das Enum-Modul bietet uns etwas mehr als 100 Funktionen für die Aufzählung. In diesem Kapitel werden einige wichtige Funktionen erläutert.

Alle diese Funktionen nehmen eine Aufzählung als erstes Element und eine Funktion als zweites und arbeiten daran. Die Funktionen werden unten beschrieben.

alle?

Wenn wir verwenden all? Funktion muss die gesamte Sammlung als wahr ausgewertet werden, andernfalls wird false zurückgegeben. Überprüfen Sie beispielsweise, ob alle Elemente in der Liste ungerade Zahlen sind.

res = Enum.all?([1, 2, 3, 4], fn(s) -> rem(s,2) == 1 end) 
IO.puts(res)

Wenn das obige Programm ausgeführt wird, wird das folgende Ergebnis erzeugt:

false

Dies liegt daran, dass nicht alle Elemente dieser Liste ungerade sind.

irgendein?

Wie der Name schon sagt, gibt diese Funktion true zurück, wenn ein Element der Sammlung true ergibt. Zum Beispiel -

res = Enum.any?([1, 2, 3, 4], fn(s) -> rem(s,2) == 1 end)
IO.puts(res)

Wenn das obige Programm ausgeführt wird, wird das folgende Ergebnis erzeugt:

true

Stück

Diese Funktion unterteilt unsere Sammlung in kleine Teile der Größe, die als zweites Argument angegeben wurde. Zum Beispiel -

res = Enum.chunk([1, 2, 3, 4, 5, 6], 2)
IO.puts(res)

Wenn das obige Programm ausgeführt wird, wird das folgende Ergebnis erzeugt:

[[1, 2], [3, 4], [5, 6]]

jeder

Es kann notwendig sein, eine Sammlung zu durchlaufen, ohne einen neuen Wert zu erzeugen. In diesem Fall verwenden wir die each Funktion -

Enum.each(["Hello", "Every", "one"], fn(s) -> IO.puts(s) end)

Wenn das obige Programm ausgeführt wird, wird das folgende Ergebnis erzeugt:

Hello
Every
one

Karte

Um unsere Funktion auf jeden Artikel anzuwenden und eine neue Sammlung zu erstellen, verwenden wir die Kartenfunktion. Es ist eines der nützlichsten Konstrukte in der funktionalen Programmierung, da es sehr ausdrucksstark und kurz ist. Betrachten wir ein Beispiel, um dies zu verstehen. Wir werden die in einer Liste gespeicherten Werte verdoppeln und in einer neuen Liste speichernres - -

res = Enum.map([2, 5, 3, 6], fn(a) -> a*2 end)
IO.puts(res)

Wenn das obige Programm ausgeführt wird, wird das folgende Ergebnis erzeugt:

[4, 10, 6, 12]

reduzieren

Das reduceFunktion hilft uns, unsere Aufzählung auf einen einzigen Wert zu reduzieren. Zu diesem Zweck stellen wir einen optionalen Akkumulator (in diesem Beispiel 5) zur Verfügung, der an unsere Funktion übergeben wird. Wenn kein Akkumulator vorhanden ist, wird der erste Wert verwendet -

res = Enum.reduce([1, 2, 3, 4], 5, fn(x, accum) -> x + accum end)
IO.puts(res)

Wenn das obige Programm ausgeführt wird, wird das folgende Ergebnis erzeugt:

15

Der Akkumulator ist der Anfangswert, der an den übergeben wird fn. Ab dem zweiten Anruf wird der vom vorherigen Anruf zurückgegebene Wert als akkumuliert übergeben. Wir können auch ohne Akkumulator reduzieren -

res = Enum.reduce([1, 2, 3, 4], fn(x, accum) -> x + accum end)
IO.puts(res)

Wenn das obige Programm ausgeführt wird, wird das folgende Ergebnis erzeugt:

10

uniq

Die uniq-Funktion entfernt Duplikate aus unserer Sammlung und gibt nur die Menge der Elemente in der Sammlung zurück. Zum Beispiel -

res = Enum.uniq([1, 2, 2, 3, 3, 3, 4, 4, 4, 4])
IO.puts(res)

Wenn Sie das obige Programm ausführen, wird das folgende Ergebnis angezeigt:

[1, 2, 3, 4]

Eifrige Bewertung

Alle Funktionen im Enum-Modul sind gespannt. Viele Funktionen erwarten eine Aufzählung und geben eine Liste zurück. Dies bedeutet, dass bei der Ausführung mehrerer Operationen mit Enum für jede Operation eine Zwischenliste erstellt wird, bis das Ergebnis erreicht ist. Betrachten wir das folgende Beispiel, um dies zu verstehen:

odd? = &(odd? = &(rem(&1, 2) != 0) 
res = 1..100_000 |> Enum.map(&(&1 * 3)) |> Enum.filter(odd?) |> Enum.sum 
IO.puts(res)

Wenn das obige Programm ausgeführt wird, wird das folgende Ergebnis erzeugt:

7500000000

Das obige Beispiel enthält eine Pipeline von Operationen. Wir beginnen mit einem Bereich und multiplizieren dann jedes Element im Bereich mit 3. Diese erste Operation erstellt nun eine Liste mit 100_000 Elementen und gibt sie zurück. Dann behalten wir alle ungeraden Elemente aus der Liste bei, generieren eine neue Liste mit jetzt 50_000 Elementen und summieren dann alle Einträge.

Das |> Das im obigen Snippet verwendete Symbol ist das pipe operator: Es nimmt einfach die Ausgabe des Ausdrucks auf der linken Seite und übergibt sie als erstes Argument an den Funktionsaufruf auf der rechten Seite. Es ist ähnlich wie bei Unix | Operator. Ziel ist es, den Datenfluss hervorzuheben, der durch eine Reihe von Funktionen transformiert wird.

Ohne das pipe Betreiber, der Code sieht kompliziert aus -

Enum.sum(Enum.filter(Enum.map(1..100_000, &(&1 * 3)), odd?))

Wir haben viele andere Funktionen, jedoch wurden hier nur einige wichtige beschrieben.

Viele Funktionen erwarten eine Aufzählung und geben a zurück listzurück. Dies bedeutet, dass bei der Ausführung mehrerer Operationen mit Enum für jede Operation eine Zwischenliste erstellt wird, bis das Ergebnis erreicht ist.

Streams unterstützen faule Operationen im Gegensatz zu eifrigen Operationen durch Aufzählungen. Zusamenfassend,streams are lazy, composable enumerables. Dies bedeutet, dass Streams keine Operation ausführen, es sei denn, dies ist unbedingt erforderlich. Betrachten wir ein Beispiel, um dies zu verstehen -

odd? = &(rem(&1, 2) != 0)
res = 1..100_000 |> Stream.map(&(&1 * 3)) |> Stream.filter(odd?) |> Enum.sum
IO.puts(res)

Wenn das obige Programm ausgeführt wird, wird das folgende Ergebnis erzeugt:

7500000000

In dem oben angegebenen Beispiel 1..100_000 |> Stream.map(&(&1 * 3))Gibt einen Datentyp zurück, einen tatsächlichen Stream, der die Kartenberechnung über den Bereich 1..100_000 darstellt. Diese Darstellung wurde noch nicht bewertet. Anstatt Zwischenlisten zu generieren, erstellen Streams eine Reihe von Berechnungen, die nur aufgerufen werden, wenn wir den zugrunde liegenden Stream an das Enum-Modul übergeben. Streams sind nützlich, wenn Sie mit großen, möglicherweise unendlichen Sammlungen arbeiten.

Streams und Enums haben viele Funktionen gemeinsam. Streams bieten hauptsächlich dieselben Funktionen wie das Enum-Modul, das Listen als Rückgabewerte generiert hat, nachdem Berechnungen für Eingabe-Enumerables durchgeführt wurden. Einige von ihnen sind in der folgenden Tabelle aufgeführt -

Sr.Nr. Funktion und ihre Beschreibung
1

chunk(enum, n, step, leftover \\ nil)

Streamen Sie die Aufzählung in Blöcken mit jeweils n Elementen, wobei jeder neue Block Schrittelemente in die Aufzählung startet.

2

concat(enumerables)

Erstellt einen Stream, der jede Aufzählung in einer Aufzählung auflistet.

3

each(enum, fun)

Führt die angegebene Funktion für jedes Element aus.

4

filter(enum, fun)

Erstellt einen Stream, der Elemente gemäß der angegebenen Funktion bei der Aufzählung filtert.

5

map(enum, fun)

Erstellt einen Stream, der die angegebene Funktion bei der Aufzählung anwendet.

6

drop(enum, n)

Lässt die nächsten n Gegenstände faul aus der Aufzählung fallen.

Strukturen sind Erweiterungen, die auf Karten basieren und Überprüfungen zur Kompilierungszeit und Standardwerte ermöglichen.

Strukturen definieren

Um eine Struktur zu definieren, wird das Konstrukt defstruct verwendet -

defmodule User do
   defstruct name: "John", age: 27
end

Die mit defstruct verwendete Schlüsselwortliste definiert, welche Felder die Struktur zusammen mit ihren Standardwerten haben wird. Strukturen nehmen den Namen des Moduls an, in dem sie definiert sind. Im obigen Beispiel haben wir eine Struktur mit dem Namen Benutzer definiert. Wir können jetzt Benutzerstrukturen erstellen, indem wir eine Syntax verwenden, die der zum Erstellen von Karten verwendeten ähnelt.

new_john = %User{})
ayush = %User{name: "Ayush", age: 20}
megan = %User{name: "Megan"})

Der obige Code generiert drei verschiedene Strukturen mit Werten -

%User{age: 27, name: "John"}
%User{age: 20, name: "Ayush"}
%User{age: 27, name: "Megan"}

Strukturen bieten Kompilierungszeitgarantien, dass nur die durch defstruct definierten Felder (und alle) in einer Struktur vorhanden sein dürfen. Sie können also keine eigenen Felder definieren, nachdem Sie die Struktur im Modul erstellt haben.

Zugriff auf und Aktualisierung von Strukturen

Bei der Erörterung von Karten haben wir gezeigt, wie wir auf die Felder einer Karte zugreifen und diese aktualisieren können. Dieselben Techniken (und dieselbe Syntax) gelten auch für Strukturen. Wenn wir beispielsweise den Benutzer aktualisieren möchten, den wir im vorherigen Beispiel erstellt haben, dann -

defmodule User do
   defstruct name: "John", age: 27
end
john = %User{}
#john right now is: %User{age: 27, name: "John"}

#To access name and age of John, 
IO.puts(john.name)
IO.puts(john.age)

Wenn das obige Programm ausgeführt wird, wird das folgende Ergebnis erzeugt:

John
27

Um einen Wert in einer Struktur zu aktualisieren, verwenden wir erneut das gleiche Verfahren wie im Kartenkapitel.

meg = %{john | name: "Meg"}

Strukturen können auch beim Mustervergleich verwendet werden, sowohl um den Wert bestimmter Schlüssel abzugleichen als auch um sicherzustellen, dass der Übereinstimmungswert eine Struktur des gleichen Typs wie der übereinstimmende Wert ist.

Protokolle sind ein Mechanismus, um Polymorphismus in Elixir zu erreichen. Das Versenden eines Protokolls ist für jeden Datentyp verfügbar, solange das Protokoll implementiert wird.

Betrachten wir ein Beispiel für die Verwendung von Protokollen. Wir haben eine Funktion namens aufgerufento_stringin den vorherigen Kapiteln, um von anderen Typen in den Zeichenfolgentyp zu konvertieren. Dies ist eigentlich ein Protokoll. Es handelt sich nach der Eingabe, die gegeben wird, ohne einen Fehler zu erzeugen. Dies mag so aussehen, als würden wir Mustervergleichsfunktionen diskutieren, aber im weiteren Verlauf stellt sich heraus, dass dies anders ist.

Betrachten Sie das folgende Beispiel, um den Protokollmechanismus besser zu verstehen.

Lassen Sie uns ein Protokoll erstellen, das anzeigt, ob die angegebene Eingabe leer ist oder nicht. Wir werden dieses Protokoll nennenblank?.

Protokoll definieren

Wir können ein Protokoll in Elixir folgendermaßen definieren:

defprotocol Blank do
   def blank?(data)
end

Wie Sie sehen, müssen wir keinen Körper für die Funktion definieren. Wenn Sie mit Schnittstellen in anderen Programmiersprachen vertraut sind, können Sie sich ein Protokoll als im Wesentlichen dasselbe vorstellen.

Dieses Protokoll besagt also, dass alles, was es implementiert, eine haben muss empty?Funktion, obwohl es Sache des Implementierers ist, wie die Funktion reagiert. Lassen Sie uns anhand des definierten Protokolls verstehen, wie einige Implementierungen hinzugefügt werden.

Implementierung eines Protokolls

Da wir ein Protokoll definiert haben, müssen wir ihm jetzt mitteilen, wie mit den verschiedenen Eingaben umgegangen werden soll, die es möglicherweise erhält. Lassen Sie uns auf dem Beispiel aufbauen, das wir zuvor genommen hatten. Wir werden das leere Protokoll für Listen, Maps und Strings implementieren. Dies zeigt, ob das, was wir übergeben haben, leer ist oder nicht.

#Defining the protocol
defprotocol Blank do
   def blank?(data)
end

#Implementing the protocol for lists
defimpl Blank, for: List do
   def blank?([]), do: true
   def blank?(_), do: false
end

#Implementing the protocol for strings
defimpl Blank, for: BitString do
   def blank?(""), do: true
   def blank?(_), do: false
end

#Implementing the protocol for maps
defimpl Blank, for: Map do
   def blank?(map), do: map_size(map) == 0
end

IO.puts(Blank.blank? [])
IO.puts(Blank.blank? [:true, "Hello"])
IO.puts(Blank.blank? "")
IO.puts(Blank.blank? "Hi")

Sie können Ihr Protokoll für so viele oder so wenige Typen implementieren, wie Sie möchten, was für die Verwendung Ihres Protokolls sinnvoll ist. Dies war ein ziemlich grundlegender Anwendungsfall für Protokolle. Wenn das obige Programm ausgeführt wird, wird das folgende Ergebnis erzeugt:

true
false
true
false

Note - Wenn Sie dies für andere Typen als die verwenden, für die Sie das Protokoll definiert haben, wird ein Fehler ausgegeben.

Datei-E / A ist ein wesentlicher Bestandteil jeder Programmiersprache, da die Sprache mit den Dateien im Dateisystem interagieren kann. In diesem Kapitel werden zwei Module erläutert - Pfad und Datei.

Das Pfadmodul

Das pathModul ist ein sehr kleines Modul, das als Hilfsmodul für Dateisystemoperationen betrachtet werden kann. Die meisten Funktionen im Dateimodul erwarten Pfade als Argumente. Am häufigsten sind diese Pfade reguläre Binärdateien. Das Pfadmodul bietet Funktionen zum Arbeiten mit solchen Pfaden. Die Verwendung von Funktionen aus dem Path-Modul im Gegensatz zur reinen Bearbeitung von Binärdateien wird bevorzugt, da das Path-Modul verschiedene Betriebssysteme transparent verwaltet. Es ist zu beachten, dass Elixir bei der Ausführung von Dateivorgängen automatisch Schrägstriche (/) in Backslashes (\) unter Windows konvertiert.

Betrachten wir das folgende Beispiel, um das Pfadmodul besser zu verstehen:

IO.puts(Path.join("foo", "bar"))

Wenn das obige Programm ausgeführt wird, wird das folgende Ergebnis erzeugt:

foo/bar

Es gibt viele Methoden, die das Pfadmodul bereitstellt. Sie können an den verschiedenen Methoden einen Blick hier . Diese Methoden werden häufig verwendet, wenn Sie viele Dateibearbeitungsvorgänge ausführen.

Das Dateimodul

Das Dateimodul enthält Funktionen, mit denen wir Dateien als E / A-Geräte öffnen können. Standardmäßig werden Dateien im Binärmodus geöffnet, sodass Entwickler die spezifischen Dateien verwenden müssenIO.binread und IO.binwriteFunktionen aus dem IO-Modul. Lassen Sie uns eine Datei namens erstellennewfile und schreibe einige Daten darauf.

{:ok, file} = File.read("newfile", [:write]) 
# Pattern matching to store returned stream
IO.binwrite(file, "This will be written to the file")

Wenn Sie die Datei öffnen, in die wir gerade geschrieben haben, wird der Inhalt folgendermaßen angezeigt:

This will be written to the file

Lassen Sie uns nun verstehen, wie das Dateimodul verwendet wird.

Datei öffnen

Um eine Datei zu öffnen, können wir eine der folgenden 2 Funktionen verwenden:

{:ok, file} = File.open("newfile")
file = File.open!("newfile")

Lassen Sie uns nun den Unterschied zwischen dem File.open Funktion und die File.open!() Funktion.

  • Das File.openFunktion gibt immer ein Tupel zurück. Wenn die Datei erfolgreich geöffnet wurde, wird der erste Wert im Tupel als zurückgegeben:okund der zweite Wert ist ein Literal vom Typ io_device. Wenn ein Fehler verursacht wird, wird ein Tupel mit dem ersten Wert als zurückgegeben:error und zweiter Wert als Grund.

  • Das File.open!() Funktion hingegen gibt a zurück io_deviceWenn die Datei erfolgreich geöffnet wurde, wird ein Fehler ausgegeben. HINWEIS: Dies ist das Muster, das in allen Funktionen des Dateimoduls verwendet wird, die wir diskutieren werden.

Wir können auch die Modi angeben, in denen wir diese Datei öffnen möchten. Um eine Datei schreibgeschützt und im utf-8-Codierungsmodus zu öffnen, verwenden wir den folgenden Code:

file = File.open!("newfile", [:read, :utf8])

In eine Datei schreiben

Wir haben zwei Möglichkeiten, in Dateien zu schreiben. Lassen Sie uns den ersten sehen, der die Schreibfunktion aus dem Dateimodul verwendet.

File.write("newfile", "Hello")

Dies sollte jedoch nicht verwendet werden, wenn Sie mehrere Schreibvorgänge in dieselbe Datei ausführen. Jedes Mal, wenn diese Funktion aufgerufen wird, wird ein Dateideskriptor geöffnet und ein neuer Prozess zum Schreiben in die Datei erstellt. Wenn Sie mehrere Schreibvorgänge in einer Schleife ausführen, öffnen Sie die Datei überFile.openund schreiben Sie mit den Methoden im E / A-Modul darauf. Betrachten wir ein Beispiel, um dasselbe zu verstehen -

#Open the file in read, write and utf8 modes. 
file = File.open!("newfile_2", [:read, :utf8, :write])

#Write to this "io_device" using standard IO functions
IO.puts(file, "Random text")

Sie können andere E / A-Modulmethoden wie verwenden IO.write und IO.binwrite um in Dateien zu schreiben, die als io_device geöffnet sind.

Lesen aus einer Datei

Wir haben zwei Möglichkeiten, aus Dateien zu lesen. Lassen Sie uns den ersten sehen, der die Lesefunktion aus dem Dateimodul verwendet.

IO.puts(File.read("newfile"))

Wenn Sie diesen Code ausführen, sollten Sie ein Tupel mit dem ersten Element als erhalten :ok und die zweite als Inhalt der neuen Datei

Wir können auch die verwenden File.read! Funktion, um nur den Inhalt der an uns zurückgegebenen Dateien zu erhalten.

Eine geöffnete Datei schließen

Wenn Sie eine Datei mit der Funktion File.open öffnen, sollten Sie sie nach Abschluss der Funktion mit der Funktion schließen File.close Funktion -

File.close(file)

In Elixir wird der gesamte Code in Prozessen ausgeführt. Prozesse sind voneinander isoliert, werden gleichzeitig ausgeführt und kommunizieren über die Nachrichtenübermittlung. Die Prozesse von Elixir sollten nicht mit den Prozessen des Betriebssystems verwechselt werden. Prozesse in Elixir sind in Bezug auf Speicher und CPU extrem leicht (im Gegensatz zu Threads in vielen anderen Programmiersprachen). Aus diesem Grund ist es nicht ungewöhnlich, dass Zehntausende oder sogar Hunderttausende von Prozessen gleichzeitig ausgeführt werden.

In diesem Kapitel lernen wir die grundlegenden Konstrukte zum Laichen neuer Prozesse sowie zum Senden und Empfangen von Nachrichten zwischen verschiedenen Prozessen kennen.

Die Spawn-Funktion

Der einfachste Weg, einen neuen Prozess zu erstellen, ist die Verwendung von spawnFunktion. Dasspawnakzeptiert eine Funktion, die im neuen Prozess ausgeführt wird. Zum Beispiel -

pid = spawn(fn -> 2 * 2 end)
Process.alive?(pid)

Wenn das obige Programm ausgeführt wird, wird das folgende Ergebnis erzeugt:

false

Der Rückgabewert der Spawn-Funktion ist eine PID. Dies ist eine eindeutige Kennung für den Prozess. Wenn Sie den Code über Ihrer PID ausführen, ist dies anders. Wie Sie in diesem Beispiel sehen können, ist der Prozess beendet, wenn wir prüfen, ob er aktiv ist. Dies liegt daran, dass der Prozess beendet wird, sobald die Ausführung der angegebenen Funktion abgeschlossen ist.

Wie bereits erwähnt, werden alle Elixir-Codes in Prozessen ausgeführt. Wenn Sie die Selbstfunktion ausführen, wird die PID für Ihre aktuelle Sitzung angezeigt.

pid = self
 
Process.alive?(pid)

Wenn das obige Programm ausgeführt wird, erzeugt es folgendes Ergebnis:

true

Nachrichtenübermittlung

Wir können Nachrichten an einen Prozess mit senden send und empfange sie mit receive. Lassen Sie uns eine Nachricht an den aktuellen Prozess weiterleiten und auf demselben empfangen.

send(self(), {:hello, "Hi people"})

receive do
   {:hello, msg} -> IO.puts(msg)
   {:another_case, msg} -> IO.puts("This one won't match!")
end

Wenn das obige Programm ausgeführt wird, wird das folgende Ergebnis erzeugt:

Hi people

Wir haben eine Nachricht mit der Sendefunktion an den aktuellen Prozess gesendet und an die PID von self übergeben. Dann haben wir die eingehende Nachricht mit dem behandeltreceive Funktion.

Wenn eine Nachricht an einen Prozess gesendet wird, wird die Nachricht im gespeichert process mailbox. Der Empfangsblock durchläuft das aktuelle Prozesspostfach und sucht nach einer Nachricht, die einem der angegebenen Muster entspricht. Der Empfangsblock unterstützt Wachen und viele Klauseln, z. B. Groß- und Kleinschreibung.

Wenn sich in der Mailbox keine Nachricht befindet, die mit einem der Muster übereinstimmt, wartet der aktuelle Prozess, bis eine übereinstimmende Nachricht eintrifft. Es kann auch eine Zeitüberschreitung angegeben werden. Zum Beispiel,

receive do
   {:hello, msg}  -> msg
after
   1_000 -> "nothing after 1s"
end

Wenn das obige Programm ausgeführt wird, wird das folgende Ergebnis erzeugt:

nothing after 1s

NOTE - Eine Zeitüberschreitung von 0 kann angegeben werden, wenn Sie bereits erwarten, dass sich die Nachricht im Postfach befindet.

Links

Die häufigste Form des Laichens in Elixir ist tatsächlich via spawn_linkFunktion. Bevor wir uns ein Beispiel mit spawn_link ansehen, lassen Sie uns verstehen, was passiert, wenn ein Prozess fehlschlägt.

spawn fn -> raise "oops" end

Wenn das obige Programm ausgeführt wird, wird der folgende Fehler ausgegeben:

[error] Process #PID<0.58.00> raised an exception
** (RuntimeError) oops
   :erlang.apply/2

Es wurde ein Fehler protokolliert, aber der Laichprozess wird noch ausgeführt. Dies liegt daran, dass Prozesse isoliert sind. Wenn wir möchten, dass sich der Fehler in einem Prozess auf einen anderen ausbreitet, müssen wir sie verknüpfen. Dies kann mit dem erfolgenspawn_linkFunktion. Betrachten wir ein Beispiel, um dasselbe zu verstehen -

spawn_link fn -> raise "oops" end

Wenn das obige Programm ausgeführt wird, wird der folgende Fehler ausgegeben:

** (EXIT from #PID<0.41.0>) an exception was raised:
   ** (RuntimeError) oops
      :erlang.apply/2

Wenn Sie dies ausführen iexShell dann behandelt die Shell diesen Fehler und wird nicht beendet. Wenn Sie jedoch zuerst eine Skriptdatei erstellen und dann verwendenelixir <file-name>.exsAufgrund dieses Fehlers wird auch der übergeordnete Prozess heruntergefahren.

Prozesse und Verknüpfungen spielen beim Aufbau fehlertoleranter Systeme eine wichtige Rolle. In Elixir-Anwendungen verknüpfen wir unsere Prozesse häufig mit Supervisoren, die erkennen, wann ein Prozess stirbt, und an seiner Stelle einen neuen Prozess starten. Dies ist nur möglich, weil Prozesse isoliert sind und standardmäßig nichts gemeinsam nutzen. Und da Prozesse isoliert sind, kann ein Fehler in einem Prozess auf keinen Fall den Status eines anderen abstürzen oder beschädigen. Während andere Sprachen erfordern, dass wir Ausnahmen abfangen / behandeln; In Elixir ist es tatsächlich in Ordnung, Prozesse fehlschlagen zu lassen, da wir erwarten, dass die Vorgesetzten unsere Systeme ordnungsgemäß neu starten.

Zustand

Wenn Sie eine Anwendung erstellen, für die beispielsweise der Status erforderlich ist, um Ihre Anwendungskonfiguration beizubehalten, oder wenn Sie eine Datei analysieren und im Speicher behalten müssen, wo würden Sie sie speichern? Die Prozessfunktionalität von Elixir kann dabei nützlich sein.

Wir können Prozesse schreiben, die eine Endlosschleife bilden, den Status beibehalten sowie Nachrichten senden und empfangen. Schreiben wir als Beispiel ein Modul, das neue Prozesse startet, die als Schlüsselwertspeicher in einer Datei mit dem Namen fungierenkv.exs.

defmodule KV do
   def start_link do
      Task.start_link(fn -> loop(%{}) end)
   end

   defp loop(map) do
      receive do
         {:get, key, caller} ->
         send caller, Map.get(map, key)
         loop(map)
         {:put, key, value} ->
         loop(Map.put(map, key, value))
      end
   end
end

Notiere dass der start_link Funktion startet einen neuen Prozess, der die ausführt loopFunktion, beginnend mit einer leeren Karte. DasloopDie Funktion wartet dann auf Nachrichten und führt für jede Nachricht die entsprechende Aktion aus. Im Fall von a:getNachricht sendet eine Nachricht an den Anrufer zurück und ruft die Schleife erneut auf, um auf eine neue Nachricht zu warten. Während:put Nachricht ruft tatsächlich auf loop mit einer neuen Version der Karte, wobei der angegebene Schlüssel und Wert gespeichert sind.

Lassen Sie uns nun Folgendes ausführen:

iex kv.exs

Jetzt solltest du in deinem sein iexSchale. Versuchen Sie Folgendes, um unser Modul zu testen:

{:ok, pid} = KV.start_link

# pid now has the pid of our new process that is being 
# used to get and store key value pairs 

# Send a KV pair :hello, "Hello" to the process
send pid, {:put, :hello, "Hello"}

# Ask for the key :hello
send pid, {:get, :hello, self()}

# Print all the received messages on the current process.
flush()

Wenn das obige Programm ausgeführt wird, wird das folgende Ergebnis erzeugt:

"Hello"

In diesem Kapitel werden wir Siegel untersuchen, die Mechanismen, die die Sprache für die Arbeit mit Textdarstellungen bereitstellt. Siegel beginnen mit dem Tilde-Zeichen (~), gefolgt von einem Buchstaben (der das Siegel kennzeichnet) und einem Begrenzer. Optional können Modifikatoren nach dem endgültigen Trennzeichen hinzugefügt werden.

Regex

Regexes in Elixir sind Siegel. Wir haben ihre Verwendung im Kapitel String gesehen. Nehmen wir noch einmal ein Beispiel, um zu sehen, wie wir Regex in Elixir verwenden können.

# A regular expression that matches strings which contain "foo" or
# "bar":
regex = ~r/foo|bar/
IO.puts("foo" =~ regex)
IO.puts("baz" =~ regex)

Wenn das obige Programm ausgeführt wird, wird das folgende Ergebnis erzeugt:

true
false

Siegel unterstützen 8 verschiedene Begrenzer -

~r/hello/
~r|hello|
~r"hello"
~r'hello'
~r(hello)
~r[hello]
~r{hello}
~r<hello>

Der Grund für die Unterstützung verschiedener Begrenzer besteht darin, dass verschiedene Begrenzer für verschiedene Siegel besser geeignet sein können. Beispielsweise kann die Verwendung von Klammern für reguläre Ausdrücke eine verwirrende Wahl sein, da sie mit den Klammern innerhalb des regulären Ausdrucks gemischt werden können. Klammern können jedoch für andere Siegel nützlich sein, wie wir im nächsten Abschnitt sehen werden.

Elixir unterstützt Perl-kompatible Regexes und Modifikatoren. Weitere Informationen zur Verwendung von Regexen finden Sie hier .

Zeichenfolgen, Zeichenlisten und Wortlisten

Abgesehen von regulären Ausdrücken hat Elixir 3 weitere eingebaute Siegel. Schauen wir uns die Siegel an.

Saiten

Das Siegel ~ wird verwendet, um Zeichenfolgen zu generieren, wie dies bei doppelten Anführungszeichen der Fall ist. Das Siegel von ~ s ist beispielsweise nützlich, wenn eine Zeichenfolge sowohl doppelte als auch einfache Anführungszeichen enthält -

new_string = ~s(this is a string with "double" quotes, not 'single' ones)
IO.puts(new_string)

Dieses Siegel erzeugt Zeichenketten. Wenn das obige Programm ausgeführt wird, wird das folgende Ergebnis erzeugt:

"this is a string with \"double\" quotes, not 'single' ones"

Char Lists

Das ~ c-Siegel wird verwendet, um Zeichenlisten zu generieren -

new_char_list = ~c(this is a char list containing 'single quotes')
IO.puts(new_char_list)

Wenn das obige Programm ausgeführt wird, wird das folgende Ergebnis erzeugt:

this is a char list containing 'single quotes'

Wortlisten

Das ~ w-Siegel wird verwendet, um Wortlisten zu generieren (Wörter sind nur reguläre Zeichenfolgen). Innerhalb des Siegels werden Wörter durch Leerzeichen getrennt.

new_word_list = ~w(foo bar bat)
IO.puts(new_word_list)

Wenn das obige Programm ausgeführt wird, wird das folgende Ergebnis erzeugt:

foobarbat

Das Siegel akzeptiert auch das c, s und a Modifikatoren (für Zeichenlisten, Zeichenfolgen bzw. Atome), die den Datentyp der Elemente der resultierenden Liste angeben -

new_atom_list = ~w(foo bar bat)a
IO.puts(new_atom_list)

Wenn das obige Programm ausgeführt wird, wird das folgende Ergebnis erzeugt:

[:foo, :bar, :bat]

Interpolation und Flucht in Siegel

Neben Siegel in Kleinbuchstaben unterstützt Elixir auch Siegel in Großbuchstaben, um mit Escapezeichen und Interpolation umzugehen. Während sowohl ~ s als auch ~ S Zeichenfolgen zurückgeben, erlaubt Ersteres Escape-Codes und Interpolation, während Letzteres dies nicht tut. Betrachten wir ein Beispiel, um dies zu verstehen -

~s(String with escape codes \x26 #{"inter" <> "polation"})
# "String with escape codes & interpolation"
~S(String without escape codes \x26 without #{interpolation})
# "String without escape codes \\x26 without \#{interpolation}"

Benutzerdefinierte Siegel

Wir können leicht unsere eigenen Siegel erstellen. In diesem Beispiel erstellen wir ein Siegel, um eine Zeichenfolge in Großbuchstaben umzuwandeln.

defmodule CustomSigil do
   def sigil_u(string, []), do: String.upcase(string)
end

import CustomSigil

IO.puts(~u/tutorials point/)

Wenn wir den obigen Code ausführen, wird das folgende Ergebnis erzeugt:

TUTORIALS POINT

Zuerst definieren wir ein Modul namens CustomSigil und innerhalb dieses Moduls haben wir eine Funktion namens sigil_u erstellt. Da im vorhandenen Siegelraum kein Siegel vorhanden ist, werden wir es verwenden. Das _u zeigt an, dass wir u als Zeichen nach der Tilde verwenden möchten. Die Funktionsdefinition muss zwei Argumente enthalten, eine Eingabe und eine Liste.

Listenverständnisse sind syntaktischer Zucker zum Durchlaufen von Aufzählungen in Elixir. In diesem Kapitel werden wir Verständnis für die Iteration und Generierung verwenden.

Grundlagen

Als wir uns das Enum-Modul im Kapitel Enumerables angesehen haben, sind wir auf die Map-Funktion gestoßen.

Enum.map(1..3, &(&1 * 2))

In diesem Beispiel übergeben wir eine Funktion als zweites Argument. Jedes Element im Bereich wird an die Funktion übergeben, und anschließend wird eine neue Liste mit den neuen Werten zurückgegeben.

Mapping, Filtern und Transformieren sind in Elixir sehr häufig. Daher gibt es eine etwas andere Möglichkeit, das gleiche Ergebnis wie im vorherigen Beispiel zu erzielen.

for n <- 1..3, do: n * 2

Wenn wir den obigen Code ausführen, wird das folgende Ergebnis erzeugt:

[2, 4, 6]

Das zweite Beispiel ist ein Verständnis, und wie Sie wahrscheinlich sehen können, ist es einfach syntaktischer Zucker für das, was Sie auch erreichen können, wenn Sie das verwenden Enum.mapFunktion. Die Verwendung eines Verständnisses gegenüber einer Funktion aus dem Enum-Modul in Bezug auf die Leistung bietet jedoch keine wirklichen Vorteile.

Das Verständnis ist nicht auf Listen beschränkt, sondern kann mit allen Aufzählungen verwendet werden.

Filter

Sie können sich Filter als eine Art Schutz für das Verständnis vorstellen. Wenn ein gefilterter Wert zurückgegeben wirdfalse oder niles ist von der endgültigen Liste ausgeschlossen. Lassen Sie uns einen Bereich durchlaufen und uns nur um gerade Zahlen kümmern. Wir werden die verwendenis_even Funktion aus dem Integer-Modul, um zu überprüfen, ob ein Wert gerade ist oder nicht.

import Integer
IO.puts(for x <- 1..10, is_even(x), do: x)

Wenn der obige Code ausgeführt wird, wird das folgende Ergebnis erzeugt:

[2, 4, 6, 8, 10]

Wir können auch mehrere Filter im gleichen Verständnis verwenden. Fügen Sie nach dem einen weiteren gewünschten Filter hinzuis_even Filter durch Komma getrennt.

: in Option

In den obigen Beispielen gaben alle Verständnisse Listen als Ergebnis zurück. Das Ergebnis eines Verständnisses kann jedoch durch Übergeben der in verschiedene Datenstrukturen eingefügt werden:into Option zum Verständnis.

Zum Beispiel a bitstring Der Generator kann mit der Option: into verwendet werden, um alle Leerzeichen in einer Zeichenfolge einfach zu entfernen.

IO.puts(for <<c <- " hello world ">>, c != ?\s, into: "", do: <<c>>)

Wenn der obige Code ausgeführt wird, wird das folgende Ergebnis erzeugt:

helloworld

Der obige Code entfernt alle Leerzeichen aus der Zeichenfolge mit c != ?\s filtern und dann mit der Option: in werden alle zurückgegebenen Zeichen in eine Zeichenfolge eingefügt.

Elixir ist eine dynamisch typisierte Sprache, sodass alle Typen in Elixir von der Laufzeit abgeleitet werden. Elixir wird jedoch mit Typenspezifikationen geliefert, für die eine Notation verwendet wirddeclaring custom data types and declaring typed function signatures (specifications).

Funktionsspezifikationen (Spezifikationen)

Standardmäßig bietet Elixir einige grundlegende Typen wie Integer oder PID sowie komplexe Typen: zum Beispiel die roundDie Funktion, die einen Gleitkommawert auf die nächste Ganzzahl rundet, nimmt eine Zahl als Argument (eine Ganzzahl oder einen Gleitkommawert) und gibt eine Ganzzahl zurück. In der zugehörigen Dokumentation wird die runde Signatur wie folgt geschrieben:

round(number) :: integer

Die obige Beschreibung impliziert, dass die Funktion auf der linken Seite als Argument verwendet, was in Klammern angegeben ist, und was auf der rechten Seite von ::, dh Integer, zurückgibt. Funktionsspezifikationen werden mit dem geschrieben@specDirektive, direkt vor der Funktionsdefinition platziert. Die Rundungsfunktion kann geschrieben werden als -

@spec round(number) :: integer
def round(number), do: # Function implementation
...

Typenspezifikationen unterstützen auch komplexe Typen. Wenn Sie beispielsweise eine Liste von Ganzzahlen zurückgeben möchten, können Sie diese verwenden [Integer]

Benutzerdefinierte Typen

Elixir bietet zwar viele nützliche integrierte Typen, es ist jedoch praktisch, bei Bedarf benutzerdefinierte Typen zu definieren. Dies kann erfolgen, wenn Module über die @ type-Direktive definiert werden. Betrachten wir ein Beispiel, um dasselbe zu verstehen -

defmodule FunnyCalculator do
   @type number_with_joke :: {number, String.t}

   @spec add(number, number) :: number_with_joke
   def add(x, y), do: {x + y, "You need a calculator to do that?"}

   @spec multiply(number, number) :: number_with_joke
   def multiply(x, y), do: {x * y, "It is like addition on steroids."}
end

{result, comment} = FunnyCalculator.add(10, 20)
IO.puts(result)
IO.puts(comment)

Wenn das obige Programm ausgeführt wird, wird das folgende Ergebnis erzeugt:

30
You need a calculator to do that?

NOTE - Über @type definierte benutzerdefinierte Typen werden exportiert und sind außerhalb des Moduls verfügbar, in dem sie definiert sind. Wenn Sie einen benutzerdefinierten Typ privat halten möchten, können Sie das verwenden @typep Richtlinie statt @type.

Verhaltensweisen in Elixir (und Erlang) sind eine Möglichkeit, den generischen Teil einer Komponente (die zum Verhaltensmodul wird) von dem spezifischen Teil (der zum Rückrufmodul wird) zu trennen und zu abstrahieren. Verhaltensweisen bieten einen Weg zu -

  • Definieren Sie eine Reihe von Funktionen, die von einem Modul implementiert werden müssen.
  • Stellen Sie sicher, dass ein Modul alle Funktionen in diesem Satz implementiert.

Wenn Sie müssen, können Sie sich Verhaltensweisen wie Schnittstellen in objektorientierten Sprachen wie Java vorstellen: eine Reihe von Funktionssignaturen, die ein Modul implementieren muss.

Verhalten definieren

Betrachten wir ein Beispiel, um unser eigenes Verhalten zu erstellen, und verwenden Sie dann dieses generische Verhalten, um ein Modul zu erstellen. Wir werden ein Verhalten definieren, das Menschen in verschiedenen Sprachen begrüßt und verabschiedet.

defmodule GreetBehaviour do
   @callback say_hello(name :: string) :: nil
   @callback say_bye(name :: string) :: nil
end

Das @callbackDie Direktive wird verwendet, um die Funktionen aufzulisten, die durch die Übernahme von Modulen definiert werden müssen. Es gibt auch die Nr. An. von Argumenten, ihrem Typ und ihren Rückgabewerten.

Ein Verhalten annehmen

Wir haben erfolgreich ein Verhalten definiert. Jetzt werden wir es in mehreren Modulen übernehmen und implementieren. Lassen Sie uns zwei Module erstellen, die dieses Verhalten auf Englisch und Spanisch implementieren.

defmodule GreetBehaviour do
   @callback say_hello(name :: string) :: nil
   @callback say_bye(name :: string) :: nil
end

defmodule EnglishGreet do
   @behaviour GreetBehaviour
   def say_hello(name), do: IO.puts("Hello " <> name)
   def say_bye(name), do: IO.puts("Goodbye, " <> name)
end

defmodule SpanishGreet do
   @behaviour GreetBehaviour
   def say_hello(name), do: IO.puts("Hola " <> name)
   def say_bye(name), do: IO.puts("Adios " <> name)
end

EnglishGreet.say_hello("Ayush")
EnglishGreet.say_bye("Ayush")
SpanishGreet.say_hello("Ayush")
SpanishGreet.say_bye("Ayush")

Wenn das obige Programm ausgeführt wird, wird das folgende Ergebnis erzeugt:

Hello Ayush
Goodbye, Ayush
Hola Ayush
Adios Ayush

Wie Sie bereits gesehen haben, übernehmen wir ein Verhalten mit dem @behaviourDirektive im Modul. Wir müssen alle im Verhalten implementierten Funktionen für alle untergeordneten Module definieren. Dies kann in etwa als gleichwertig mit Schnittstellen in OOP-Sprachen angesehen werden.

Elixir hat drei Fehlermechanismen: Fehler, Würfe und Exits. Lassen Sie uns jeden Mechanismus im Detail untersuchen.

Error

Fehler (oder Ausnahmen) werden verwendet, wenn außergewöhnliche Dinge im Code passieren. Ein Beispielfehler kann abgerufen werden, indem versucht wird, einer Zeichenfolge eine Zahl hinzuzufügen.

IO.puts(1 + "Hello")

Wenn das obige Programm ausgeführt wird, wird der folgende Fehler ausgegeben:

** (ArithmeticError) bad argument in arithmetic expression
   :erlang.+(1, "Hello")

Dies war ein eingebauter Beispielfehler.

Fehler auslösen

Wir können raiseFehler bei der Verwendung der Erhöhungsfunktionen. Betrachten wir ein Beispiel, um dasselbe zu verstehen -

#Runtime Error with just a message
raise "oops"  # ** (RuntimeError) oops

Andere Fehler können ausgelöst werden, indem Raise / 2 den Fehlernamen und eine Liste von Schlüsselwortargumenten übergibt

#Other error type with a message
raise ArgumentError, message: "invalid argument foo"

Sie können auch Ihre eigenen Fehler definieren und diese auslösen. Betrachten Sie das folgende Beispiel -

defmodule MyError do
   defexception message: "default message"
end

raise MyError  # Raises error with default message
raise MyError, message: "custom message"  # Raises error with custom message

Fehler beheben

Wir möchten nicht, dass unsere Programme abrupt beendet werden, sondern dass die Fehler sorgfältig behandelt werden. Hierfür verwenden wir die Fehlerbehandlung. Wirrescue Fehler bei der Verwendung der try/rescuebauen. Betrachten wir das folgende Beispiel, um dasselbe zu verstehen:

err = try do
   raise "oops"
rescue
   e in RuntimeError -> e
end

IO.puts(err.message)

Wenn das obige Programm ausgeführt wird, wird das folgende Ergebnis erzeugt:

oops

Wir haben Fehler in der Rettungsanweisung mithilfe des Mustervergleichs behandelt. Wenn wir den Fehler nicht verwenden und ihn nur zu Identifikationszwecken verwenden möchten, können wir auch das Formular verwenden -

err = try do
   1 + "Hello"
rescue
   RuntimeError -> "You've got a runtime error!"
   ArithmeticError -> "You've got a Argument error!"
end

IO.puts(err)

Wenn Sie das obige Programm ausführen, wird das folgende Ergebnis angezeigt:

You've got a Argument error!

NOTE- Die meisten Funktionen in der Elixir-Standardbibliothek werden zweimal implementiert, wobei einmal Tupel zurückgegeben werden und das andere Mal Fehler auftreten. Zum Beispiel dieFile.read und die File.read!Funktionen. Das erste hat ein Tupel zurückgegeben, wenn die Datei erfolgreich gelesen wurde und ein Fehler aufgetreten ist. Dieses Tupel wurde verwendet, um den Grund für den Fehler anzugeben. Der zweite hat einen Fehler ausgelöst, wenn ein Fehler aufgetreten ist.

Wenn wir den ersten Funktionsansatz verwenden, müssen wir case für die Musterübereinstimmung mit dem Fehler verwenden und entsprechend handeln. Im zweiten Fall verwenden wir den Try-Rescue-Ansatz für fehleranfälligen Code und behandeln Fehler entsprechend.

Würfe

In Elixir kann ein Wert geworfen und später abgefangen werden. Wurf und Fang sind für Situationen reserviert, in denen es nicht möglich ist, einen Wert abzurufen, es sei denn, Sie verwenden Wurf und Fang.

Die Instanzen sind in der Praxis eher ungewöhnlich, außer wenn sie mit Bibliotheken verbunden sind. Nehmen wir nun zum Beispiel an, dass das Enum-Modul keine API zum Suchen eines Werts bereitgestellt hat und dass wir das erste Vielfache von 13 in einer Liste von Zahlen finden mussten -

val = try do
   Enum.each 20..100, fn(x) ->
      if rem(x, 13) == 0, do: throw(x)
   end
   "Got nothing"
catch
   x -> "Got #{x}"
end

IO.puts(val)

Wenn das obige Programm ausgeführt wird, wird das folgende Ergebnis erzeugt:

Got 26

Ausgang

Wenn ein Prozess an „natürlichen Ursachen“ stirbt (z. B. nicht behandelte Ausnahmen), sendet er ein Ausgangssignal. Ein Prozess kann auch durch explizites Senden eines Ausgangssignals sterben. Betrachten wir das folgende Beispiel:

spawn_link fn -> exit(1) end

Im obigen Beispiel wurde der verknüpfte Prozess durch Senden eines Exit-Signals mit dem Wert 1 beendet. Beachten Sie, dass der Exit auch mit try / catch „abgefangen“ werden kann. Zum Beispiel -

val = try do
   exit "I am exiting"
catch
   :exit, _ -> "not really"
end

IO.puts(val)

Wenn das obige Programm ausgeführt wird, wird das folgende Ergebnis erzeugt:

not really

Nach

Manchmal muss sichergestellt werden, dass eine Ressource nach einer Aktion bereinigt wird, die möglicherweise einen Fehler auslösen kann. Mit dem try / after-Konstrukt können Sie dies tun. Zum Beispiel können wir eine Datei öffnen und sie mit einer after-Klausel schließen - auch wenn etwas schief geht.

{:ok, file} = File.open "sample", [:utf8, :write]
try do
   IO.write file, "olá"
   raise "oops, something went wrong"
after
   File.close(file)
end

Wenn wir dieses Programm ausführen, wird es uns einen Fehler geben. Aber dieafter Die Anweisung stellt sicher, dass der Dateideskriptor bei einem solchen Ereignis geschlossen wird.

Makros sind eine der fortschrittlichsten und leistungsstärksten Funktionen von Elixir. Wie bei allen erweiterten Funktionen einer Sprache sollten Makros sparsam verwendet werden. Sie ermöglichen die Durchführung leistungsfähiger Codetransformationen in der Kompilierungszeit. Wir werden nun kurz verstehen, was Makros sind und wie man sie verwendet.

Zitat

Bevor wir über Makros sprechen, schauen wir uns zunächst die Elixir-Interna an. Ein Elixir-Programm kann durch eigene Datenstrukturen dargestellt werden. Der Baustein eines Elixir-Programms ist ein Tupel mit drei Elementen. Beispielsweise wird die Funktionsaufrufsumme (1, 2, 3) intern dargestellt als -

{:sum, [], [1, 2, 3]}

Das erste Element ist der Funktionsname, das zweite ist eine Schlüsselwortliste mit Metadaten und das dritte ist die Argumentliste. Sie können dies als Ausgabe in der IEX-Shell erhalten, wenn Sie Folgendes schreiben:

quote do: sum(1, 2, 3)

Operatoren werden auch als solche Tupel dargestellt. Variablen werden auch mit solchen Tripletts dargestellt, außer dass das letzte Element ein Atom anstelle einer Liste ist. Wenn wir komplexere Ausdrücke zitieren, können wir sehen, dass der Code in solchen Tupeln dargestellt wird, die häufig in einer baumähnlichen Struktur ineinander verschachtelt sind. Viele Sprachen würden solche Darstellungen alsAbstract Syntax Tree (AST). Elixir nennt diese zitierten Ausdrücke.

Unquote

Wie können wir die interne Struktur unseres Codes abrufen, nachdem wir sie abrufen können? Um neuen Code oder neue Werte einzufügen, verwenden wirunquote. Wenn wir einen Ausdruck deaktivieren, wird er ausgewertet und in den AST injiziert. Betrachten wir ein Beispiel (in der IEX-Shell), um das Konzept zu verstehen -

num = 25

quote do: sum(15, num)

quote do: sum(15, unquote(num))

Wenn das obige Programm ausgeführt wird, wird das folgende Ergebnis erzeugt:

{:sum, [], [15, {:num, [], Elixir}]}
{:sum, [], [15, 25]}

Im Beispiel für den Anführungszeichenausdruck wurde num nicht automatisch durch 25 ersetzt. Wir müssen diese Variable aus dem Anführungszeichen entfernen, wenn wir den AST ändern möchten.

Makros

Nachdem wir nun mit Anführungszeichen und Anführungszeichen vertraut sind, können wir die Metaprogrammierung in Elixir mithilfe von Makros untersuchen.

Im einfachsten Fall sind Makros spezielle Funktionen, die einen Ausdruck in Anführungszeichen zurückgeben, der in unseren Anwendungscode eingefügt wird. Stellen Sie sich vor, das Makro wird durch den Ausdruck in Anführungszeichen ersetzt und nicht wie eine Funktion aufgerufen. Mit Makros haben wir alles Notwendige, um Elixir zu erweitern und unseren Anwendungen dynamisch Code hinzuzufügen

Lassen Sie uns implementieren, es sei denn als Makro. Wir beginnen mit der Definition des Makros mit demdefmacroMakro. Denken Sie daran, dass unser Makro einen Ausdruck in Anführungszeichen zurückgeben muss.

defmodule OurMacro do
   defmacro unless(expr, do: block) do
      quote do
         if !unquote(expr), do: unquote(block)
      end
   end
end

require OurMacro

OurMacro.unless true, do: IO.puts "True Expression"

OurMacro.unless false, do: IO.puts "False expression"

Wenn das obige Programm ausgeführt wird, wird das folgende Ergebnis erzeugt:

False expression

Was hier passiert, ist, dass unser Code durch den zitierten Code ersetzt wird, der vom Makro "Es sei denn" zurückgegeben wird . Wir haben den Ausdruck aufgehoben, um ihn im aktuellen Kontext auszuwerten, und den do-Block, um ihn in seinem Kontext auszuführen, nicht in Anführungszeichen gesetzt. Dieses Beispiel zeigt uns die Metaprogrammierung mit Makros im Elixier.

Makros können in viel komplexeren Aufgaben verwendet werden, sollten jedoch sparsam verwendet werden. Dies liegt daran, dass Metaprogrammierung im Allgemeinen als schlechte Praxis angesehen wird und nur bei Bedarf verwendet werden sollte.

Elixir bietet eine hervorragende Interoperabilität mit Erlang-Bibliotheken. Lassen Sie uns kurz einige Bibliotheken besprechen.

Das Binärmodul

Das integrierte Elixir String-Modul verarbeitet Binärdateien, die UTF-8-codiert sind. Das Binärmodul ist nützlich, wenn Sie mit Binärdaten arbeiten, die nicht unbedingt UTF-8-codiert sind. Betrachten wir ein Beispiel, um das Binärmodul besser zu verstehen.

# UTF-8
IO.puts(String.to_char_list("Ø"))

# binary
IO.puts(:binary.bin_to_list "Ø")

Wenn das obige Programm ausgeführt wird, wird das folgende Ergebnis erzeugt:

[216]
[195, 152]

Das obige Beispiel zeigt den Unterschied; Das String-Modul gibt UTF-8-Codepunkte zurück, während: binary Binärdatenbytes behandelt.

Das Kryptomodul

Das Kryptomodul enthält Hashing-Funktionen, digitale Signaturen, Verschlüsselung und mehr. Dieses Modul ist nicht Teil der Erlang-Standardbibliothek, sondern in der Erlang-Distribution enthalten. Dies bedeutet, dass Sie bei jeder Verwendung Folgendes auflisten müssen: Krypto in der Anwendungsliste Ihres Projekts. Sehen wir uns ein Beispiel mit dem Kryptomodul an -

IO.puts(Base.encode16(:crypto.hash(:sha256, "Elixir")))

Wenn das obige Programm ausgeführt wird, wird das folgende Ergebnis erzeugt:

3315715A7A3AD57428298676C5AE465DADA38D951BDFAC9348A8A31E9C7401CB

Das Digraph-Modul

Das Digraph-Modul enthält Funktionen für den Umgang mit gerichteten Graphen, die aus Eckpunkten und Kanten bestehen. Nach dem Erstellen des Diagramms helfen die darin enthaltenen Algorithmen dabei, beispielsweise den kürzesten Pfad zwischen zwei Scheitelpunkten oder Schleifen im Diagramm zu finden. Beachten Sie, dass die Funktionenin :digraph Ändern Sie die Diagrammstruktur indirekt als Nebeneffekt, während Sie die hinzugefügten Scheitelpunkte oder Kanten zurückgeben.

digraph = :digraph.new()
coords = [{0.0, 0.0}, {1.0, 0.0}, {1.0, 1.0}]
[v0, v1, v2] = (for c <- coords, do: :digraph.add_vertex(digraph, c))
:digraph.add_edge(digraph, v0, v1)
:digraph.add_edge(digraph, v1, v2)
for point <- :digraph.get_short_path(digraph, v0, v2) do 
   {x, y} = point
   IO.puts("#{x}, #{y}")
end

Wenn das obige Programm ausgeführt wird, wird das folgende Ergebnis erzeugt:

0.0, 0.0
1.0, 0.0
1.0, 1.0

Das Mathematikmodul

Das Mathematikmodul enthält allgemeine mathematische Operationen, die Trigonometrie-, Exponential- und Logarithmusfunktionen abdecken. Betrachten wir das folgende Beispiel, um zu verstehen, wie das Mathematikmodul funktioniert:

# Value of pi
IO.puts(:math.pi())

# Logarithm
IO.puts(:math.log(7.694785265142018e23))

# Exponentiation
IO.puts(:math.exp(55.0))

#...

Wenn das obige Programm ausgeführt wird, wird das folgende Ergebnis erzeugt:

3.141592653589793
55.0
7.694785265142018e23

Das Warteschlangenmodul

Die Warteschlange ist eine Datenstruktur, die (doppelendige) FIFO-Warteschlangen (First-In First-Out) effizient implementiert. Das folgende Beispiel zeigt, wie ein Warteschlangenmodul funktioniert -

q = :queue.new
q = :queue.in("A", q)
q = :queue.in("B", q)
{{:value, val}, q} = :queue.out(q)
IO.puts(val)
{{:value, val}, q} = :queue.out(q)
IO.puts(val)

Wenn das obige Programm ausgeführt wird, wird das folgende Ergebnis erzeugt:

A
B