Users who are new to functional programming often face the problem of deeply nested code which is very hard to maintain. The problem is that functional programming languages don’t have a return statement.
Let’s take the following Go code:
func bar(username string) err {
, err := db.get_by_name(username)
userif err != nil {
return err
}
, err := db.get_drinks_for_user(user)
drinkif err != nil {
return err
}
.cash = user.cash - drink.price
user
return nil
}
Now let’s try implementing that in Elixir. First we gonna define some structs:
defmodule Drink do
defstruct [:name, :taste, :min_age, :price]
end
defmodule User do
defstruct [:name, :age, :cash]
end
defmodule App do
@drinks [
Drink{name: "Bitter beer", taste: :bitter, min_age: 18, price: 5},
%Drink{name: "Desperados", taste: :sweet, min_age: 18, price: 20},
%Drink{name: "Coke", taste: :sweet, min_age: 0, price: 2},
%Drink{name: "Vodak", taste: :burning, min_age: 18, price: 40}
%]
def get_cheapest_bitter_drink(drinks) do
drinks|> Enum.filter(&(&1.taste == :bitter))
|> get_cheapest_drink
end
def get_cheapest_drink([]), do: {:error, "Drinks are empty"}
def get_cheapest_drink(drinks) do
=
drink
drinks|> Enum.sort(&(&1.price < &2.price))
|> List.first()
{:ok, drink}
end
def get_drinks_for_user(%{age: age, cash: cash}) do
@drinks
|> Enum.filter(&(&1.price <= cash && &1.min_age <= age))
end
def get_user("max"), do: %User{name: "max", age: 12, cash: 20}
def get_user("marie"), do: %User{name: "marie", age: 21, cash: 0}
def get_user("alex"), do: %User{name: "alex", age: 31, cash: 100}
def get_user("foo"), do: {:error, "Connection timeout"}
def get_user(_), do: nil
end
Now we want to add a function to the module App where the input is a username. We then fetch the user by its name from a data source, get the cheapest drink he can buy and update the user’s cash.
A nested solution would look as following:
def nested(username) do
# Get the user
case get_user(username) do
# We found a user with the given username
User{} = user ->
%
# Get his drinks
= get_drinks_for_user(user)
drinks
# Get the cheapest drink
case get_cheapest_drink(drinks) do
{:ok, drink} ->
# Update his cash
= Map.put(user, :cash, user.cash - drink.price)
user {:ok, drink, user}
->
other
otherend
# We didn't find a user with the username
nil ->
{:error, "No user present with name #{username}"}
# An unexpected error occurred
->
other
otherend
end
In real time scenario this can get even more nested. Some may be annoyed and instead of handling cases explicitly to avoid nesting they would write:
def foo() do
{:ok, user} = get_user()
end
This is bad as this causes a runtime exception.
The solution for this is the with statement in Elixir. We can rewrite the function to:
def not_nested(username) do
User{} = user <- get_user(username),
with %<- get_drinks_for_user(user),
drinks {:ok, drink} <- get_cheapest_drink(drinks) do
= Map.put(user, :cash, user.cash - drink.price)
user {:ok, drink, user}
else
# Patterns within the with statement did not match.
# In case we get a nil error, we know that is from the
# get_user function. Let's transform it to a meaningful
# error message.
nil -> {:error, "No user present with name #{username}"}
# We got another unexpected error.
-> other
other end
end
Not only is that easier for others to read & understand but also we reduce the LOC. This is great, now we don’t have to fear nesting in Elixir again.