Типы данных: атомы, числа, кортежи, списки, map, бинарные данные

Палитра значений, из которых строятся программы на Erlang.

Терм (term) — любое значение в Erlang: число, атом, кортеж, список и так далее. Всё, чем оперирует программа, — это термы.

У Erlang небольшой, но выразительный набор типов. Разобравшись с ними, вы поймёте большую часть кода. Это сознательная черта языка: типов нарочно немного, и каждый занимает чёткую нишу. Освоив этот короткий список, вы получаете почти полный словарь, на котором написаны любые программы на Erlang, от учебных до промышленных. Пройдёмся по основным.

Атомы

Атом — это именованная константа, значением которой является само имя. Атомы пишутся с маленькой буквы (или в одинарных кавычках). Их используют как метки и флаги: ok, error, true, false, undefined. Если вы программировали на других языках, ближайшая аналогия — элементы перечисления (enum) или символы (symbols) из Ruby и Lisp. Атом не несёт никакой «начинки», кроме собственного имени, и в этом вся его суть: ok значит ровно «ok», ни больше ни меньше. Атомы пронизывают весь Erlang — имена модулей и функций, флаги, теги результатов — всё это атомы, поэтому привыкнуть к ним стоит сразу.

Status = ok,
Color = 'Красный',  % атом с заглавной — в кавычках
Result = error.

Числа

Целые числа в Erlang произвольной точности — они не переполняются, а растут по мере надобности. Это приятное свойство для тех, кто хоть раз ловил коварную ошибку переполнения в C или Java: здесь факториал большого числа или вычисление с гигантскими значениями просто работают, без молчаливого «обнуления» старших разрядов. Есть и числа с плавающей точкой — для них действуют обычные оговорки о неточности дробной арифметики, поэтому деньги, например, лучше хранить в целых (в копейках), а не во float.

A = 42,
Big = 2 bsl 1000,   % огромное число, без переполнения
Pi = 3.14159,
Hex = 16#FF.         % 255 в шестнадцатеричной

Кортежи (tuples)

Кортеж — это фиксированный набор значений в фигурных скобках: {...}. Кортежи удобны, когда количество элементов известно и каждый имеет смысл по позиции. Очень распространён шаблон «тег + данные»: первым элементом кладут атом-метку, а дальше — связанные с ней данные. Именно так в Erlang принято возвращать результат, который может быть успехом или ошибкой: {ok, Значение} либо {error, Причина}. Вызывающий код смотрит на первый элемент и понимает, как трактовать остальное. Этот скромный приём — один из самых узнаваемых идиом языка, вы встретите его буквально в каждом проекте.

Point = {3, 4},
Person = {person, "Анна", 30},
Reply = {ok, "данные получены"}.

Списки

Список — последовательность элементов в квадратных скобках. У списка есть «голова» (первый элемент) и «хвост» (всё остальное). Оператор | разделяет голову и хвост.

Nums = [1, 2, 3, 4],
Head = hd(Nums),     % 1
Tail = tl(Nums),     % [2,3,4]
WithZero = [0 | Nums]. % [0,1,2,3,4]

Строки в Erlang — это, по сути, списки кодов символов. "abc" равно [97, 98, 99]. Это историческое решение иногда удивляет: строка и список чисел — одно и то же, и оболочка может неожиданно показать ваш список чисел как строку, если все они окажутся печатными кодами символов. Для серьёзной работы с текстом сегодня чаще используют бинарные строки (о них ниже) — они компактнее и эффективнее. Но понимать, что классическая строка есть список, важно: это объясняет многие особенности поведения и сообщений об ошибках.

Map — ассоциативный массив

Map (отображение) хранит пары «ключ — значение», как словарь. Это современная и удобная структура для конфигураций и записей; по сравнению со старыми способами хранить именованные поля она читается куда естественнее. Ключом может быть значение любого типа, а доступ к значению по ключу — быстрый. Важно помнить про неизменяемость: «обновляя» map синтаксисом Map#{ключ => новое}, вы получаете новый map, а исходный остаётся прежним. Map отлично подходит, когда набор полей заранее не фиксирован или может расти, — например, для конфигурации или для представления записи о пользователе.

User = #{name => "Иван", age => 25},
Name = maps:get(name, User),       % "Иван"
Older = User#{age => 26}.          % новый map с age=26

Бинарные данные

Бинарные данные — это эффективная последовательность байтов, записываемая в двойных угловых скобках. Их применяют для сетевых протоколов, файлов, кодирования, а также для текста в UTF-8. Это одна из сильных сторон Erlang и прямое наследие его телеком-прошлого: разбирать пакеты протоколов на уровне отдельных битов — повседневная задача в телекоме, и язык даёт для этого исключительно удобный синтаксис сопоставления по образцу. В примере ниже мы одним выражением «отрезаем» от бинарного значения первые два байта и забираем остаток как новую бинарную последовательность — попробуйте представить, сколько кода заняла бы та же операция на низкоуровневом языке с ручной работой с указателями.

Bin = <<1, 2, 3>>,
Text = <<"привет">>,
<<A, B, Rest/binary>> = <<10, 20, 30, 40>>.
% A=10, B=20, Rest = <<30,40>>

Как работает под капотом

Атомы хранятся в общей таблице атомов: одинаковый атом — это всегда одна и та же запись, поэтому сравнение атомов мгновенно. По сути, внутри атом представлен небольшим числом-индексом, и проверка «равны ли два атома» сводится к сравнению этих чисел — отсюда и скорость. Но у этого приёма есть оборотная сторона. Важно: таблица атомов не очищается сборщиком мусора, поэтому нельзя бесконтрольно превращать строки в атомы (можно переполнить таблицу). По умолчанию в ней около миллиона мест, и если их исчерпать, виртуальная машина аварийно остановится целиком — это один из немногих способов «положить» весь узел Erlang.

У остальных типов своя механика, и понимание её помогает писать быстрый код. Кортежи лежат в памяти подряд — доступ по индексу быстрый, зато «изменение» кортежа требует создать новый. Списки — связные, поэтому добавление в голову (через |) дёшево и происходит за постоянное время, а вот добавление в хвост или обращение к последнему элементу дороги, ведь приходится пройти весь список. Отсюда практический вывод, который вы будете применять постоянно: накапливайте результат, добавляя в голову, а в самом конце один раз переверните список функцией lists:reverse. Это типичный и идиоматичный для Erlang приём, и он гораздо быстрее наивной дописки в хвост в цикле.

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

  • Создавать атомы из пользовательского ввода. Таблица атомов конечна — это путь к падению. Используйте бинарные строки.
  • Путать кортеж и список. Кортеж — фиксированная структура по позиции, список — переменная последовательность.
  • Добавлять элементы в хвост списка в цикле. Это медленно; накапливайте в голову и переворачивайте.

Итоги

  • Атомы — именованные константы (ok, error); сравниваются мгновенно.
  • Числа — произвольной точности; кортежи — фиксированные наборы по позиции.
  • Списки — последовательности с головой и хвостом; строки — это списки кодов.
  • Map — словарь «ключ-значение»; бинарные данные — эффективные байтовые последовательности.
Проверьте себя
1. Что такое атом в Erlang?
AПеременная без значения
BИменованная константа, значение которой — само имя (ok, error)
CТип числа
DПустой список
2. Чем список отличается от кортежа?
AНичем
BСписок — переменная последовательность с головой и хвостом, кортеж — фиксированный набор по позиции
CСписок быстрее всегда
DКортеж не может содержать числа
3. Почему опасно создавать атомы из произвольного пользовательского ввода?
AАтомы занимают много места на диске
BТаблица атомов не очищается сборщиком мусора и может переполниться
CАтомы нельзя сравнивать
DЭто запрещено синтаксисом