Базовые типы данных

Прежде чем матчить и крутить процессы, нужно знать материал, из которого всё лепится: набор базовых типов 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 почти ничего не весит в памяти. Понимание того, какие структуры дёшевы, а какие — нет, отличает наивный код от эффективного, и мы вернёмся к этому в разделе про коллекции, где разберём стоимость операций над списками и кортежами подробно.

Проверьте себя
1. Что такое атом в Elixir?
AИзменяемая переменная
BИменованная константа, равная своему имени
CТип числа
DСиноним строки
2. Почему опасно делать String.to_atom от пользовательского ввода?
AЭто медленно
BТаблица атомов не чистится сборщиком мусора и может переполниться
CАтомы занимают много памяти каждый
DЭто синтаксическая ошибка