Типы данных и неизменяемость
Дальше: Справочник Elixir · таблицы Операции для списка, Map, кортежа — ниже
Play ITЗагрузка интерактивного демо…
Типы данных и неизменяемость
Типы данных в Elixir делятся на две большие категории: базовые (или примитивные) и составные. Базовые типы представляют собой простые значения, которые не содержат внутри себя других структур. Составные типы строятся из базовых или других составных элементов и позволяют моделировать сложные формы информации. Все типы в Elixir реализованы как значения, передаваемые по значению, а не по ссылке, что упрощает рассуждения о поведении программы и делает её более предсказуемой.
Play ITЗагрузка интерактивного демо…
Целые числа
Целые числа в Elixir — это значения без дробной части, которые могут быть как положительными, так и отрицательными. Язык поддерживает произвольную точность целых чисел, что означает отсутствие ограничений на размер числа, кроме доступной памяти. Разработчик может свободно работать с числами, содержащими тысячи или даже миллионы цифр, без необходимости использовать специальные библиотеки. Такая гибкость особенно полезна в криптографии, математических вычислениях и других областях, где требуется высокая точность.
Целые числа можно записывать в десятичной, шестнадцатеричной, восьмеричной и двоичной системах счисления. Для этого используются префиксы: 0x для шестнадцатеричных, 0o для восьмеричных и 0b для двоичных литералов. Например, число 255 может быть записано как 0xFF, 0o377 или 0b11111111. Все эти формы эквивалентны и интерпретируются одинаково во время выполнения программы.
255 == 0xFF # true
255 == 0o377 # true
255 == 0b11111111 # true
1_000_000 + 2_500 # 1_002_500 — подчёркивания только для читаемости
Разбор:
0x,0o,0b— префиксы систем счисления; значение одно и то же, меняется только запись.- Оператор
==сравнивает числа семантически, даже если литералы записаны по-разному. - Подчёркивания в числах (
1_000_000) игнорируются и нужны только для удобства чтения.
Числа с плавающей точкой
Числа с плавающей точкой в Elixir соответствуют стандарту IEEE 754 двойной точности (64-битные). Они используются для представления вещественных чисел, то есть значений, которые могут содержать дробную часть. Запись таких чисел осуществляется в десятичной форме с обязательным наличием точки, даже если дробная часть отсутствует. Например, 3.0 — корректное число с плавающей точкой, тогда как 3 будет воспринято как целое число.
Арифметические операции с числами с плавающей точкой выполняются с использованием аппаратной поддержки процессора, если она доступна. Однако, как и в большинстве языков программирования, такие операции могут быть подвержены ошибкам округления из-за особенностей двоичного представления десятичных дробей. Это требует внимания при сравнении значений или выполнении финансовых расчётов, где важна абсолютная точность.
1 === 1.0 # false — разные типы
1 == 1.0 # true — численное равенство
0.1 + 0.2 == 0.3 # false из-за округления IEEE 754
Разбор:
===требует совпадения и значения, и типа: целое1и float1.0не равны строго.==для чисел допускает приведение:1и1.0считаются равными по величине.- Сумма
0.1 + 0.2в двоичном float не даёт ровно0.3— для денег используютDecimalили целые копейки.
Атомы
Атомы — одна из самых характерных черт Elixir и всей экосистемы Erlang. Атом представляет собой константу, имя которой одновременно является её значением. Атомы начинаются с двоеточия, например — :ok, :error, :user_id. Они часто используются для обозначения состояний, меток, ключей в структурах данных или возвращаемых значений функций. Поскольку атомы неизменяемы и глобально уникальны, их сравнение происходит за константное время, что делает их чрезвычайно эффективными для использования в условиях высокой нагрузки.
Важно понимать, что атомы хранятся в специальной таблице атомов, которая имеет ограниченный размер. Создание слишком большого количества атомов во время выполнения программы может привести к исчерпанию этой таблицы и аварийному завершению системы. По этой причине атомы не должны создаваться динамически из ненадёжных источников, таких как пользовательский ввод. Вместо этого рекомендуется использовать строки или другие типы данных, когда требуется гибкость в формировании имён.
status = :ok
{:ok, data} = {:ok, %{id: 1}}
# Плохо для пользовательского ввода:
# String.to_atom(user_input) # риск переполнения таблицы атомов
Разбор:
- Атом
:okчасто обозначает успешное завершение без дополнительных данных. - В кортеже
{:ok, data}атом:ok— метка ветки,data— полезная нагрузка. String.to_atom/1из ненадёжного ввода создаёт новые атомы на лету — в продакшене для таких ключей лучше строки.
Строки
Строки в Elixir — это последовательности байтов, закодированные в формате UTF-8. Это означает, что каждая строка является бинарным объектом, который может содержать любой текст, включая символы всех известных языков мира, эмодзи и специальные символы. Строки записываются в двойных кавычках: "Привет, мир!".
Поскольку строки в Elixir реализованы как бинарники, они поддерживают все операции, доступные для бинарных данных. При этом язык предоставляет богатый набор функций для работы с текстом — поиск подстрок, замена, разбиение, преобразование регистра и многое другое. Эти функции находятся в модуле String, который является частью стандартной библиотеки.
Особенностью строк в Elixir является их неизменяемость. Любая операция, изменяющая строку, создаёт новый бинарный объект. Это может показаться неэффективным на первый взгляд, но благодаря механизму копирования при записи (copy-on-write) и оптимизациям на уровне BEAM, такие операции выполняются быстро и с минимальным расходом памяти.
name = "Алиса"
greeting = "Привет, #{name}!"
String.length(greeting) # 13
String.upcase(greeting) # "ПРИВЕТ, АЛИСА!"
String.contains?(greeting, "Алиса") # true
Разбор:
#{name}— интерполяция: значение переменной встраивается в строку при создании новой строки.String.length/1считает кодовые точки Unicode, не байты.String.upcase/1возвращает новую строку; исходнаяgreetingне меняется.String.contains?/2проверяет подстроку и возвращаетtrueилиfalse.
Символы и кодовые точки
Хотя Elixir не имеет отдельного типа "символ" в том виде, в каком он существует в некоторых других языках, он предоставляет работу с кодовыми точками Unicode. Каждый символ в строке представлен своей кодовой точкой — целым числом, соответствующим стандарту Unicode. Функции из модуля String позволяют перебирать строку по кодовым точкам, получать их числовые значения и выполнять различные преобразования.
Для удобства записи отдельных символов используется синтаксис ?a, который возвращает кодовую точку символа a (в данном случае — 97). Этот подход позволяет легко работать с отдельными символами без необходимости извлекать их из строк.
?a # 97
?ж # кодовая точка кириллической «ж»
"élixir" |> String.codepoints()
# ["é", "l", "i", "x", "i", "r"]
Разбор:
- Префикс
?перед символом даёт его Unicode code point как целое число. - Для многобайтовых символов (
?ж) результат — полная кодовая точка, а не один байт. String.codepoints/1разбивает строку на логические символы (графемы в простых случаях — по одной "букве").- Строка в Elixir хранится в UTF-8, но API работает с символами, а не с сырыми байтами.
Кортежи (Tuples)
Кортеж — это упорядоченная коллекция элементов фиксированного размера. Кортежи в Elixir записываются в фигурных скобках, например — {:ok, "файл загружен"} или {1, 2, 3}. Каждый элемент может быть любого типа — число, строка, атом, другой кортеж — ограничений нет. Размер кортежа определяется во время его создания и не может быть изменён позже.
Кортежи часто используются для возврата нескольких значений из функции, особенно для передачи результата операции вместе со статусом. Стандартная практика в экосистеме Elixir — возвращать пару вида {:ok, значение} при успехе и {:error, причина} при ошибке. Такой подход делает обработку исключений явной и предсказуемой.
Пример:
File.read("example.txt")
# Может вернуть —
# {:ok, "Содержимое файла"}
# или
# {:error, :enoent}
Разбор:
File.read/1возвращает кортеж результата, а не бросает исключение в обычном потоке.- Вариант
{:ok, ...}содержит содержимое файла, вариант{:error, ...}— причину ошибки. - Такой контракт удобно обрабатывать через
case,withи pattern matching.
Доступ к элементам кортежа осуществляется по индексу с помощью функции elem/2:
user = {:user, "Алексей", 34}
{:user, name, age} = user
# name == "Алексей", age == 34
# Доступ по индексу — elem/2 (реже, чем сопоставление с образцом) —
name = elem(user, 1)
Разбор:
user— кортеж фиксированной длины из трех элементов.- Шаблон
{:user, name, age}проверяет форму кортежа и извлекает значения по позициям. elem(user, 1)читает второй элемент (индексация с нуля).- Pattern matching обычно читается лучше, чем доступ по индексу, особенно для доменных данных.
Изменение элемента кортежа невозможно напрямую, поскольку все данные в Elixir неизменяемы. Однако можно создать новый кортеж на основе старого с помощью синтаксиса обновления:
person = {:user, "Мария", 28}
updated_person = put_elem(person, 2, 29) # {:user, "Мария", 29}
Разбор:
put_elem/3не изменяет исходный кортеж, а возвращает новый.- Индекс
2указывает на третий элемент, который заменяется с28на29. - Иммутабельность сохраняет предсказуемость:
personиupdated_person— разные значения.
Функция put_elem/3 принимает исходный кортеж, индекс и новое значение, возвращая новый кортеж. Это соответствует общей философии языка: вместо модификации — создание нового значения.
Интерактивное демо — примеры кода в виджете на JavaScript/Python/C#; синтаксис Elixir — в блоках ниже.
Play ITЗагрузка интерактивного демо…
Списки (Lists)
Список в Elixir — это односвязный список, реализованный как цепочка пар "голова–хвост". Голова содержит первый элемент, хвост — оставшуюся часть списка (или пустой список, если элемент последний). Списки записываются в квадратных скобках — [1, 2, 3], ["привет", "мир"].
Особенность списков — эффективное добавление элементов в начало. Операция [элемент | список] выполняется за константное время. Добавление в конец или произвольный доступ по индексу требует прохода по всему списку и имеет линейную сложность.
Примеры:
list = [1, 2, 3]
new_list = [0 | list] # [0, 1, 2, 3]
# Разбор списка через сопоставление с образцом
[head | tail] = new_list
# head == 0
# tail == [1, 2, 3]
Разбор:
- Операция
[0 | list]добавляет элемент в начало списка за O(1). new_listстановится новым списком, исходныйlistне изменяется.- Шаблон
[head | tail]разбирает список на голову и хвост. - Такой паттерн — базовый инструмент для рекурсивной обработки списков.
Списки могут содержать элементы разных типов:
mixed = [:status, "готово", 200]
Разбор:
- Elixir разрешает гетерогенные списки, где элементы имеют разные типы.
- В примере смешаны атом, строка и число.
- В прикладном коде чаще используют однородные списки, чтобы упрощать обработку и чтение.
Однако в реальных программах рекомендуется использовать однородные списки для повышения читаемости и поддержки типовой дисциплины.
Проверка принадлежности элемента списку:
1 in [1, 2, 3] # true
5 in [1, 2, 3] # false
Разбор:
- Оператор
inпроверяет, есть ли элемент в перечислении. - Для списков проверка линейная: поиск идет по элементам слева направо.
- Выражение возвращает булево значение, поэтому его удобно использовать в условиях.
Длина списка определяется функцией length/1:
length([:a, :b, :c]) # 3
Разбор:
length/1считает количество элементов в списке.- Для linked list операция требует прохода по всем узлам, то есть O(n).
- Функция возвращает целое число и часто используется в проверках и валидации.
Важно не путать списки с кортежами: списки предназначены для хранения последовательностей переменной длины, кортежи — для фиксированных наборов связанных значений.
Keyword list — список пар "атом → значение", удобен для опций:
opts = [timeout: 5000, retries: 3, timeout: 10_000] # дубликат ключа допустим
Keyword.get(opts, :timeout) # 5000 — первое вхождение
Keyword.get(opts, :missing, :default) # :default
Разбор:
- Синтаксис
[key: value]— сокращение для списка кортежей{:key, value}. - Порядок ключей сохраняется; это отличает keyword list от обычной
map. Keyword.get/3безопасно читает значение и позволяет задать значение по умолчанию.- Дубликаты ключей редки в продакшене, но синтаксически разрешены.
Операции (список — linked list, не массив):
| Действие | Синтаксис / функция |
|---|---|
| Добавить в начало | [value | list] |
| Прочитать голову / хвост | hd(list), tl(list) или [h | t] |
| Длина | length(list) |
| Вставка по индексу | List.insert_at(list, index, value) |
| Удаление по индексу | List.delete_at(list, index) |
| Замена по индексу | List.replace_at(list, index, value) |
Map: Map.put(map, key, val), Map.get(map, key), Map.delete(map, key).
Tuple: только чтение elem(tuple, index) — размер фиксирован при создании.
Бинарники (Binaries)
Бинарник — это последовательность байтов. Он представляет собой непрерывный блок памяти, используемый для хранения сырых данных — изображений, сетевых пакетов, сериализованных структур и, что особенно важно, строк. В Elixir строки являются UTF-8-бинарниками.
Создание бинарника:
<<1, 2, 3>> # бинарник из трёх байтов
<<65, 66, 67>> # соответствует "ABC" в ASCII
"Привет" # это тоже бинарник: <<208, 159, 209, 128, ...>>
Разбор:
- Синтаксис
<<...>>создает бинарник как последовательность байтов. "Привет"тоже бинарник, потому что строки в Elixir хранятся в UTF-8.- Значения
65,66,67иллюстрируют соответствие байтов и символов ASCII.
Бинарники поддерживают мощный синтаксис сопоставления с образцом, позволяющий разбирать структурированные данные на лету:
data = <<10, 20, 30>>
<<a, b, c>> = data
# a == 10, b == 20, c == 30
Разбор:
- Переменная
dataсодержит три байта. - Шаблон
<<a, b, c>>разбирает бинарник по сегментам фиксированной длины. - В результате каждый байт связывается с отдельной переменной.
Можно указывать размер и тип каждого сегмента:
<<flags::8, length::16, payload::binary>> = <<1, 0, 5, "hello">>
# flags == 1
# length == 5
# payload == "hello"
Разбор:
::8и::16задают размер поля в битах для первых сегментов.payload::binaryзабирает оставшуюся часть как бинарник.- Такой формат удобен для разбора протоколов с заголовком и полезной нагрузкой.
Эта возможность делает Elixir особенно удобным для работы с сетевыми протоколами, файловыми форматами и другими бинарными структурами.
Конкатенация бинарников:
greeting = "Привет"
full = greeting <> ", мир!" # "Привет, мир!"
Разбор:
- Оператор
<>выполняет конкатенацию бинарников. - Строки тоже бинарники, поэтому операция работает с обычным текстом.
- Результат записывается в новую переменную
full; исходная строка не меняется.
Оператор <> работает только с бинарниками и строками (которые являются их частным случаем).
Битовые строки (Bitstrings)
Битовая строка — обобщение бинарника, в котором количество битов не обязано быть кратно восьми. Бинарник — это частный случай битовой строки, где длина кратна 8.
Пример битовой строки:
<<1::3, 0::2, 1::3>> # 8 бит → эквивалентно <<0b10001001>>
<<1::1>> # 1 бит — это уже не бинарник, а битовая строка
Разбор:
- Битовые сегменты позволяют задавать поля с длиной, не кратной байту.
- Первая строка суммарно дает 8 бит, поэтому значение также является бинарником.
- Вторая строка содержит 1 бит и относится только к bitstring.
Проверка:
is_binary(<<1::8>>) # true
is_binary(<<1::1>>) # false
is_bitstring(<<1::1>>) # true
Разбор:
is_binary/1проверяет кратность длины восьми битам.<<1::1>>не бинарник, но остается bitstring.is_bitstring/1дает обобщенную проверку для всех битовых строк.
Битовые строки редко используются в повседневном коде, но незаменимы при работе с аппаратными интерфейсами, шифрованием или компактными представлениями данных.
Диапазоны (Ranges)
Диапазон — это структура, представляющая последовательность целых чисел от начала до конца включительно. Записывается через ..:
1..5 # диапазон от 1 до 5
-3..0 # от -3 до 0
Разбор:
..создает структуруRangeс начальной и конечной границей.- Диапазон не материализует список сразу и экономит память.
- Его можно использовать в
Enum,forи проверках вхождения.
Диапазоны не генерируют все числа сразу — они хранят только границы. Это делает их экономичными по памяти.
Проверка вхождения:
3 in 1..5 # true
6 in 1..5 # false
Разбор:
- Проверка
inсRangeопределяет, попадает ли число в границы диапазона. - Результат булевый, поэтому выражение удобно в условных конструкциях.
- Для
3условие истинно, для6— ложно, так как число вне диапазона.
Диапазоны часто используются в циклах и генераторах:
for n <- 1..3, do: n * n # [1, 4, 9]
Разбор:
for(comprehension) проходит по диапазону и строит новую коллекцию.- Выражение
n * nвычисляется для каждого элемента. - В результате получается список квадратов
[1, 4, 9].
Интерактивное демо — виджет на JavaScript/Python/C#; для Elixir см. примеры с
%{}ниже.
Play ITЗагрузка интерактивного демо…
Отображения (Maps)
Отображение — основная структура данных для хранения пар "ключ–значение". Ключами могут быть любые типы — атомы, строки, числа, даже другие структуры. Значения также не ограничены.
Создание:
person = %{"имя" => "Иван", "возраст" => 40}
config = %{host: "localhost", port: 8080}
Разбор:
%{}создает map — основную структуру "ключ-значение" в Elixir.- В
personключи строковые ("имя"), вconfig— атомы (:host,:port) в сокращенной форме. - Выбор типа ключей влияет на стиль доступа и на интеграцию с внешними данными.
Обратите внимание: при использовании атомов в качестве ключей допускается сокращённая запись ключ: значение.
Доступ к значению:
person["имя"] # "Иван"
config[:host] # "localhost"
Разбор:
- Доступ через
map[key]универсален и работает для любых типов ключей. - Для строкового ключа нужен строковый индекс, для атома — атомный.
- Если ключ отсутствует, выражение вернет
nil, что важно учитывать в логике.
Если ключ — атом, можно использовать точечную нотацию:
config.host # "localhost"
Разбор:
- Точечная нотация работает для map со существующим атомным ключом.
- Такой доступ более краткий и читаемый для конфигурационных структур.
- При отсутствии ключа возможна ошибка, поэтому в динамических сценариях безопаснее
Map.get/3.
Обновление значения:
updated = %{config | port: 9000} # %{host: "localhost", port: 9000}
Разбор:
- Синтаксис
%{map | key: value}обновляет только уже существующие ключи. - Возвращается новая map, исходная
configостается прежней. - Для добавления новых ключей предпочтителен
Map.put/3.
Этот синтаксис требует, чтобы ключ уже существовал. Для добавления нового ключа используется полная форма:
%{config | new_key: "значение"} # ошибка, если new_key не существует
%{config | "новый_ключ" => "значение"} # тоже ошибка
# Правильно — создать новое отображение —
Map.put(config, :timeout, 5000)
# или
%{config | timeout: 5000} # только если timeout уже есть
Разбор:
- Первые два выражения демонстрируют ограничение обновляющего синтаксиса на "только существующие поля".
Map.put(config, :timeout, 5000)универсален: создаст ключ, если его нет.%{config | timeout: 5000}безопасен только когда:timeoutуже объявлен в map.
Лучше всего использовать Map.put/3 для универсального обновления:
Map.put(%{}, :key, "value") # %{key: "value"}
Разбор:
Map.put/3принимает map, ключ и новое значение.- Если ключ отсутствует, он добавляется; если есть, значение заменяется.
- Функция возвращает новую map без мутации исходной структуры.
В современных версиях Elixir (1.7+) map сохраняет порядок вставки ключей. Не путайте с keyword list ([key: value]) — там ключи-атомы и другой синтаксис, он чаще используется для опций функций.
Операции (Map):
| Действие | Синтаксис / функция |
|---|---|
| Добавить или заменить | Map.put(map, key, value) |
| Прочитать | Map.get(map, key) или map[key] |
| Удалить | Map.delete(map, key) |
Множества (MapSet)
Для задач "уникальные элементы без дубликатов" в Elixir есть MapSet:
set = MapSet.new([1, 2, 2, 3]) # #MapSet<[1, 2, 3]>
set = MapSet.put(set, 4)
set = MapSet.delete(set, 2)
MapSet.member?(set, 3) # true
Разбор:
MapSet.new/1удаляет дубликаты и строит множество уникальных значений.MapSet.put/2добавляет элемент,MapSet.delete/2удаляет элемент.MapSet.member?/2проверяет принадлежность и возвращает булево значение.- Все операции возвращают новое множество, сохраняя иммутабельность.
MapSet удобно применять для:
- дедупликации событий;
- проверки "видели ли уже этот идентификатор";
- быстрых membership-проверок в пайплайнах
Enum/Stream.
Операции (Set):
| Действие | Синтаксис / функция |
|---|---|
| Добавить | MapSet.put(set, value) |
| Удалить | MapSet.delete(set, value) |
| Проверить наличие | MapSet.member?(set, value) |
nil, false и "истинность"
В условиях if, unless, cond и в операторах && / || ложными считаются только false и nil. Число 0, пустая строка "", пустой список [] и пустая map %{} — истинные (truthy). Это отличается от JavaScript и Python, где пустые коллекции часто считаются ложью.
if [], do: "да" # выполнится
if nil, do: "нет" # не выполнится
Разбор:
- В Elixir только
falseиnilсчитаются ложными в условиях. - Пустой список
[]truthy, поэтому первая строка выполняет веткуdo. - Во второй строке условие
nil, поэтомуifне входит в ветку.
Переменные
В Elixir переменные — это именованные ссылки на значения. Объявление переменной происходит в момент присваивания. Синтаксис прост — имя переменной, за которым следует оператор =, и значение справа:
x = 42
name = "Елена"
status = :active
Разбор:
- Пример показывает связывание переменных с разными типами значений — число, строка, атом.
=выступает как pattern matching, а не императивное присваивание.- Переменные дальше используются в выражениях как неизменяемые значения текущего контекста.
Имена переменных должны начинаться со строчной буквы или символа подчёркивания и могут содержать буквы, цифры, знаки подчёркивания и символы @ (в особых случаях). Примеры допустимых имён — counter, user_id, _temp, max_value.
Особенность переменных в Elixir — их связь с механизмом сопоставления с образцом (pattern matching). Оператор = не является классическим присваиванием, как в императивных языках. Он выражает утверждение: "левая часть должна соответствовать правой". Если соответствие возможно, переменные в левой части связываются со значениями из правой части. Если нет — возникает ошибка времени выполнения.
Пример:
{a, b} = {10, 20}
# a == 10, b == 20
Разбор:
- Шаблон слева требует, чтобы справа был кортеж из двух элементов.
- При совпадении
aсвязывается с первым элементом,b— со вторым. - Такой подход позволяет сразу декомпозировать данные без дополнительных вызовов.
Здесь переменные a и b связываются с элементами кортежа. Это декларативное утверждение о структуре данных.
Переменные в Elixir могут быть связаны только один раз в рамках одного контекста. Повторное использование имени переменной приводит не к изменению значения, а к пересвязыванию (rebinding). Это означает, что старая переменная остаётся неизменной, а новое имя указывает на новое значение. Такое поведение сохраняет неизменяемость данных, но даёт удобство в написании кода.
Пример:
x = 5
x = x + 1
# x теперь связано со значением 6
# исходное значение 5 не изменилось — просто создано новое связывание
Разбор:
- Во второй строке выполняется пересвязывание имени
xс новым значением. - Старое значение не мутирует, так как данные в Elixir неизменяемые.
- Такой стиль снижает побочные эффекты и делает шаги преобразования явными.
Пересвязывание работает только с неаннотированными переменными. Если переменная используется в левой части сопоставления внутри функции или блока, где она уже была связана, интерпретатор требует точного совпадения, если не указано иное.
Чтобы запретить пересвязывание и заставить переменную вести себя как константа, можно использовать модульный атрибут или явно передать значение в замыкание без повторного связывания. Однако в большинстве случаев пересвязывание считается нормальной практикой и не нарушает функциональной чистоты.
Имена, начинающиеся с заглавной буквы, зарезервированы для модулей и не могут использоваться как переменные:
User = "admin" # ошибка: User — это имя модуля
Разбор:
- Имена с заглавной буквы зарезервированы для модулей, а не для переменных.
- Попытка связать
Userкак переменную приводит к ошибке. - Для переменных используйте
user,user_nameи другие имена со строчной буквы.
Переменные, начинающиеся с подчёркивания, сигнализируют о том, что значение не будет использоваться. Это соглашение помогает избежать предупреждений компилятора о неиспользуемых переменных:
{:ok, data} = File.read("config.txt")
# при ошибке сопоставление не сработает — используйте case/with
Разбор:
- Шаблон ожидает успешный результат чтения файла.
- Если
File.read/1вернет{:error, reason}, возникнетMatchError. - Для безопасной обработки обеих веток обычно применяют
caseилиwith.
Структуры (Structs)
Структура — это расширение отображения с фиксированным набором ключей, определённых во время компиляции. Структуры обеспечивают типобезопасность, читаемость и производительность при работе с именованными данными. Каждая структура привязана к модулю, и её имя совпадает с именем модуля.
Объявление структуры:
defmodule User do
defstruct name: "", age: 0, active: true
end
Создание экземпляра:
user = %User{name: "Дмитрий", age: 31}
# %User{name — "Дмитрий", age: 31, active: true}
Поля, не указанные при создании, получают значения по умолчанию, заданные в defstruct.
Доступ к полям:
user.name # "Дмитрий"
user.age # 31
Обновление:
updated = %{user | age: 32}
Попытка добавить поле, не объявленное в структуре, вызовет ошибку:
%{user | role: "admin"} # ** (KeyError) key :role not found
Это отличает структуры от обычных отображений и делает их подходящими для моделирования доменных сущностей.
Структуры наследуют все свойства отображений, но имеют собственный тип, что позволяет функциям проверять принадлежность к конкретной структуре:
is_struct(user) # true
is_map(user) # true (структура — подтип отображения)
Можно определять методы внутри модуля структуры, создавая поведение, связанное с данными:
defmodule User do
defstruct name: "", age: 0
def greet(%User{name: name}) do
"Привет, #{name}!"
end
end
user = %User{name: "Анна"}
User.greet(user) # "Привет, Анна!"
Арность и спецификации
У каждой функции есть арность — число аргументов. sum/1 и sum/2 — разные функции. В документации и @doc это отражается явно.
Опциональная аннотация @spec описывает типы аргументов и результата; вместе с Dialyzer она помогает ловить несоответствия на этапе анализа, без жёсткой компиляции типов как в Haskell.
Функции как тип данных
В Elixir функции являются полноценными значениями первого класса. Их можно присваивать переменным, передавать как аргументы, возвращать из других функций и хранить в структурах данных.
Функции создаются с помощью ключевого слова fn:
adder = fn a, b -> a + b end
result = adder.(3, 4) # 7
Обратите внимание на синтаксис вызова: после имени переменной ставится точка и скобки. Это отличает вызов анонимной функции от вызова именованной функции модуля.
Функции могут захватывать окружение, в котором были определены (замыкания):
multiplier = 10
scale = fn x -> x * multiplier end
scale.(5) # 50
Здесь multiplier — свободная переменная, захваченная из внешнего контекста.
Именованные функции модуля также могут быть представлены как значения с помощью оператора &:
double = &(&1 * 2)
# эквивалентно fn x -> x * 2 end
Enum.map([1, 2, 3], double) # [2, 4, 6]
# Или ссылка на функцию модуля —
formatter = &String.upcase/1
formatter.("hello") # "HELLO"
Анонимные и именованные функции сами по себе не хранят состояние между вызовами. Чистая функция при одинаковых аргументах даёт одинаковый результат; если внутри есть IO, обращение к процессу или БД — это уже эффект, и его стоит явно выделять в архитектуре.
Процессы и PID
Elixir построен на модели акторов: каждая единица выполнения — это изолированный процесс, который взаимодействует с другими процессами исключительно через обмен сообщениями. Эти процессы управляются виртуальной машиной BEAM и не имеют отношения к операционным потокам или процессам ОС. Они легковесны, потребляют мало памяти (около 2–3 КБ на процесс) и могут запускаться в количестве миллионов на одной машине.
Каждый процесс имеет уникальный идентификатор — PID (Process Identifier). PID — это специальный тип данных, который можно получить при создании процесса:
pid = spawn(fn ->
receive do
{:hello, sender} -> send(sender, {:world})
end
end)
PID выглядит как #PID<0.123.0> и может использоваться для отправки сообщений:
send(pid, {:hello, self()})
Функция self() возвращает PID текущего процесса.
Процессы не разделяют память. Любые данные, передаваемые между ними, копируются. Это исключает гонки данных и делает программу устойчивой к сбоям: падение одного процесса не влияет на другие.
Проверка типа:
is_pid(pid) # true
PID остаётся действительным даже после завершения процесса, но попытка отправить сообщение мёртвому процессу приведёт к его тихому игнорированию.
Процессы — основа конкурентности в Elixir. Они позволяют строить отказоустойчивые, масштабируемые и отзывчивые системы без использования блокировок, мьютексов или других примитивов синхронизации.
Как закрепить тему на практике
Мини-упражнение на 10 минут:
- Создайте
mapс настройками приложения и обновите 2 поля черезMap.put/3. - Преобразуйте список email-ов в
MapSetи удалите дубликаты. - Разберите кортеж
{:ok, value}через pattern matching. - Запустите процесс через
spawn/1и отправьте ему сообщение.
После этого переходите к Управляющим конструкциям и операторам Elixir, чтобы связать типы данных с ветвлениями и обработкой ошибок.
Частые ошибки в работе с типами
| Ошибка | Почему случается | Как исправить |
|---|---|---|
Путать строку ("") и charlist (~c"") | похожий внешний вид | для текста по умолчанию используйте строки |
| Динамически создавать атомы из ввода | риск переполнения таблицы атомов | хранить пользовательские значения как строки |
Выбирать tuple для изменяемых наборов данных | неудобное обновление и чтение | для доменных данных использовать map или struct |
| Ожидать, что пустой список "ложный" | перенос привычки из других языков | помнить: ложные только false и nil |
Что дальше по маршруту
- Управляющие конструкции и операторы Elixir.
- Функции и процессы в Elixir.
- Простые приложения на Elixir.
Как выбирать структуру данных под задачу
Быстрый ориентир для практики:
| Задача | Что выбрать | Почему |
|---|---|---|
| Последовательный обход и рекурсия | list | естественная модель head/tail |
| Фиксированный набор связанных значений | tuple | компактно и быстро для паттерн-матчинга |
| Доменная сущность с полями | struct | предсказуемая форма и безопасность полей |
| Словарь параметров | map | гибкие ключи и удобное обновление |
| Набор уникальных элементов | MapSet | отсутствие дублей и быстрый membership-check |
| Опции функции | keyword list | привычный формат key: value для API |
Если сомневаетесь между tuple и map, в прикладном коде чаще выигрывает map/struct из-за читаемости.
От типов к архитектуре
На этом уровне закладывается архитектурная устойчивость:
- правильный выбор структур упрощает API модулей;
- ясные контракты данных облегчают обработку ошибок через
caseиwith; - иммутабельность делает конкурентный код предсказуемым;
- одинаковые формы ответов (
{:ok, _}/{:error, _}) ускоряют интеграцию между компонентами.
Дальше — Управляющие конструкции и операторы, затем Функции и процессы.