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 (перезапуск при сбоях).
Поддержать проект