Elixir: Avoiding nesting

Posted on April 15, 2021
Tags: elixir

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 {
  
  user, err := db.get_by_name(username)
  if err != nil {
    return err
  }

  drink, err := db.get_drinks_for_user(user)
  if err != nil {
    return err
  }
  
  user.cash = user.cash - drink.price

  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 
        drinks = get_drinks_for_user(user)
        
        # Get the cheapest drink
        case get_cheapest_drink(drinks) do
          {:ok, drink} ->

            # Update his cash
            user = Map.put(user, :cash, user.cash - drink.price)
            {:ok, drink, user}

          other ->
            other
        end

      # We didn't find a user with the username
      nil ->
        {:error, "No user present with name #{username}"}
      
      # An unexpected error occurred
      other ->
        other
    end
  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
    with %User{} = user <- get_user(username),
         drinks <- get_drinks_for_user(user),
         {:ok, drink} <- get_cheapest_drink(drinks) do
      user = Map.put(user, :cash, user.cash - drink.price)
      {: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.