Programming can end up becoming cluttered up. As functionality grows, the need to nest functions grows too. One of the features that make Elixir stand out is its pipe operator, denoted by |>
. In this blog, we'll explore the pipe operator in Elixir and how it can make your code more readable and maintainable.
The pipe operator allows you to chain functions together, passing the output of one function as the first argument to the next function.
Suppose we have a list of numbers that we want to add together, double the result, and then convert it to a string. Here's how we could do that without using the pipe operator:
iex> numbers = [21, 42, 69, 99, 111]
[21, 42, 69, 99, 111]
iex> sum = Enum.sum(numbers)
342
iex> double = sum * 2
684
iex> result = Integer.to_string(double)
"684"
This code works, but it's not very readable. We have to keep track of multiple intermediate variables (sum
, double
, and result
) and read the code from the inside out.
Now let's look at the same code using the pipe operator:
iex> numbers = [21, 42, 69, 99, 111]
[21, 42, 69, 99, 111]
iex> numbers |> Enum.sum() |> Kernel.*(2) |> Integer.to_string()
"684"
This code is much more concise and easier to read. The pipeline starts with the list of numbers and passes it to the Enum.sum
function. The output of that function is then passed to the Kernel.*(2)
function, which doubles the result. Finally, the output of that function is passed to Integer.to_string
, which converts the result to a string. The result is a single expression that can be read from left to right.
Here's another example that shows how the pipe operator can be used with anonymous functions:
iex> "bruce banner"
... |> String.split()
... |> Enum.map(&String.capitalize/1)
... |> Enum.join(" ")
"Bruce Banner"
In this example, we use the String.split
function to split a string into a list of words, then use Enum.map
with an anonymous function to capitalize the first letter of each word, and finally use Enum.join
to combine the words back into a single string.
The pipe operator can also be used with multiple arguments. In this case, you can include the function name and its arguments in parentheses:
iex> [1, 2, 3, 4] |> Enum.reduce(0, &+/2)
10
In this example, we use the Enum.reduce
function to sum a list of numbers. The first argument to Enum.reduce
is the initial value (0), and the second argument is the anonymous function that adds two numbers together.
The above example might seem basic however, the pipe operator can be used with even more complex functions.
defmodule PhoenixAuth.Accounts.User do
use Ecto.Schema
import Ecto.Changeset
import Pbkdf2 #to hash password before it is saved
@derive {Jason.Encoder, except: [:__meta__, :auth_tokens, :password]}
schema "users" do
field :email, :string
field :password, :string
field :username, :string
timestamps()
end
@doc false
def changeset(user, attrs) do
user
|> cast(attrs, [:username, :email, :password])
|> validate_required([:username, :email, :password])
|> validate_length(:username, min: 5, max: 25)
|> validate_length(:password, min: 8, max: 30)
|> unique_constraint(:email)
|> unique_constraint(:username)
|> update_change(:email, fn email -> String.downcase(email) end)
|> update_change(:username, &(String.downcase(&1)))
|> hash_password()
end
defp hash_password(changeset) do
case changeset do
%Ecto.Changeset{
valid?: true,
changes: %{password: password}
} -> change(changeset,
Pbkdf2.add_hash(password, hash_key: :password))
_ -> changeset
end
end
end
In the above example, we can see that the pipe operator can be used to nest even more functions. The above is a schema module for a User struct which using Ecto will be saved to the users table in the database once migrations are made. A changeset is used to chain functions that will determine how an entity is saved.
In conclusion, the pipe operator is a powerful feature of Elixir that allows you to chain functions together in a concise and readable way. It can make your code more maintainable and easier to understand.