Базовые типы данных
Прежде чем матчить и крутить процессы, нужно знать материал, из которого всё лепится: набор базовых типов Elixir компактен и продуман.
В функциональном языке тип данных важнее класса: вся программа — это преобразование одних неизменяемых значений в другие.
Пройдёмся по ключевым типам:
42 # integer
3.14 # float
:ok # atom — константа, равная своему имени
true # на самом деле атом :true
"строка" # бинарная UTF-8 строка
'charlist' # список кодов символов (наследие Erlang)
{:ok, 42} # кортеж (tuple) — фиксированный размер
[1, 2, 3] # список (связный)
%{name: "Ann"} # map — ассоциативный массив
[a: 1, b: 2] # keyword list — список пар-атомов
Атомы (:ok, :error) — это именованные константы, равные самим себе. Они пронизывают весь Elixir: статусы возвращаются как {:ok, value} или {:error, reason}.
Map — главная структура для данных с ключами. Доступ к значению — через map.key (для атом-ключей) или map[key]:
user = %{name: "Ann", age: 30}
user.name # => "Ann"
user[:age] # => 30
Map.put(user, :city, "Msk") # возвращает НОВЫЙ map
Как работает под капотом (BEAM)
Все значения неизменяемы. Когда вы делаете Map.put, старый map не меняется — создаётся новый. Но BEAM не копирует всё целиком: внутренние структуры (map'ы, списки) разделяют общую неизменяемую часть в памяти (structural sharing). Атомы хранятся в глобальной таблице атомов и сравниваются за константу — поэтому матчинг по :ok мгновенный. Важное следствие: таблица атомов не очищается сборщиком мусора, поэтому создавать атомы динамически из пользовательского ввода опасно.
Списки хранятся как цепочка ячеек:
[1, 2, 3] == 1 -> 2 -> 3 -> []
head tail
Добавление в голову O(1), доступ по индексу O(n).
Та же идея на Python ▶
Неизменяемые трансформации легко показать на Python: вместо мутации создаём новый объект.
# Аналог Map.put: не мутируем, а копируем с изменением
user = {"name": "Ann", "age": 30}
updated = {**user, "city": "Msk"} # новый dict
print(user) # {'name': 'Ann', 'age': 30} — старый цел
print(updated) # {'name': 'Ann', 'age': 30, 'city': 'Msk'}
# Кортеж как неизменяемый "tuple" Elixir
status = ("ok", 42)
print(status[0], status[1]) # ok 42
# Атом ~ строковая константа-статус
OK, ERROR = "ok", "error"
result = (OK, 100)
print(result) # ('ok', 100)
Частые ошибки
- Путать строки и charlist.
"abc"(двойные кавычки) — бинарная строка;'abc'(одинарные) — список кодов. Это разные типы. - Динамически плодить атомы.
String.to_atom(user_input)может переполнить таблицу атомов и уронить узел. ИспользуйтеString.to_existing_atom/1. - Ждать мутации.
Map.put(m, :k, 1)не меняетm— нужно присвоить результат новой переменной.
Best practices
- Возвращайте статусы кортежами
{:ok, value}/{:error, reason}— это идиома всего экосистемы. - Для структурированных данных предпочитайте map; для опций функций — keyword list.
- Никогда не создавайте атомы из недоверенного ввода.
Итог. Набор типов невелик, но выразителен: атомы для меток, кортежи для фиксированных группировок, списки для последовательностей, map для словарей. Всё неизменяемо. На этих кирпичиках стоит главная суперсила языка — паттерн-матчинг, к которому мы переходим.
Структуры как типизированные map
Когда данных с фиксированным набором полей становится много, поверх map'а строят структуры через defstruct. Это всё тот же map под капотом, но с известным набором ключей и проверкой на этапе компиляции, что вы не обратились к опечатанному полю. Структуры дают читаемость записей при сохранении всех преимуществ неизменяемости: «обновление» структуры через синтаксис %User{user | age: 31} создаёт новую структуру, а не меняет старую.
Ещё одна частая структура — диапазон 1..100, который не разворачивается в список из ста элементов, а хранится компактно как «начало, конец, шаг» и лениво отдаёт значения при обходе. Это важная деталь производительности: 1..1_000_000 почти ничего не весит в памяти. Понимание того, какие структуры дёшевы, а какие — нет, отличает наивный код от эффективного, и мы вернёмся к этому в разделе про коллекции, где разберём стоимость операций над списками и кортежами подробно.