Dave Thomasの著書 『Programming Elixir』で、彼は「Elixirは不変データを強制する」と述べ、続けて述べています。
Elixirでは、変数が[1,2,3]などのリストを参照すると、(変数を再バインドするまで)常に同じ値を参照することがわかります。
これは「変更しない限り変更されない」ように聞こえるので、可変性と再バインドの違いが何であるかについて混乱しています。違いを強調する例は本当に役立ちます。
不変性とは、データ構造が変わらないことを意味します。たとえば、関数HashSet.new
は空のセットを返します。そのセットへの参照を保持している限り、空ではなくなりません。 Elixirでcanすることは、何かへの変数参照を破棄し、それを新しい参照に再バインドすることです。例えば:
s = HashSet.new
s = HashSet.put(s, :element)
s # => #HashSet<[:element]>
cannotが起こるのは、明示的に再バインドせずにその参照の下の値が変化することです:
s = HashSet.new
ImpossibleModule.impossible_function(s)
s # => #HashSet<[:element]> will never be returned, instead you always get #HashSet<[]>
これをRubyと比較すると、次のようなことができます。
s = Set.new
s.add(:element)
s # => #<Set: {:element}>
Elixirの「変数」を命令型言語の変数、「値のスペース」と考えないでください。むしろ「値のラベル」としてそれらを見てください。
Erlangで変数(「ラベル」)がどのように機能するかを見ると、よりよく理解できるでしょう。 「ラベル」を値にバインドするたびに、そのラベルは永久にバインドされたままになります(もちろん、スコープルールがここに適用されます)。
Erlangでは、cannotできません:
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)
代わりに、これを書く必要があります:
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"
ご覧のとおり、これは主にコードのリファクタリングのために非常に不便です。最初の行の後に新しい行を挿入する場合は、すべてのv *の番号を変更するか、「v1a = ...」などのように記述する必要があります。
したがって、Elixirでは、主に便宜上、変数を再バインド(「ラベル」の意味を変更)できます。
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"
Summary:命令型言語では、変数は名前付きスーツケースのようなものです。「v」という名前のスーツケースがあります。最初にサンドイッチを入れます。その中にAppleを入れます(サンドイッチは失われ、ガベージコレクターによって食べられる可能性があります)。ErlangとElixirでは、変数はa placeではありませんは何かを入れるためのものです。aname/labelの値です。Elixirではラベルの意味を変更できますが、Erlangではできません。これは、変数がスペースを占有しないため、ErlangまたはElixirで「変数にメモリを割り当てる」ことが意味をなさない理由です。値はそうなります。違いがはっきりとわかります。
さらに掘り下げたい場合:
1)Prologで「非バインド」変数と「バインド」変数がどのように機能するかを見てください。これは、「変らない変数」という少し奇妙なアーランの概念のソースです。
2)Erlangの「=」は実際には代入演算子ではなく、単なる一致演算子です。バインドされていない変数を値と一致させる場合、変数をその値にバインドします。バインドされた変数の一致は、バインドされた値の一致に似ています。したがって、これはmatchエラーを生成します:
v = 1,
v = 2, % in fact this is matching: 1 = 2
3)Elixirではそうではありません。そのため、Elixirでは、強制的に一致させるための特別な構文が必要です。
v = 1
v = 2 # rebinding variable to 2
^v = 3 # matching: 2 = 3 -> error
Erlangと、その上に構築された明らかにElixirは、不変性を受け入れます。 特定のメモリ位置にある値を変更することはできません。変数がガベージコレクションされるか、スコープ外になるまで。
変数は不変のものではありません。彼らが指すデータは不変のものです。そのため、変数の変更は再バインドと呼ばれます。
あなたはそれが指しているものを変えないで、それを他の何かに向けています。
x = 1
に続く x = 2
は、1が2だったコンピューターのメモリーに保存されているデータを変更しません。2を新しい場所に配置し、x
をポイントします。
x
は一度に1つのプロセスからのみアクセスできるため、同時性に影響はありません。また、同時性は、とにかく不変なものがある場合でも注意する主要な場所です。
再バインドはオブジェクトの状態をまったく変更せず、値は同じメモリ位置に残りますが、ラベル(変数)は別のメモリ位置を指すようになったため、不変性は保持されます。再バインドはErlangでは使用できませんが、ElixirではErlang VMの実装のおかげでErlang VMによって課せられた制約を解消することはできません。この選択の背後にある理由は、JosèValim この要点で によって十分に説明されています。
リストがあったとしましょう
l = [1, 2, 3]
また、リストを取得し、それらに対して繰り返し「スタッフ」を実行する別のプロセスがあり、このプロセス中にリストを変更するのは悪いことです。あなたはそのリストを
send(worker, {:dostuff, l})
さて、次のコードでは、他のプロセスが実行していることとは関係のないさらなる作業のために、より多くの値でlを更新する場合があります。
l = l ++ [4, 5, 6]
リストを正しく変更したため、最初のプロセスの動作は未定義になりましたか?違う。
元のリストは変更されません。あなたが本当にしたことは、古いリストに基づいて新しいリストを作成し、その新しいリストにlを再バインドすることでした。
別のプロセスがlにアクセスすることはありません。元々指していたデータlは変更されておらず、他のプロセス(おそらく無視しない限り)は、元のリストへの独自の参照を持っています。
重要なのは、プロセス間でデータを共有し、別のプロセスがデータを見ている間にそれを変更できないことです。 Javaのようないくつかの可変型(すべてのプリミティブ型と参照自体)がある言語では、intを含む構造体/オブジェクトを共有し、そのintを1つのスレッドから変更できます。別の人がそれを読んでいた間。
実際、Javaで別のスレッドが読み取っている間に、部分的に大きな整数型を変更することは可能です。とにかく、ポイントは、両方が同時に見ている場所でデータを変更することにより、他のプロセス/スレッドの下からラグを引き出すことができるということです。
それは、Erlangでも拡張Elixirでも不可能です。ここで不変性が意味するものです。
少し具体的に言うと、Erlang(VM Elixirが実行される)の元の言語)では、すべてが単一割り当ての不変変数であり、Elixirはこの問題を回避するために開発されたErlangプログラマーのパターンを隠しています。
Erlangでは、a = 3の場合、その変数がスコープから外れてガベージコレクションされるまで、その変数の存在期間中にaがその値になります。
これは時々役に立ちました(割り当てまたはパターンマッチの後に何も変わらないので、関数が何をしているのかを推論するのは簡単です)が、関数を実行しているコースで変数またはコレクションに複数のことをしている場合は少し面倒です。
多くの場合、コードは次のようになります。
A=input,
A1=do_something(A),
A2=do_something_else(A1),
A3=more_of_the_same(A2)
これは少し不格好で、リファクタリングが必要以上に困難になりました。 Elixirはこれを舞台裏で行っていますが、マクロとコンパイラによって実行されるコード変換を介してプログラマから隠しています。
変数は実際には不変であり、すべての新しい再バインド(割り当て)は、その後のアクセスにのみ表示されます。以前のアクセスはすべて、呼び出し時に古い値を参照し続けます。
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