This is just one segment in a compilation of my favorite functions and patterns in functional programming. This bit focuses on the with
statement. For more, visit the Index where I share the rest.
The with
statement may be my favorite feature of Elixir since the pipe operator. It enables the chaining of uncertainty. It affords elegance to the otherwise unsavory business of nested control structures.
There are few things in code more corrupting of beauty than:
case fetch_token() do
{:ok, token} ->
case call_outside_server(token) do
{:ok, resp} ->
case write_to_db(resp) do
{:ok, db_entry} -> db_entry
error -> error
end
error ->
error
end
error ->
error
end
I encounter this pattern all the time, and I never fail to shudder. Let’s try again:
with {:ok, token} <- fetch_token(),
{:ok, resp} <- call_outside_server(token),
{:ok, db_entry} <- write_to_db(resp) do
db_entry
else
error -> error
end
What’s happening? with
evaluates each expression to the right of <-
. Provided the result matches, it continues down the chain; if the entire chain is successful, code following do
is executed. Provided any result in the chain doesn’t match, its value is passed to the else
statement. Hence, we can flatten our nested control structures to resemble something more like a pipeline, as well as consolidate redundant error handling in one place.
with
also permits evaluation of bare expressions. Let’s say we need to perform a calculation on the response of call_outside_server/1
before passing it to write_to_db
. Turns out that’s easy:
with {:ok, token} <- fetch_token(),
{:ok, resp} <- call_outside_server(token),
resp = groom_response(resp),
{:ok, db_entry} <- write_to_db(resp) do
db_entry
else
error -> error
end
Further reading
- Initial proposal
- Elixir School
- Check out our Open Source repositories here.
We're building an AI-powered Product Operations Cloud, leveraging AI in almost every aspect of the software delivery lifecycle. Want to test drive it with us? Join the ProdOps party at ProdOps.ai.