Подтвердить что ты не робот

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

В книге Дейва Томаса "Программирование эликсира" он утверждает, что "Эликсир наставляет неизменные данные" и продолжает:

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

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

4b9b3361

Ответ 1

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

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

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

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}>

Ответ 2

Не думайте о "переменных" в 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 вы можете перепроверять переменные (изменить значение "label" ), в основном для вашего удобства:

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"

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

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

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

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

Ответ 3

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

Переменные не являются непреложными. Данные, на которые они указывают, являются непреложными. Поэтому изменение переменной называется переделкой.

Вы указываете на что-то еще, не изменяя то, на что оно указывает.

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

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

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

Скажем, у вас есть список

l = [1, 2, 3]

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

send(worker, {:dostuff, l})

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

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

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

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

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

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

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

Это невозможно в Эрланге и в добавок Эликсир. Что означает неизменность здесь.

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

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

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

Код часто выглядит так:

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

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

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

immutability-in-elixir

Ответ 4

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

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