Yes, finally! We now have in Elixir the with
keyword! (See what I did there with the post title?)
In a gist, with
allows you to sequence function calls and return a certain value, unless one of the functions returns something else.
But what does that actually mean? Allow me to explain with a real life sample of something that was a pain to deal with previously: sequential flows.
Let’s say you have a User record that you want to insert in your database, but you want to make sure that:
- Values that have been provided are correct
- The record is inserted in the database
- An e-mail is sent afterwards.
Given there is validation, database operation and communication with external services, things can go sideways. Before with
you’d likely have to do something like:
def register_user(name, email) do | |
validate_email(email) | |
|> create_user(name) | |
|> send_email | |
end | |
defp validate_email(email) do | |
# returns {:ok, email} when email is valid | |
# or {:error, error_message} otherwise | |
end | |
defp create_user({:error, error_message}, _name) do | |
{:error, error_message} | |
end | |
defp create_user({:ok, email}, name) do | |
# creates user on DB and returns {:ok, user} on success | |
# or {:error, error_message} on failure | |
end | |
defp send_email({:ok, user}) do | |
# returns {:ok} or {:error, error_message} | |
end | |
defp send_email({:error, message}) do | |
{:error, message} | |
end |
Having the code layout like this allows us to keep a clean public API function, but the private functions always have to take into account that an {:error, …}
can be received as an argument. Variations on this exist using case
s or if
s.
Not pretty and highly coupling: the definition of a function depends on the different outputs form other.
Enter with
!
def register_user(name, email) do | |
with {:ok} <- validate_email(email), | |
{:ok, user} <- create_user(email, name), | |
{:ok} <- send_email(user), | |
do: {:ok, user, "foobar"} | |
end | |
defp validate_email(email) do | |
# Validates the user e-mail and returns {:ok} when valid | |
# or {:error, error_message} when invalid | |
end | |
defp create_user(email, name) do | |
# creates user on DB and returns {:ok, user} on success | |
# or {:error, error_message} on failure | |
end | |
defp send_email(user) do | |
# sends an e-mail to the user and returns {:ok} on success | |
# or {:error, error_message} on failure | |
end |
The beauty of with
is that we now don’t have to worry about taking special care of error cases. As long as the function on the right of <-
successfuly pattern matches the definition on the left of <-
, the next block will be executed.
At the end, the do
block is returned. In the case above, that’d be {:ok, user, “foobar”}
.
If, for example, create_user/1
returned {:error, [:id, “ primary key violation”]}
, then this would be the value returning from with and send_email
would never be executed. Awesomeness!
Hope I helped shedding some light to with, here are the official docs for good measure.
Check my other Elixir posts here.
Thanks for reading!