Elixir has become one of the most loved languages. Elixir applications are scalable, very reliable, and the syntax is also quite nice. In this article, we will discuss about how elixir pipe operator (|>) works
When we use functional programming languages, sometimes code becomes messy. Let's take a look at this code, this function returns the average height of the adults in Delhi state
calculate_avg_height(adults(state('New Delhi')))
We can rewrite this code using the pipe operator
'New Delhi' |> state |> adults |> calculate_avg_height
Pipe operator makes our lives easier and makes our code looks beautiful. In this section, we build our own pipe operator with <|> this symbol. Rules of pipe operator are simple, the result of one function passes as an argument to the next function.
Let's say we need to perform a series of addition and multiplication operations. we can simply use a pipe operator
1 |> add(2) |> multiply(5) |> div(15)
In the first stage, 1 will be passed as an argument to add function
In second stage 1 and 2 are added and pass 3 as an argument to multiply the function
Multiply function performs 3 * 5 and pass 15 to div function and return 1 as result
Before we jump into building pipe operator. We need to have knowledge about elixir macros. You can learn about Elixir macros in this article
We use macros to build pipe operator. This basic difference between functions and macros is macros accept representation of code but functions accepts evaluation of code
We use 1 |> add(2) |> multiply(5) |> div(15) expression as reference to build pipe operator
Let's define a pipe macro. It will do pattern matching on the left and right arguments
defmodule Pipe do
defmacro left <|> right do
end
end
Define the unpipe function to pluck and store all the functions in array. You noticed that we store all the functions in AbstractSyntaxTree (AST) format
def unpipe(expr) do
Enum.reverse(unpipe(expr, []))
end
defp unpipe({:<|>, _, [left, right]}, acc) do
unpipe(right, unpipe(left, acc))
end
defp unpipe(other, acc) do
[{other, 0} | acc]
end
We need to define more function to pipe all the functions into a single function. You can picture the process by observing the following diagram
In the first stage we will add 1 as an argument to quoted expression {: add,_, [2]} In next stage we add {:add,_, [1, 2]} as an argument to {:multiply _, [3]} and so on
We use elixir inbuilt function List.foldl function to perform pipe operation and return the expression {:div, _, [{:multiply, _, [{:add, _, [1,2],3]},15]} to the caller
[{h, _} | t] = unpipe({:<|>, [], [left, right]})
value = List.foldl(t,h , fn {x,pos},acc -> pipe(acc,x,pos) end)
def pipe(expr, {call, line, _}, integer) do
{call, line, List.insert_at([], integer, expr)}
end
The resulting module will look like this.
# pipe.ex
defmodule Pipe do
defmacro left <|> right do
[{h, _} | t] = unpipe({:<|>, [], [left, right]})
List.foldl(t,h , fn {x,pos},acc -> pipe(acc,x,pos) end)
end
def unpipe(expr) do
Enum.reverse(unpipe(expr, []))
end
defp unpipe({:<|>, _, [left, right]}, acc) do
unpipe(right, unpipe(left, acc))
end
defp unpipe(other, acc) do
[{other, 0} | acc]
end
def pipe(expr, {call, line, atom}, integer) when is_atom(atom) do
{call, line, List.insert_at([], integer, expr)}
end
def pipe(expr, {op, line, args} = op_args, integer) when is_list(args) do
{op, line, List.insert_at(args, integer, expr)}
end
end
You can test this operator in iex terminal.
iex(2)> require Pipe
iex(3)> import Pipe
iex(5)> 2 <|> div(1) <|> IO.puts
2
:ok
I hope you understand how pipe operator works . If you want to learn more interesting topics in elixir.