Являются ли переменные эликсира действительно неизменными?


в книге Дэйва Томаса Programming Elixir он утверждает ,что "эликсир обеспечивает неизменные данные" и продолжает говорить:

в Elixir, как только переменная ссылается на список, такой как [1,2,3], вы знаете, что он всегда будет ссылаться на те же значения (пока вы не свяжете переменную).

Это звучит как "он никогда не изменится, если вы не измените его", поэтому я смущен тем, что разница между изменчивостью и повторным привязыванием. Пример, подчеркивающий различия было бы очень полезно.

4 59

4 ответа:

неизменяемость означает, что структуры данных не меняются. Например, функция HashSet.new возвращает пустой набор, и пока вы держите ссылку на этот набор, он никогда не станет непустым. Что ты можете сделать в Эликсире, хотя это выбросить переменную ссылку на что-то и привязать его к новой ссылке. Например:

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

что не может happen-это значение под этой ссылкой, изменяющееся без явной повторной привязки это:

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 вы не может писать так:

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"

резюме: в императивных языках переменные похожи на именованные чемоданы: у вас есть чемодан с именем "в." Сначала вы кладете в него бутерброд. Затем вы кладете в него яблоко (бутерброд теряется и, возможно, съедается сборщиком мусора). В Erlang и Elixir переменная не место чтобы положить что-то. Это просто имя/метка для значения. В Elixir вы можете изменить значение метки. В Эрланге вы не можете. вот почему не имеет смысла "выделять память для переменной" ни в Erlang, ни в Elixir, потому что переменные не занимают пространство. Значения. теперь, возможно, вы ясно видите разницу.

если вы хотите копать глубже:

1) Посмотрите, как" несвязанные "и" связанные " переменные работают в прологе. Это источник этой, может быть, немного странной концепции Эрланга "переменных, которые не меняются".

2) Обратите внимание, что "=" в Erlang действительно не оператор присваивания, это просто оператор соответствия! При сопоставлении несвязанной переменной со значением вы привязываете переменную к этому значению. Совмещение связанная переменная-это как сопоставление значения, к которому она привязана. Так что это даст матч ошибка:

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

3) это не так в Эликсире. Поэтому в Elixir должен быть специальный синтаксис для принудительного сопоставления:

v = 1
v = 2   # rebinding variable to 2
^v = 3  # matching: 2 = 3 -> error

Эрланг и, очевидно, эликсир, который построен поверх него, охватывает неизменность. они просто не позволяют значения в определенном месте памяти, чтобы изменить. никогда, пока переменная не соберет мусор или не выйдет из области видимости.

переменные не являются неизменяемой вещью. Данные, которые они указывают-это непреложная вещь. Вот почему изменение переменной называется повторной привязкой.

вы указываете на что-то другое, не меняя то, на что он указывает.

x = 1 следовал по x = 2 не изменяет данные, хранящиеся в памяти компьютера, где был 1 к 2. Он ставит 2 в новом месте и указывает x на него.

x доступно только для одного процесса за раз, поэтому это не влияет на параллелизм, а параллелизм-это главное место, чтобы даже заботиться, если что-то неизменное в любом случае.

повторная привязка не изменяет состояние объекта вообще, значение все еще находится в том же месте памяти, но это метка (переменная) теперь указывает на другую ячейку памяти, поэтому неизменность сохраняется. Повторное связывание недоступно в Erlang, но пока оно находится в Elixir, это не тормозит никаких ограничений, наложенных Erlang VM, благодаря его реализации. Причины этого выбора хорошо объяснил Жозе Валим в этом суть .

допустим, у вас был список

l = [1, 2, 3]

и у вас был еще один процесс, который принимал списки, а затем выполнял "вещи" против их многократно и менять их во время этого процесса было бы плохо. Вы можете отправить этот список как

send(worker, {:dostuff, l})

теперь ваш следующий бит кода может захотеть обновить l с большим количеством значений для дальнейшей работы, которая не связана с тем, что делает этот другой процесс.

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

О нет, теперь, когда первый процесс будет иметь неопределенное поведение, потому что вы изменили список справа? Неправильный.

этот исходный список остается неизменным. Что вы действительно сделали, так это составили новый список основываясь на старом и повторно привязать l к этому новому списку.

отдельный процесс никогда не имеет доступа к l. данные, на которые первоначально указывал l, не изменяются, а другой процесс (предположительно, если он не проигнорировал его) имеет свою собственную отдельную ссылку на этот исходный список.

важно то, что вы не можете обмениваться данными между процессами, а затем изменить его, а другой процесс, глядя на него. В языке, как Java, где у вас есть некоторые изменяемые типы (все примитивные типы, плюс ссылки можно было бы разделить структуру / объект, который содержал бы сказать int и изменить этот int из одного потока, пока другой читал его.

фактически, можно частично изменить большой целочисленный тип в java, пока он читается другим потоком. Или, по крайней мере, раньше это было, не уверен, что они зажали этот аспект вещей с 64-битным переходом. В любом случае, вы можете вытащить ковер из-под других процессов / потоков, изменив данные в месте, которое оба смотрят одновременно.

это невозможно в Эрланге и по расширению эликсира. Вот что здесь означает неизменность.

чтобы быть немного более конкретным, в Erlang (исходный язык для эликсира VM работает) все было однонаправленными неизменяемыми переменными, и Elixir скрывает шаблон, разработанный программистами Erlang для работы вокруг этого.

в Эрланге, если a=3, то это было то, что a будет его значением в течение этого времени существование переменной до тех пор, пока она не выпала из области видимости и не была собрана мусор.

это было полезно время от времени (ничего не меняется после присвоения или соответствия шаблону, поэтому легко рассуждать о том, что делает функция), но также немного громоздко, если вы делали несколько вещей с переменной или коллекцией в течение выполнения функции.

код часто будет выглядеть так:

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

это было немного неуклюже и сделало рефакторинг более сложным, чем так и должно было быть. Эликсир делает это за кулисами, но скрывает это от программиста с помощью макросов и преобразований кода, выполняемых компилятором.

отличная дискуссия здесь

неизменность в эликсире

переменные действительно неизменяемы в смысле, каждый новый rebinding (назначение) виден только для доступа, который приходит после этого. Все предыдущие обращения, все еще ссылаются на старые значения во время их вызова.

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