Неизменяемость и привязка имён

В Elixir = — это не присваивание, а оператор сопоставления. Понять это — значит понять половину языка.

«Переменная» в Elixir — лишь имя, временно указывающее на неизменяемое значение. Само значение изменить нельзя никогда.

В императивных языках x = x + 1 меняет ячейку памяти. В Elixir значения неизменяемы: вы не меняете данные, вы создаёте новые и привязываете к ним имена.

x = 1          # привязали имя x к значению 1
x = x + 1      # x теперь указывает на НОВОЕ значение 2,
               # старая единица никуда не делась

list = [1, 2, 3]
new_list = [0 | list]   # [0, 1, 2, 3]
# list по-прежнему [1, 2, 3] — он не изменился

Оператор = сопоставляет правую часть с шаблоном слева. Если слева переменная — она привязывается. Чтобы сравнить со значением переменной, а не перепривязать её, ставят пин ^:

x = 1
^x = 1     # ok: сопоставление, 1 == 1
^x = 2     # ** MatchError: 2 не равно 1

Как работает под капотом (BEAM)

Неизменяемость — фундамент конкурентности на BEAM. Раз значение нельзя изменить, два процесса могут читать его без блокировок и гонок: данные просто не могут «поменяться под рукой». Когда процесс отправляет сообщение другому, значение копируется в кучу получателя (либо разделяется для крупных бинарей), и каждый работает со своей неизменяемой копией. Поэтому в Elixir нет мьютексов вокруг данных — нечего защищать.

  Процесс A         Процесс B
  [data]   --copy-->  [data']
  Никакой общей мутируемой памяти.
  Нет гонок -> нет блокировок -> нет дедлоков.

Та же идея на Python ▶

Покажем разницу между мутацией и неизменяемым обновлением — это и есть стиль Elixir.

# "Изменяемый" подход (НЕ в духе Elixir)
mutable = [1, 2, 3]
mutable.append(4)        # старый список изменён
print(mutable)           # [1, 2, 3, 4]

# Неизменяемый подход (как в Elixir): создаём НОВЫЙ
original = (1, 2, 3)              # tuple неизменяем
new_one = (0,) + original        # новый объект
print(original)                  # (1, 2, 3) — цел
print(new_one)                   # (0, 1, 2, 3)

# = как привязка имени к новому значению
x = 1
x = x + 1
print(x)                         # 2 (старая 1 не "изменилась")

Частые ошибки

  • Думать, что функции меняют аргументы. Никакая функция не изменит переданный список — она вернёт новый.
  • Забыть присвоить результат. List.delete(list, 2) без присваивания бесполезен — результат пропадёт.
  • Случайно перепривязать переменную. Внутри case/if легко затереть имя; используйте ^, когда хотите именно сравнить.

Best practices

  • Принимайте неизменяемость как преимущество: код легче рассуждать, тестировать и распараллеливать.
  • Используйте пин ^, чтобы матчить по значению переменной, а не перепривязывать её.
  • Стройте логику как поток «значение → новое значение», а не «изменить состояние».

Итог. Неизменяемость — не ограничение, а суперспособность: она убирает целый класс багов и делает безопасной конкурентность. А = как оператор сопоставления подводит нас к центральной теме языка — паттерн-матчингу.

Почему это упрощает рассуждения о коде

Неизменяемость даёт неочевидную, но огромную выгоду: локальность рассуждений. Если вы передали значение в функцию, вы знаете наверняка, что оно не изменится — функция физически не может его «испортить». Значит, читая код, вам не нужно держать в голове, кто и где мог поменять ваши данные за вашей спиной. В языках с мутацией это постоянный источник багов: список, переданный в библиотеку, неожиданно оказался изменён.

Эта же гарантия делает безопасным распараллеливание. Раз значение нельзя изменить, его могут одновременно читать сколько угодно процессов без единой блокировки. Именно поэтому в Elixir тема «потокобезопасности структур данных» попросту не возникает — все структуры данных потокобезопасны по построению. Цена — небольшой расход памяти на создание новых версий вместо изменения старых, но благодаря structural sharing и эффективному сборщику мусора на маленьких кучах эта цена на практике мизерна, а выгода в надёжности и конкурентности — колоссальна.

Проверьте себя
1. Что делает оператор = в Elixir?
AПрисваивает значение переменной, меняя её ячейку
BСопоставляет правую часть с шаблоном слева и привязывает переменные
CСравнивает на равенство как ==
DСоздаёт указатель
2. Зачем нужен пин-оператор ^ перед переменной?
AЧтобы ускорить матчинг
BЧтобы сопоставить по текущему значению переменной, а не перепривязывать её
CЧтобы объявить константу
DЧтобы скопировать значение