Оператор конвейера |>

Оператор |> (pipe) — визитная карточка Elixir. Он превращает мешанину вложенных вызовов в читаемую цепочку «сделай это, потом это».

Pipe берёт результат слева и подставляет его первым аргументом в функцию справа. Код начинает читаться как рецепт.

Сравните вложенные вызовы и их pipe-версию:

# Без pipe — читается изнутри наружу, наоборот порядку действий
String.upcase(String.reverse(String.trim("  hello  ")))

# С pipe — слева направо, в порядке выполнения
"  hello  "
|> String.trim()
|> String.reverse()
|> String.upcase()
# => "OLLEH"

Каждый шаг получает результат предыдущего как первый аргумент, остальные передаются явно:

[1, 2, 3, 4, 5]
|> Enum.filter(fn x -> rem(x, 2) == 0 end)  # [2, 4]
|> Enum.map(fn x -> x * 10 end)             # [20, 40]
|> Enum.sum()                               # 60

Как работает под капотом (BEAM)

Pipe — чисто синтаксическая трансформация на этапе компиляции. Выражение a |> f(b) компилятор разворачивает в f(a, b) ещё до генерации байткода. Никакой рантайм-магии, лишних аллокаций или замедления нет — на уровне BEAM это обычные вложенные вызовы функций. Именно поэтому стандартная библиотека спроектирована так, что «главный» аргумент (коллекция, строка) идёт первым: чтобы данные удобно текли через pipe.

  data |> f() |> g() |> h()

   data ──> f ──> g ──> h ──> результат

  компилируется в:  h(g(f(data)))
  (никаких накладных расходов в рантайме)

Та же идея на Python ▶

В Python нет оператора pipe, но композицию функций легко собрать вручную или через reduce.

from functools import reduce

def pipe(value, *funcs):
    return reduce(lambda acc, f: f(acc), funcs, value)

result = pipe("  hello  ",
              str.strip,
              lambda s: s[::-1],
              str.upper)
print(result)            # OLLEH

# Цепочка обработки коллекции, как Enum-конвейер
nums = [1, 2, 3, 4, 5]
evens = [x for x in nums if x % 2 == 0]   # filter
scaled = [x * 10 for x in evens]          # map
print(sum(scaled))                        # 60

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

  • Передавать данные не первым аргументом. Если функции нужен «главный» аргумент не первым, pipe подставит не туда — нужна анонимная обёртка.
  • Pipe из выражения, а не функции. x |> y, где y не вызов функции, чаще всего ошибка.
  • Слишком длинные цепочки. 15 шагов в одном pipe тяжело отлаживать; разбивайте на именованные функции.

Best practices

  • Проектируйте функции так, чтобы «данные» были первым аргументом — тогда они идеально встают в pipe.
  • Давайте промежуточным конвейерам имена через отдельные функции, если цепочка разрослась.
  • Используйте pipe для линейных трансформаций; для ветвлений — case/with.

Итог. Pipe делает код самодокументируемым: вы читаете преобразования в порядке их выполнения. Это бесплатно на уровне рантайма и формирует весь стиль стандартной библиотеки. Дальше — анонимные функции и захваты, которые часто стоят внутри pipe.

Когда pipe мешает, а не помогает

Pipe прекрасен для линейных трансформаций, но у него есть граница применимости, которую стоит чувствовать. Если в цепочке появляется ветвление или обработка ошибок на каждом шаге, pipe становится неуклюжим — туда нельзя естественно вставить «а если тут ошибка». Для таких случаев в Elixir есть конструкция with, которая последовательно матчит результаты и аккуратно выходит при первом несовпадении. Грубо говоря: pipe — для «всё хорошо, просто преобразуй», with — для «цепочка шагов, каждый из которых может не сложиться».

Ещё одна тонкость — отладка длинных конвейеров. Поскольку промежуточные значения не имеют имён, вставить отладочный вывод посередине нельзя напрямую. Идиоматичное решение — вставить |> IO.inspect(label: "после фильтра"): эта функция печатает значение и возвращает его без изменений, идеально вписываясь в pipe. Этот приём — первый инструмент отладки, который осваивает каждый Elixir-разработчик, и он отлично иллюстрирует, почему стандартная библиотека спроектирована «данные первым аргументом».

Проверьте себя
1. Куда оператор |> подставляет значение слева?
AПоследним аргументом функции справа
BПервым аргументом функции справа
CВ возвращаемое значение
DВ отдельную переменную
2. Где выполняется разворачивание pipe a |> f(b) в f(a, b)?
AВ рантайме при каждом вызове
BНа этапе компиляции — без накладных расходов
CВ планировщике BEAM
DВ сборщике мусора