Sind Elixiervariablen wirklich unveränderlich?

72

In Dave Thomas 'Buch Programming Elixir erklärt er "Elixir erzwingt unveränderliche Daten" und fährt fort:

Wenn eine Variable in Elixir auf eine Liste wie [1,2,3] verweist, wissen Sie, dass sie immer auf dieselben Werte verweist (bis Sie die Variable erneut binden).

Das klingt wie "es wird sich nie ändern, wenn Sie es nicht ändern", also bin ich verwirrt darüber, was der Unterschied zwischen Veränderlichkeit und erneuter Bindung ist. Ein Beispiel, das die Unterschiede hervorhebt, wäre sehr hilfreich.

Odhran Roche
quelle
Es ist Schatten, keine Neuzuweisung.
Rechtsfalte

Antworten:

60

Unveränderlichkeit bedeutet, dass sich Datenstrukturen nicht ändern. Zum Beispiel gibt die Funktion HashSet.neweine leere Menge zurück und solange Sie an der Referenz auf diese Menge festhalten, wird sie niemals nicht leer. Was Sie in Elixir jedoch tun können , ist, eine variable Referenz auf etwas wegzuwerfen und sie erneut an eine neue Referenz zu binden. Zum Beispiel:

s = HashSet.new
s = HashSet.put(s, :element)
s # => #HashSet<[:element]>

Was nicht passieren kann , ist, dass sich der Wert unter dieser Referenz ändert, ohne dass Sie ihn explizit neu binden:

s = HashSet.new
ImpossibleModule.impossible_function(s)
s # => #HashSet<[:element]> will never be returned, instead you always get #HashSet<[]>

Vergleichen Sie dies mit Ruby, wo Sie Folgendes tun können:

s = Set.new
s.add(:element)
s # => #<Set: {:element}>
Paweł Obrok
quelle
1
Ist eine erneute Bindung also nur eine lokale Veränderlichkeit? Innerhalb eines Blocks können Sie eine Variable erneut binden, aber sobald der Gültigkeitsbereich überschritten ist, kehrt die Variable zu ihrem ursprünglichen Wert zurück - stimmt das?
Odhran Roche
1
Nur lokal - ja. Wenn die Variable jedoch den Gültigkeitsbereich verlässt, hört sie einfach auf zu existieren. Die Daten, auf die die Variable zeigte, müssen nicht unbedingt (können beispielsweise von der Funktion zurückgegeben werden) und diese Daten sind unveränderlich.
Paweł Obrok
1
Wenn Sie unveränderliche Variablen möchten, müssen Sie Erlang verwenden oder der Elixir-Variablen das Präfix hinzufügen ^. Rebind ist in Elixir nur ein ausgefallener Begriff, um zu verbergen, dass eine Variable tatsächlich veränderlich ist. Zu bedenken, dass ich Elixir liebe, aber ich mag es wirklich nicht, wenn die Community versucht, die Veränderlichkeit der Variablen hinter ausgefallenen Begriffen und Erklärungen zu verbergen.
Exadra37
1
@ Exadra37 Nicht so. Sie können sich den Pin-Operator ^in einem Ausdruck so vorstellen, dass er nur die Variable durch ihren Wert ersetzt. Dieser Operator findet den größten Teil seiner Verwendung beim Mustervergleich usw. Was die Unveränderlichkeit betrifft, so ist Elixir in jeder Hinsicht unveränderlich. Wenn der Wert einer Variablen geändert werden soll, kann der Wert, der an der Stelle gespeichert ist, auf die diese Variable zeigt, nicht selbst zu einem anderen Wert mutiert werden. Stattdessen wird die Variable an einen neuen Speicherort zurückgebunden, an dem der neue Wert gespeichert werden kann. Daher der Begriff "neu binden".
Sri Kadimisetty
1
@sri Ich interessiere mich nicht für die Implementierungsdetails, auch bekannt als die Funktionsweise des Kerns, ich interessiere mich für die Schnittstelle, auch bekannt als die Art und Weise, wie sie verwendet werden kann. Wenn ich also dieselbe Variable zweimal verwenden und einen anderen Wert erhalten kann, dann für mich ist nicht unveränderlich, sondern veränderlich. Jetzt können Sie mit allen gewünschten technischen Erklärungen kommen, und ich kenne viele davon, aber das wird nie ändern, dass die Elixir-API zur Verwendung von Variablen veränderbare Variablen zulässt, die Erlang-API zur Verwendung von Variablen jedoch nur unveränderliche Variablen zulässt .
Exadra37
94

Stellen Sie sich "Variablen" in Elixir nicht als Variablen in imperativen Sprachen vor, "Leerzeichen für Werte". Betrachten Sie sie eher als "Bezeichnungen für Werte".

Vielleicht sollten Sie es besser verstehen, wenn Sie sich ansehen, wie Variablen ("Labels") in Erlang funktionieren. Immer wenn Sie ein "Label" an einen Wert binden, bleibt es für immer daran gebunden (hier gelten natürlich die Bereichsregeln).

In Erlang kann man das nicht schreiben:

v = 1,      % value "1" is now "labelled" "v"
            % wherever you write "1", you can write "v" and vice versa
            % the "label" and its value are interchangeable

v = v+1,    % you can not change the label (rebind it)
v = v*10,   % you can not change the label (rebind it)

Stattdessen müssen Sie Folgendes schreiben:

v1 = 1,       % value "1" is now labelled "v1"
v2 = v1+1,    % value "2" is now labelled "v2"
v3 = v2*10,   % value "20" is now labelled "v3"

Wie Sie sehen, ist dies sehr unpraktisch, hauptsächlich für das Refactoring von Code. Wenn Sie nach der ersten Zeile eine neue Zeile einfügen möchten, müssen Sie alle v * neu nummerieren oder etwas wie "v1a = ..." schreiben.

In Elixir können Sie also Variablen neu binden (die Bedeutung des "Labels" ändern), hauptsächlich zur Vereinfachung:

v = 1       # value "1" is now labelled "v"
v = v+1     # label "v" is changed: now "2" is labelled "v"
v = v*10    # value "20" is now labelled "v"

Zusammenfassung: In imperativen Sprachen sind Variablen wie benannte Koffer: Sie haben einen Koffer mit dem Namen "v". Zuerst legst du ein Sandwich hinein. Dann legst du einen Apfel hinein (das Sandwich ist verloren und wird vielleicht vom Müllsammler gegessen). In Erlang und Elixir ist die Variable kein Ort, an dem etwas eingefügt werden kann. Sie ist nur ein Name / eine Bezeichnung für einen Wert. In Elixir können Sie die Bedeutung der Bezeichnung ändern. In Erlang können Sie nicht.Dies ist der Grund, warum es weder in Erlang noch in Elixir sinnvoll ist, "Speicher für eine Variable zuzuweisen", da Variablen keinen Speicherplatz belegen. Werte tun.Jetzt sehen Sie vielleicht den Unterschied deutlich.

Wenn Sie tiefer graben möchten:

1) Sehen Sie sich an, wie "ungebundene" und "gebundene" Variablen in Prolog funktionieren. Dies ist die Quelle dieses vielleicht etwas seltsamen Erlang-Konzepts von "Variablen, die nicht variieren".

2) Beachten Sie, dass "=" in Erlang wirklich kein Zuweisungsoperator ist, sondern nur ein Übereinstimmungsoperator! Wenn Sie eine ungebundene Variable mit einem Wert abgleichen, binden Sie die Variable an diesen Wert. Das Abgleichen einer gebundenen Variablen entspricht dem Abgleichen eines Werts, an den sie gebunden ist. Dies führt also zu einem Übereinstimmungsfehler :

v = 1,
v = 2,   % in fact this is matching: 1 = 2

3) Bei Elixir ist das nicht der Fall. In Elixir muss es also eine spezielle Syntax geben, um den Abgleich zu erzwingen:

v = 1
v = 2   # rebinding variable to 2
^v = 3  # matching: 2 = 3 -> error
Miroslav Prymek
quelle
1
@ DavidC: Dies sind zwei Fragen: 1. Ist es möglich / vorstellbar? 2. Ist es eine gute Idee? Ich denke, die Antwort für 1 ist "Ja" und für 2 "Nein". Es gibt sehr gute Funktionsmechanismen (Map, Reduce, ...), die imho besser geeignet sind. Aber diese Frage ist weit
gefasst
@MiroslavPrymek bei Punkt 3 Ihrer Antwort, gibt es Fälle in Elixir, in denen das Matching streng erzwungen würde?
Simo
@simo Nein, das weiß ich. Das Handbuch sagt auch nichts dergleichen: elixir-lang.org/getting-started/pattern-matching.html
Miroslav Prymek
4
x = x +1tut meinem mathematischen Gehirn weh. Erlang stoppt diese Kopfschmerzen und Elixir nicht. :(
Kevin Monk
38

Erlang und offensichtlich Elixier, das darauf aufgebaut ist, umfasst Unveränderlichkeit. Sie erlauben einfach nicht, dass sich Werte an einem bestimmten Speicherort ändern.Niemals, bis die Variable Müll gesammelt bekommt oder außerhalb des Gültigkeitsbereichs liegt.

Variablen sind nicht unveränderlich. Die Daten, auf die sie verweisen, sind unveränderlich. Aus diesem Grund wird das Ändern einer Variablen als erneutes Binden bezeichnet.

Sie richten es auf etwas anderes und ändern nicht das, worauf es zeigt.

x = 1gefolgt von x = 2ändert nichts an den im Computerspeicher gespeicherten Daten, bei denen die 1 eine 2 war. Es setzt eine 2 an eine neue Stelle und zeigt xdarauf.

x ist jeweils nur für einen Prozess zugänglich, sodass dies keine Auswirkungen auf die Parallelität hat. Parallelität ist der wichtigste Ort, um sich überhaupt darum zu kümmern, ob etwas ohnehin unveränderlich ist.

Durch das erneute Binden wird der Status eines Objekts überhaupt nicht geändert. Der Wert befindet sich immer noch am selben Speicherort. Die Bezeichnung (Variable) verweist jetzt auf einen anderen Speicherort, sodass die Unveränderlichkeit erhalten bleibt. Das erneute Binden ist in Erlang nicht verfügbar, aber während es sich in Elixir befindet, bremst dies dank seiner Implementierung keine von der Erlang-VM auferlegten Einschränkungen. Die Gründe für diese Wahl werden von Josè Valim in diesem Kern gut erklärt .

Angenommen, Sie hatten eine Liste

l = [1, 2, 3]

und Sie hatten einen anderen Prozess, der Listen nahm und dann wiederholt "Sachen" gegen sie ausführte und sie während dieses Prozesses zu ändern, wäre schlecht. Sie könnten diese Liste wie senden

send(worker, {:dostuff, l})

Jetzt möchte Ihr nächster Code möglicherweise l mit mehr Werten für weitere Arbeiten aktualisieren, die nichts mit dem zu tun haben, was dieser andere Prozess tut.

l = l ++ [4, 5, 6]

Oh nein, jetzt wird dieser erste Prozess ein undefiniertes Verhalten haben, weil Sie die Liste geändert haben, oder? Falsch.

Diese ursprüngliche Liste bleibt unverändert. Was Sie wirklich getan haben, war, eine neue Liste basierend auf der alten zu erstellen und l wieder an diese neue Liste zu binden.

Der separate Prozess hat niemals Zugriff auf l. Die Daten, auf die ich ursprünglich verwiesen habe, sind unverändert, und der andere Prozess (vermutlich, sofern er nicht ignoriert wird) hat einen eigenen Verweis auf diese ursprüngliche Liste.

Was zählt, ist, dass Sie Daten nicht prozessübergreifend gemeinsam nutzen und dann ändern können, während ein anderer Prozess sie betrachtet. In einer Sprache wie Java, in der Sie einige veränderbare Typen haben (alle primitiven Typen plus Referenzen selbst), ist es möglich, eine Struktur / ein Objekt, die beispielsweise ein int enthält, gemeinsam zu nutzen und dieses int von einem Thread zu ändern, während ein anderer es liest.

Tatsächlich ist es möglich, einen großen Integer-Typ in Java teilweise zu ändern, während er von einem anderen Thread gelesen wird. Zumindest war es früher nicht sicher, ob sie diesen Aspekt der Dinge mit dem 64-Bit-Übergang eingeklemmt haben. Der Punkt ist jedenfalls, dass Sie den Teppich unter anderen Prozessen / Threads herausziehen können, indem Sie Daten an einer Stelle ändern, die beide gleichzeitig betrachten.

Das ist in Erlang und im weiteren Sinne in Elixir nicht möglich. Das bedeutet hier Unveränderlichkeit.

Um etwas genauer zu sein, in Erlang (die Originalsprache für VM Elixir läuft darauf) waren alles unveränderliche Variablen mit einfacher Zuweisung, und Elixir verbirgt ein Muster, das Erlang-Programmierer entwickelt haben, um dies zu umgehen.

Wenn in Erlang a = 3 ist, ist dies der Wert von a für die Dauer der Existenz dieser Variablen, bis sie aus dem Gültigkeitsbereich ausfällt und Müll gesammelt wird.

Dies war manchmal nützlich (nichts ändert sich nach der Zuweisung oder Musterübereinstimmung, so dass es leicht zu überlegen ist, was eine Funktion tut), aber auch etwas umständlich, wenn Sie während des Kurses, in dem eine Funktion ausgeführt wird, mehrere Dinge mit einer Variablen oder Sammlung tun.

Code würde oft so aussehen:

A=input, 
A1=do_something(A), 
A2=do_something_else(A1), 
A3=more_of_the_same(A2)

Dies war etwas klobig und machte das Refactoring schwieriger als nötig. Elixir tut dies hinter den Kulissen, versteckt es jedoch über Makros und Code-Transformationen, die vom Compiler ausgeführt werden, vor dem Programmierer.

Tolle Diskussion hier

Unveränderlichkeit im Elixier

Subhash Chandra
quelle
6
+1 sehr klare Antwort. Es erklärt sehr gut, welche Compilertechnik der Unveränderlichkeit und ihren Gründen zugrunde liegt. Diese Antwort zusammen mit der Antwort von Prymek gab mir endlich ein gutes Verständnis für diese Angelegenheit. Beide sollten Teil der offiziellen Elixier-Dokumentation sein.
Guido
@subhash, sagst du also, wenn du es x = 1; f = fn -> x end; x = 2; #=> 2in Elixir machst , fgreift das Lambda nicht einmal über die x-Variable zu x, sondern über eine sekundäre Referenz? Ist es so möglich, immer noch auf die 1 zuzugreifen, obwohl x auf 2 gesetzt wird? Mir ist klar, dass ein Teil davon vom Elixier stammt, das es sofort kompiliert, aber abgesehen von dieser Tatsache, gibt es eine sekundäre Referenz, die verwendet wird, um auch nach dem erneuten Binden auf Daten zuzugreifen?
Tallboy
1
Diese Antwort unter anderem hier haben größte Anstrengungen, um das Thema zu erklären
Srle
1
Das ist die gute Erklärung für mich! Variablen sind die veränderbare Sache in Erlang. Der Schlüssel ist zu verstehen, dass Erlang jede Variable erzwingt, die nur von dem Thread verwendet wird, den sie erstellt hat, sodass in dieser veränderlichen Sache keine Parallelität auftreten kann.
Pauls
1
Toller Beitrag, es ist klar genug, vor allem: Elixir macht das hinter den Kulissen, versteckt es aber über Makros und Code-Transformationen, die vom Compiler ausgeführt werden, vor dem Programmierer.
Ace.Yin
2

Die Variablen sind wirklich im Sinne unveränderlich, jede neue Neubindung (Zuweisung) ist nur für den Zugriff sichtbar, der danach kommt. Alle vorherigen Zugriffe beziehen sich immer noch auf alte Werte zum Zeitpunkt ihres Aufrufs.

foo = 1
call_1 = fn -> IO.puts(foo) end

foo = 2
call_2 = fn -> IO.puts(foo) end

foo = 3
foo = foo + 1    
call_3 = fn -> IO.puts(foo) end

call_1.() #prints 1
call_2.() #prints 2
call_3.() #prints 4
Ragavan
quelle
0

Um es sehr einfach zu machen

Variablen in Elixir sind nicht wie Container, in denen Sie ständig Elemente zum Container hinzufügen, entfernen oder ändern.

Stattdessen ähneln sie Beschriftungen, die an einen Container angehängt sind. Wenn Sie eine Variable neu zuweisen, ist dies so einfach, dass Sie eine Beschriftung aus einem Container auswählen und auf einem neuen Container mit den erwarteten Daten platzieren.

Ravi D.
quelle