Не нарушайте конвейер, даже если вам нужен промежуточный результат.

В этой статье я хотел бы поделиться своим недавним открытием о конвейере в Elixir. Я удивлен, насколько это просто и что я никогда раньше этим не пользовался. Думаю, большинство из вас это уже знают, но я хочу поделиться этим подходом с теми, кто сочтет его полезным, как и я.
Встречайте трубу
Оператор канала Эликсир |> - не новость в программировании. Если мы возьмем, например, старый добрый Ocaml, мы можем встретить там похожий подход:
let sum x y = x + y;; sum 3 (sum 1 2);; # - : int = 6 1 |> sum 2 |> sum 3;; # - : int = 6
Но давайте теперь сосредоточимся на Эликсире. Представьте, что вы хотите обработать строку таким образом, чтобы получить список всех слов в верхнем регистре. Обычно вы бы сделали:
iex(1)> String.split(String.upcase("Ruby is dead"))
["RUBY", "IS", "DEAD"]
Однако благодаря оператору pipe мы можем:
iex(2)> "Ruby is dead" ...(2)> |> String.upcase() ...(2)> |> String.split() ["RUBY", "IS", "DEAD"]
Это дает тот же результат более элегантным способом, потому что оператор канала «Эликсир» передает результат выражения в качестве первого параметра другого выражения.
Понять трубу
В Макромодуле есть интересная функция, которую я хотел бы здесь вызвать. Макросы - это конструкции времени компиляции, которые вызываются с AST Elixir в качестве входных данных и расширенным набором AST Elixir в качестве выходных. В частности, нас интересует функцияunpipe/1, которая разбивает выражение конвейера на список.
Взгляните на наш предыдущий пример. Во-первых, нам нужно получить AST-представление нашего конвейера:
iex(3)> ast = quote do
...(3)> "Ruby is dead"
...(3)> |> String.upcase()
...(3)> |> String.split()
...(3)> end
{:|>, [context: Elixir, import: Kernel],
[
{:|>, [context: Elixir, import: Kernel],
[
"Ruby is dead",
{{:., [], [{:__aliases__, [alias: false], [:String]}, :upcase]}, [], []}
]},
{{:., [], [{:__aliases__, [alias: false], [:String]}, :split]}, [], []}
]}
Как видите, строка ”Ruby is dead” - это первый аргумент, переданный в String.upcase/2, а результат становится входом для функции String.split/1.
Теперь давайте отключим AST:
iex(4)> Macro.unpipe(ast)
[
{"Ruby is dead", 0},
{{{:., [], [{:__aliases__, [alias: false], [:String]}, :upcase]}, [], []}, 0},
{{{:., [], [{:__aliases__, [alias: false], [:String]}, :split]}, [], []}, 0}
]
Мы получаем список, который следует непосредственно за конвейером:
- AST строки
”Ruby is dead”. String.upcase/2функцияString.split/1функция
0 означает, что каждый элемент (или результат выполнения, начиная со второго) будет передан в качестве первого атрибута последовательным функциям.
Используйте трубу
Взглянем теперь на более сложный конвейер. Наш вариант использования выглядит следующим образом:
- У нас есть структуры, которые мы хотим сериализовать.
- Структура содержит одну ассоциацию.
- Данная ассоциация также должна быть сериализована, если она существует.
- Все остальные нерелевантные поля нужно отбросить, сбросить только значимые.
- Чистая карта должна представлять окончательную структуру, которую нужно сохранить.
Вот реализация:
Практически все достигается с помощью описанной выше реализации, но мы не кодируем связанную структуру. Именно для этого мы введем в конвейер:
Взгляните на предложение case в конвейере и сопоставление с образцом там. Попутно мы изменили промежуточный результат и передали его остальной части конвейера.
Хорошо, я знаю, что это довольно нереально:
- Обычно вы расширяете
Videoсериализацию за счет кодирования ее ассоциаций. - В качестве альтернативы вы можете передать функцию, которая будет соответствовать шаблону аргумента (функция как именованное выражение case) и сделать то же самое.
Однако ради этой статьи я хотел найти смехотворно простой способ показать вам простой трюк, о котором вы, возможно, не знали.
Трубить или не трубить?
Теперь вы можете думать примерно так:

Почему бы и нет? Но, пожалуйста, остановитесь на мгновение и подумайте о плюсах и минусах.
Заглянув в Руководство по стилю Credo, вы увидите это:
Предпочтительно начинать конвейерные цепочки с «чистого» значения, а не с вызова функции.
# preferred way - very readable due to the clear flow of data username |> String.strip() |> String.downcase() # also okay - but often slightly less readable String.strip(username) |> String.downcase()
И я полностью согласен с этим - либо используйте каналы во всем выражении, либо ничего - не смешивайте синтаксисы!
Во-вторых, на мой взгляд, использование оператора вертикальной черты только один раз для каждого выражения - это излишний замысел. Я не вижу причин использовать username |> String.strip() вместо просто String.strip(username).
Вертикальные или горизонтальные трубы?
Недавно я нашел интересное обсуждение на форуме Elixir о форматировании кода в терминах каналов. Автору было интересно, какие трубы использовать: горизонтальные или вертикальные:
"Ruby is dead" |> String.upcase() |> String.split()
vs.
"Ruby is dead" |> String.upcase() |> String.split()
В Кодовой базе Эликсира чаще встречается первый подход, тем не менее, встречается и второй.
Если строка короткая (обычно менее 80 символов), это вопрос личных предпочтений или рекомендаций компании / проекта. Конечно, если речь идет о длинных строках, форматтер выровняет их по вертикали.
Кстати, какой стиль вы предпочитаете? Лично мне нравится, чтобы все выражения были построчными (учитывая, что я никогда не использую только один канал).
Бонус: отказоустойчивая труба
С теми, кто уже знал все упомянутые вещи, я хотел бы поделиться еще одним, чтобы оставить вам хоть какую-то ценность.
Вы до сих пор думали, как можно было бы переопределить или присвоить псевдониму оператора канала? Я уверен, что вы этого не сделали, поскольку вы в здравом уме, но я воспользовался возможностью.
Функция (или фактически макрос) будет предельно простой:
defmacro left ~> right do
quote do
unquote(left) |> unquote(right)
end
end
и вы можете использовать его таким же образом:
iex(1)> 1..10 ~> Enum.random() 6
Однако сейчас не очень полезно иметь еще один псевдоним для той же функции. Давайте немного расширим его:
defmacro left ~> right do
quote do
case unquote(left) do
result ->
result |> unquote(right)
end
end
end
Я не особо менял его, но я ввел возможность сопоставления с образцом результата с левой стороны перед передачей его в функцию справа. Как мы можем это использовать? Например, чтобы подтвердить предыдущий результат, мы можем сделать:
defmacro left ~> right do
quote do
case unquote(left) do
{:error, error} ->
{:error, error}
result ->
result |> unquote(right)
end
end
end
В приведенном выше случае мы проверяем, произвела ли предыдущая операция {:error, error} кортеж, и не обрабатываем результат в таком случае.
Напоследок могу привести практический пример:
Обратите внимание, что функция parse/1 (и любая последующая) не должна обрабатывать {:error, error} кортеж! Он согласован в нашем Pipe макросе, и мы можем сосредоточиться только на содержательной логике без ненужной обработки ошибок.
Резюме
Я надеюсь, что эта статья окажется для вас полезной и вы получите реальную пользу, которую сможете добавить в свой собственный проект. Каналы - действительно полезные конструкции, которые могут сделать ваш код более читабельным при разумном использовании - помните об этом при разработке потоков в своих приложениях.
Оставьте свой адрес электронной почты, чтобы сразу же получать последние новости:
https://tinyletter.com/KamilLelonek
И если вы когда-нибудь задумывались о внесении каких-либо изменений в оператор канала, просто подумайте дважды, действительно ли это упростит ситуацию:

Во всяком случае, счастливого пипинга!
