ElixirMiddleCoding
Что такое оператор pipe (|>) и как он улучшает читаемость кода?
Оператор |> передаёт результат левого выражения первым аргументом в правую функцию, позволяя писать цепочки преобразований линейно сверху вниз вместо глубокой вложенности вызовов. Для сложных блоков внутри цепочки используются then/1 и tap/1.
Оператор pipe |> в Elixir
Оператор |> (pipe) передаёт результат выражения слева в качестве первого аргумента функции справа. Это позволяет выстраивать цепочки преобразований данных линейно — сверху вниз, без глубокой вложенности вызовов.
Базовый синтаксис
# Без pipe — читается изнутри наружу
result = Enum.join(Enum.map(String.split("hello world", " "), &String.upcase/1), ", ")
# С pipe — читается сверху вниз
result =
"hello world"
|> String.split(" ") # ["hello", "world"]
|> Enum.map(&String.upcase/1) # ["HELLO", "WORLD"]
|> Enum.join(", ") # "HELLO, WORLD"
Как работает pipe
Pipe — это синтаксический сахар, трансформируемый компилятором:
x |> f() # => f(x)
x |> f(a, b) # => f(x, a, b)
x |> f(a) |> g() # => g(f(x, a))
Практический пример: обработка HTTP-параметров
defmodule JobFilter do
def apply(params) do
params
|> Map.get("skills", "")
|> String.split(",")
|> Enum.map(&String.trim/1)
|> Enum.reject(&(&1 == ""))
|> Enum.map(&String.downcase/1)
|> Enum.uniq()
end
end
JobFilter.apply(%{"skills" => " Elixir , Python , elixir "})
# => ["elixir", "python"]
Pipe с анонимными функциями
Анонимную функцию нельзя передать в pipe напрямую без скобок — нужно обернуть в именованную или использовать (&...)/1:
# Ошибка
# [1,2,3] |> fn x -> x * 2 end.()
# Правильно: именованная функция через &
[1, 2, 3]
|> Enum.map(&(&1 * 2))
|> Enum.sum()
# => 12
# Правильно: через then/1 для произвольных трансформаций
"alice@example.com"
|> String.split("@")
|> then(fn [user, domain] -> %{user: user, domain: domain} end)
then/1 и tap/1
then(value, fun)— передаёт значение в функцию и возвращает результат (для сложных блоков в цепочке).tap(value, fun)— вызывает функцию ради побочного эффекта (логирование, отладка), но возвращает оригинальное значение без изменений.
"raw_data"
|> process()
|> tap(&Logger.debug("after process: #{inspect(&1)}"))
|> save()
Подводные камни
- Pipe передаёт только первый аргумент — если API функции принимает основные данные вторым аргументом, pipe не подойдёт без обёртки; например,
String.contains?(needle, haystack)— типичная «перевёрнутая» сигнатура в сторонних библиотеках. - Анонимные функции требуют явного вызова —
value |> my_fun.()с точкой перед скобками, иначе ошибка компиляции. - Длинные цепочки скрывают тип промежуточных значений — если функция в середине цепочки вернёт
{:error, reason}вместо ожидаемого значения, следующий шаг упадёт с непонятной ошибкой; используйтеwithдля монадических цепочек с ошибками. - Отладка цепочки — вставьте
|> tap(&IO.inspect(&1, label: "step 3"))вместо разрыва цепочки; удалите перед коммитом. - then vs обычная функция — избыточное использование
thenдля простых случаев ухудшает читаемость; предпочтительно выносить сложную логику в именованные функции.
Common mistakes
- Сводить pipe operator к названию метода без lifecycle и failure path.
- Игнорировать модель runtime: Elixir компилируется в BEAM bytecode и наследует процессы, message passing, supervision и hot-code friendly модель Erlang VM.
- Не отделять validation, authorization, transaction boundary и business logic.
What the interviewer is testing
- Объясняет pipe operator через конкретную точку lifecycle в Elixir.
- Приводит корректный минимальный пример без вымышленных методов или callbacks.
- Называет edge cases: пустые значения, ошибки, транзакции, безопасность или concurrency.