Untangling Nested Case Statements Using The With Statement in Elixir

April 24, 2018

Untangling Nested Case Statements Using Elixir’s With Statement

I love spaghetti and meatballs. And when I first heard the term spaghetti code, it sounded tasty. Alas, it’s a pejorative phrase for code that is messy and overly complicated. If you write spaghetti code, no one will want to hire you.

Elixir basics

That’s true in any language, including Elixir.

One way to introduce spaghetti code into your Elixir code base is with a nested case statement.

What is a nested case statement?

Below is a simple example of a double nested case statement. Don’t worry too much about making sense of the higher level context about what this code is doing. I took some actual production code from a project and altered it for educational purposes. Instead, if you need a higher level context, pretend we’re simply setting the is_snow_tire property to a boolean value based on the diameter property of a tire.

The whole point is to focus on the nested case aspect of this code.

It’s not too hard to reason about, but I still find myself stopping to wonder what the difference between the two error return clauses are and if they are actually doing anything different.

defmodule Car do
  def undo_snow_tire(params) do
    case get_tire_by_diameter(params["diameter"]) do
      {:ok, tire} ->
        changeset = Tire.tire_changeset(tire,
        %{
            is_snow_tire: false,
          })
        case Repo.update(changeset) do
          {:ok, t} ->
            {:ok, t}
          {:error, changeset} ->
            {:error, changeset}
        end
      {:error, error} -> {:error, error}
    end
  end
  def get_tire_by_diameter(diameter), do: {:ok, "demo"}
end

Looking at the actual code upon which this was based off of, it actually doesn’t matter what the second element of the return error tuple is (I’m asking you to take my word for it in this instance as I don’t want to clutter up this post with more code that is not related to the concept we’re discussing).

So, what is wrong with a nested case statement?

I would argue there are 3 things wrong with it.

Point 1 – Readability

A nested case statement is simply not as readable as relying on pattern matching or using a with statement (coming later in this post). It’s harder for new developers coming into your team to understand your code base and make changes to it.

Point 2 – Maintainability and Adding New Features

The more you have to stop and think about what your code is doing, the harder it is to debug effectively and promptly. It’s also harder to add new features.

You want to be able to build a great product and have an easy time maintaining it. This will help you win against your competitors against the marketplace.

An extreme example for clarity

In case (pardon the pun) you didn’t believe the above example, here is an example of a nested case statement from a user in the elixir forum. I changed some of the variable names, but hopefully you can see my point about nested case statements.1

defmodule DemoModule do
  def start_or_resume_user_session(session_id) when is_bitstring(session_id) do
    case User.Supervisor.start_expression({"session", "global_id"}) do
      {:ok, root_session} ->
        case get_user_session(session_id) do
          {:ok, user_session} ->
            case get_latest_user_session_global_id(session_id, root_session) do
              # I want to do stuff if it's nil
              {:ok, nil} ->
                case (root_session |> instance(user_session)) do
                  {:ok, {_, session}} ->
                    case session |> get_user_info do
                      {:ok, session_info} ->
                        case get_ib_global_id(session_info) do
                          {:ok, user_session_global_id} -> {:ok, user_session_global_id}
                          {:error, reason} -> {:error, reason}
                        end
                      {:error, reason} -> {:error, reason}
                    end
                  {:error, reason} -> {:error, reason}
                end
              # Return it if not nil
              {:ok, existing_user_session_global_id} -> {:ok, existing_user_session_global_id}
              {:error, reason} -> {:error, reason}
            end
          {:error, reason} -> {:error, reason}
        end
      {:error, reason} -> {:error, reason}
    end
  end
end

Notice how much harder this makes the code to read.

So what is is one to do? One solution is the with statement.

Syntax of a with statement

Let’s look at rewriting the double nested case statement using a with statement.

defmodule Car do
  def undo_snow_tire(params) do
    with {:ok, tire} <- get_tire_by_diameter(params["diameter"]),
         {:ok, tire} <- Repo.update(Tire.changeset_for_update(tire, %{is_snow_tire: false})) do
      {:ok, tire}
    else
      {:error, error} -> {:error, error}
    end
  end
  def get_tire_by_diameter(diameter), do: {:ok, "demo"}
end

So the great thing about the with statement is that it lets you say “each intermediate result must match each pattern to the left of the “<-“ to execute the code in the with/do block, otherwise proceed to else”. Notice that your eye now can scan each statement to check what must match much more easily than it can with a nested case statement.

Advantages to using with

Advantage 1 – In our specific example, we must pattern match on the {:ok, tire} tuple for each function call. If a function should pattern match on a one element tuple like {:ok} instead, you can specify that too. Thus, the pattern match is very flexible.

Advantage 2 – You get can handle the same conditions as a nested case but with more concise code.

Advantage 3 – The with statement is an alternative way to do pattern matching on results and doing error handling. I almost think of it as an alternative to railway-oriented programming.

A Word on Elixir versions

Now this fellow named Scott notes that it’s not until Elixir 1.3 that you can use an else block in a with statement2. In Elixir 1.2, you only get the with/do block only.

Summary

I love spaghetti, but not in my code. Hopefully, this example of using the with statement will help you keep your codebase tidy too!

Footnotes

  1. Source: https://elixirforum.com/t/case-pyramid-of-doom-nested-with-nested-happy-path/1407 

  2. <li id="fn:2">
      <p>
        <a href="http://www.scottmessinger.com/2016/03/25/railway-development-in-elixir-using-with/">Source: http://www.scottmessinger.com/2016/03/25/railway-development-in-elixir-using-with/</a> <a href="#fnref:2" class="reversefootnote">&#8617;</a>
      </p>
    </li>

Profile picture

Written by Bruce Park who lives and works in the USA building useful things. He is sometimes around on Twitter.