Неизменяемые данные и одно присваивание
Главное правило, которое отличает Erlang от привычных языков.
Одно присваивание (single assignment) — переменная в Erlang связывается со значением один раз и больше не меняется в пределах своей области видимости.
В большинстве языков переменная — это ящик, в который можно класть разные значения по очереди. В Erlang переменная — это имя для значения, и однажды связав имя со значением, изменить его уже нельзя. Это называется неизменяемостью (immutability), и из неё вытекает удивительно много хорошего. Ближе к математике, чем к привычному программированию: в математике, если мы написали «пусть x равно 5», то x не «станет потом» шестёркой в середине доказательства — это была бы бессмыслица. Erlang относится к переменным ровно так же серьёзно.
Поначалу это раздражает почти каждого, кто пришёл из императивных языков. «Как же мне теперь увеличить счётчик в цикле?» — первый вопрос новичка. Ответ в том, что и циклов в привычном виде здесь нет: их заменяет рекурсия, где каждый шаг — это новый вызов функции с новыми значениями параметров. Так что неизменяемость — не отдельное капризное правило, а часть цельного мировоззрения языка, в котором вычисление есть преобразование одних неизменяемых значений в другие, а не порча значений на месте.
Связывание, а не присваивание
Имена переменных в Erlang начинаются с заглавной буквы. Знак = — это не «присвоить», а «сопоставить»: связать левую часть с правой. Эта мелочь с заглавной буквы на самом деле фундаментальна для синтаксиса: именно по регистру первой буквы компилятор отличает переменную (X, Result, UserName) от атома — константы (ok, error). Перепутав регистр, вы получите не то, что хотели, поэтому правило стоит закрепить с самого начала.
1> X = 5.
5
2> X = 5.
5
3> X = 6.
** exception error: no match of right hand side value 6
Первая строка связала X с пятёркой. Вторая строка не ошибка: мы сопоставили X (уже равный 5) с числом 5 — совпало. А третья строка пытается сопоставить X (равный 5) с числом 6 — не совпало, отсюда ошибка no match. Это и есть pattern matching, о котором будет отдельный урок. Здесь важно прочувствовать сдвиг в восприятии: знак = работает в обе стороны как утверждение о равенстве, а не как команда «положи правое в левое». Если слева стоит ещё не связанная переменная, утверждение делается истинным через связывание; если переменная уже связана, утверждение просто проверяется. В одном операторе уживаются и присваивание, и сравнение — в зависимости от того, что слева.
Как же тогда «менять» данные
Раз изменять нельзя, мы создаём новые значения на основе старых. Хотите «изменить» список — постройте новый список. Это звучит расточительно, но виртуальная машина умеет переиспользовать неизменяемые структуры, не копируя их целиком. Подумайте о том, как мы обращаемся с числами: число 5 неизменяемо, и никто не страдает от того, что нельзя «изменить пятёрку». Чтобы получить 6, мы просто берём другое число. Erlang распространяет этот привычный для чисел принцип на все данные — списки, кортежи, отображения. «Изменить» что угодно означает «вычислить новую версию», а старая остаётся нетронутой и по-прежнему доступной.
List = [2, 3, 4],
Bigger = [1 | List].
% List остался [2,3,4], Bigger стал [1,2,3,4]
% Старое значение не тронуто
Зачем такая строгость
Неизменяемость — не каприз, а основа конкурентной модели Erlang. Когда данные нельзя изменить, не существует «гонок» за общую память: два процесса не могут испортить одно значение, потому что значение в принципе нельзя испортить. Не нужны блокировки и мьютексы. Код становится предсказуемым: видя X, вы уверены, что он не «подменится» в середине вычисления.
Польза проявляется не только в конкурентности. Неизменяемость резко упрощает отладку и рассуждение о коде в целом. В императивном языке, чтобы понять, чему равна переменная в данной точке, иногда приходится мысленно «прокрутить» всю историю её изменений: где-то её поменяли в цикле, где-то — в вызванной функции по ссылке, где-то — в обработчике события. В Erlang таких сюрпризов не бывает: значение переменной задано в месте её связывания и больше нигде не меняется. Это свойство называют ссылочной прозрачностью, и оно означает, что одинаковый вызов функции с одинаковыми аргументами всегда даёт одинаковый результат. Такой код легче тестировать, легче читать и почти невозможно «случайно сломать на расстоянии».
Как работает под капотом
Каждый процесс хранит данные в своей куче. Когда вы строите новый список из старого, BEAM часто переиспользует «хвост» старого списка (он ведь не изменится), создавая лишь новую «голову». Это называют структурным разделением (structural sharing). Поэтому неизменяемость не означает постоянного копирования — она дешевле, чем кажется. Безопасно делить общую часть можно именно потому, что её гарантированно никто не изменит: будь данные изменяемыми, такое разделение было бы миной замедленного действия. То есть неизменяемость не только удобна для программиста, но и открывает виртуальной машине оптимизации, недоступные в изменяемом мире.
А при отправке данных другому процессу значение копируется (ведь у процессов разные кучи), и это тоже безопасно: получатель работает со своей копией. На первый взгляд копирование при пересылке кажется накладным, но оно — обратная сторона той самой изоляции, что делает «let it crash» возможным. Раз у каждого процесса своя память, отправитель и получатель никак не могут случайно влиять друг на друга через общие данные, и сборщику мусора каждого процесса не нужно согласовывать свою работу с чужим. Это сознательный размен: немного копирования в обмен на полную развязку процессов и предсказуемую конкурентность.
Частые ошибки
- Пытаться «переприсвоить» переменную. Получите
no match. Введите новое имя:X1,X2или осмысленные имена. - Начинать имя переменной с маленькой буквы. Тогда это будет атом, а не переменная. Имена переменных — с заглавной.
- Ждать, что функция «изменит» переданный список. Функции возвращают новые значения, а не правят аргументы.
Итоги
- Переменные связываются один раз и неизменяемы;
=— это сопоставление, а не присваивание. - Повторное связывание с другим значением даёт ошибку
no match. - «Изменение» данных — это создание новых значений; BEAM переиспользует структуры.
- Неизменяемость убирает гонки данных и блокировки — фундамент безопасной конкурентности.