LEARN X · ЗА 16 МИН
Elixir
Экспресс-тур по Elixir: весь функциональный язык на BEAM в одном плотно закомментированном примере — типы, паттерн-матчинг, конвейер, процессы.
Elixir — функциональный язык на виртуальной машине BEAM (от Erlang): неизменяемые данные, сопоставление с образцом и лёгкие процессы для параллелизма. Весь язык ниже — в комментариях к рабочему коду. Запускайте в iex или через elixir файл.exs.
Вывод и комментарии
# Это однострочный комментарий — отдельного блочного комментария в Elixir нет.
IO.puts("Привет, мир") # печатает строку и перевод строки => Привет, мир
IO.write("без \n") # печатает без перевода строки
# IO.inspect печатает СЫРОЕ представление значения и ВОЗВРАЩАЕТ его (удобно в конвейере).
IO.inspect([1, 2, 3]) # => [1, 2, 3]
IO.inspect(%{a: 1}, label: "map") # => map: %{a: 1}
Базовые типы
42 # integer (целое, произвольной точности)
0xFF # 255 — шестнадцатеричное
1_000_000 # подчёркивания для читаемости => 1000000
3.14 # float (число с плавающей точкой, всегда с точкой)
true # boolean — это на самом деле атомы :true и :false
false
nil # пустота / отсутствие значения (атом :nil)
:ok # atom (атом) — константа, чьё имя и есть значение
:error
:"с пробелом" # атом может быть в кавычках
"строка" # string — это UTF-8 бинарник (binary)
?a # => 97 — кодовая точка символа
is_integer(42) # => true — проверки типов: is_atom, is_binary, is_list...
i 42 # в iex: подробная инфо о значении
Сопоставление с образцом
Знак = в Elixir — это не присваивание, а оператор сопоставления (match): левая часть должна совпасть с правой, иначе ошибка.
x = 1 # связывает x со значением 1 (слева переменная — совпадает всегда)
1 = x # OK: 1 совпадает с 1
# 2 = x # => ** (MatchError) — 2 не равно 1
# Деструктуризация: разбираем структуру на части
{a, b, c} = {:ok, 200, "OK"} # a = :ok, b = 200, c = "OK"
[head | tail] = [1, 2, 3] # head = 1, tail = [2, 3]
%{name: n} = %{name: "Ада", age: 36} # n = "Ада" (берём только нужный ключ)
_ = 99 # _ — анонимная переменная: совпадает с чем угодно, значение игнорируется
# ^ (pin) — использовать ТЕКУЩЕЕ значение переменной, а не связывать заново
fixed = 10
^fixed = 10 # OK: совпадает с 10
# ^fixed = 20 # => ** (MatchError)
Коллекции
# Список (list) — связный список, эффективен спереди
list = [1, 2, 3]
[0 | list] # => [0, 1, 2, 3] — добавление в голову (быстро)
list ++ [4] # => [1, 2, 3, 4] — конкатенация
list -- [2] # => [1, 3] — вычитание элементов
hd(list) # => 1 (голова), tl(list) => [2, 3] (хвост)
# Кортеж (tuple) — хранится в памяти подряд, эффективен по индексу
tuple = {:ok, "данные"}
elem(tuple, 1) # => "данные"
# Keyword list — список пар [{atom, value}], упорядочен, ключи-атомы могут повторяться
opts = [limit: 10, sort: :asc] # сахар для [{:limit, 10}, {:sort, :asc}]
opts[:limit] # => 10
# Map (ассоциативный массив) — ключи любого типа, уникальны
m = %{"a" => 1, :b => 2}
m["a"] # => 1
m.b # => 2 (точечный доступ только для атомов-ключей)
m2 = %{m | :b => 99} # => обновление существующего ключа
m3 = Map.put(m2, :c, 3) # => добавление нового ключа
Строки и бинарники
name = "Эликсир"
"Привет, #{name}!" # => "Привет, Эликсир!" — интерполяция #{}
"a" <> "b" # => "ab" — конкатенация строк оператором <>
String.upcase("ok") # => "OK"
String.length("привет") # => 6 (считает графемы, а не байты)
String.split("a,b,c", ",") # => ["a", "b", "c"]
String.replace("foo", "o", "0") # => "f00"
String.contains?("hello", "ell") # => true
# Многострочный текст — heredoc
text = """
строка 1
строка 2
"""
# Под капотом строка — бинарник (последовательность байтов)
byte_size("я") # => 2 (UTF-8: кириллица занимает 2 байта)
Операторы и условия
1 + 2 * 3 # => 7
div(10, 3) # => 3 (целочисленное деление), rem(10, 3) => 1 (остаток)
10 == 10.0 # => true (нестрогое равенство), 10 === 10.0 => false (строгое)
true and false # строгие булевы: and / or / not (ждут именно boolean)
nil || "запасное" # => "запасное" — ||/&&/! работают с истинностью (всё кроме nil/false — истина)
# if / unless — это выражения, ВОЗВРАЩАЮТ значение
status = if 5 > 3, do: :больше, else: :меньше # => :больше
unless false, do: IO.puts("выполнится")
# cond — первая истинная ветка (аналог цепочки else-if)
grade =
cond do
90 <= 95 -> "A"
80 <= 95 -> "B"
true -> "иначе" # true — ветка по умолчанию
end
# case — сопоставление значения с образцами
case {:ok, 42} do
{:ok, n} when n > 40 -> "много: #{n}" # with guard (охранное условие)
{:ok, n} -> "ок: #{n}"
{:error, _} -> "ошибка"
_ -> "что-то ещё"
end
Конвейер |>
Оператор |> (pipe) передаёт результат слева первым аргументом функции справа — читается слева направо, без вложенных скобок.
# Без конвейера — читается изнутри наружу:
String.upcase(String.reverse("elixir")) # => "RIXILE"
# С конвейером — линейно и наглядно:
"elixir"
|> String.reverse() # => "rixile"
|> String.upcase() # => "RIXILE"
# Классический пример обработки коллекции:
1..10
|> Enum.filter(fn x -> rem(x, 2) == 0 end) # => [2, 4, 6, 8, 10]
|> Enum.map(fn x -> x * x end) # => [4, 16, 36, 64, 100]
|> Enum.sum() # => 220
Функции
# Анонимная функция: fn ... end, вызов через точку .()
add = fn a, b -> a + b end
add.(2, 3) # => 5
# & — краткая запись: &1, &2 — позиционные аргументы
square = &(&1 * &1)
square.(5) # => 25
inc = &Kernel.+(&1, 1) # захват именованной функции
# Именованные функции живут только внутри модуля:
defmodule Math do
def double(x), do: x * 2 # def — публичная функция
defp secret(), do: 42 # defp — приватная (видна только внутри модуля)
# Несколько клауз (multiple clauses) — выбор по образцу аргументов:
def fact(0), do: 1 # базовый случай
def fact(n) when n > 0, do: n * fact(n - 1) # рекурсивный случай
# Значение по умолчанию через \\\\
def greet(name, greeting \\\\ "Привет"), do: "#{greeting}, #{name}!"
end
Math.double(21) # => 42
Math.fact(5) # => 120
Math.greet("Ада") # => "Привет, Ада!"
Модули
defmodule MyApp.Strings do # имена модулей — атомы вида MyApp.Strings
def shout(s), do: String.upcase(s) <> "!"
end
MyApp.Strings.shout("hi") # => "HI!"
defmodule Demo do
import String, only: [upcase: 1] # import — звать функции без префикса модуля
alias MyApp.Strings, as: S # alias — короткое имя для длинного модуля
require Integer # require — нужно для использования макросов модуля
def run do
upcase("ok") # => "OK" (без "String.")
S.shout("yo") # => "YO!"
Integer.is_even(4) # => true (is_even — макрос, потому и require)
end
end
# Атрибуты модуля — константы и метаданные
defmodule Config do
@version "1.0.0" # @имя — атрибут модуля
def version, do: @version
end
Рекурсия и Enum
Циклов for/while нет — данные неизменяемы, поэтому повторение делается рекурсией, а на практике — модулем Enum.
# Enum — высокоуровневые операции над любыми коллекциями (Enumerable)
Enum.map([1, 2, 3], fn x -> x * 10 end) # => [10, 20, 30]
Enum.filter([1, 2, 3, 4], &(rem(&1, 2) == 0)) # => [2, 4]
Enum.reduce([1, 2, 3, 4], 0, fn x, acc -> x + acc end) # => 10 (свёртка)
Enum.each([1, 2], &IO.puts/1) # печатает 1 и 2, возвращает :ok
Enum.sort([3, 1, 2]) # => [1, 2, 3]
Enum.take(1..1000, 3) # => [1, 2, 3]
# Тот же reduce, но руками через рекурсию и образцы:
defmodule MyList do
def sum([]), do: 0 # пустой список => 0
def sum([head | tail]), do: head + sum(tail) # голова + сумма хвоста
end
MyList.sum([1, 2, 3, 4]) # => 10
# Comprehension (for) — это синтаксический сахар, тоже не цикл:
for x <- 1..3, y <- [:a, :b], do: {x, y} # => [{1, :a}, {1, :b}, {2, :a}, ...]
Структуры и протоколы
# defstruct — именованный map с фиксированным набором полей, привязанный к модулю
defmodule User do
defstruct name: "", age: 0 # поля и значения по умолчанию
end
u = %User{name: "Ада", age: 36} # создание структуры
u.name # => "Ада"
u2 = %{u | age: 37} # обновление поля (как у map)
%User{name: n} = u # структуры тоже сопоставляются с образцом => n = "Ада"
# Protocol (протокол) — полиморфизм: одно поведение, разные реализации для разных типов
defprotocol Size do
def size(data) # объявляем интерфейс
end
defimpl Size, for: BitString do # реализация для строк
def size(s), do: byte_size(s)
end
defimpl Size, for: List do # реализация для списков
def size(l), do: length(l)
end
Size.size("abc") # => 3
Size.size([1, 2]) # => 2
Параллелизм
На BEAM конкурентность — это лёгкие процессы (не потоки ОС), общающиеся сообщениями. Их можно создавать миллионами.
self() # => PID текущего процесса, напр. #PID<0.110.0>
# spawn — запустить функцию в новом изолированном процессе, возвращает его PID
pid = spawn(fn -> IO.puts("я в отдельном процессе") end)
# Обмен сообщениями: send отправляет, receive ждёт и сопоставляет с образцом
parent = self()
spawn(fn ->
receive do # процесс блокируется, пока не придёт сообщение
{:ping, from} -> send(from, :pong) # отвечаем отправителю
end
end)
|> (fn worker -> send(worker, {:ping, parent}) end).()
receive do
:pong -> IO.puts("получили pong") # => получили pong
after
1000 -> IO.puts("таймаут") # after — выход по таймауту (мс)
end
# В реальных приложениях процессы оборачивают в абстракции OTP:
# GenServer (сервер с состоянием), Task (фоновая задача), Supervisor (перезапуск при сбоях).