home

Elixir's With Statement

Nov 07, 2016

A reason that I'm such a huge fan of Elixir is that everything just seems to click. The code is elegant and expressive while managing to be efficient.

One thing that hasn't clicked until just now is the with statement. It turns out that it's pretty easy to understand and really quite useful. It addresses a specific and not-uncommon problem in a very Elixir-way. What's the problem? Pretend we have the following:

defmodule User do
  defstruct name: nil, dob: nil

  def create(params) do
  end
end

The create function should either create a User struct or return an error if the params are invalid. How would you go about doing this? Here's an example using pipes:

def create(params) do
  %User{}
  |> parse_dob(params["dob"])
  |> parse_name(params["name"])
end

defp parse_dob(user, nil), do: {:error, "dob is required"}
defp parse_dob(user, dob) when is_integer(dob), do: %{user | dob: dob}
defp parse_dob(_user, _invalid), do: {:error "dob must be an integer"}

defp parse_name(_user, {:error, _} = err), do: err
defp parse_name(user, nil), do: {:error, "name is required"}
defp parse_name(user, ""), do: parse_name(user, nil)
defp parse_name(user, name), do: %{user | name: name}

The problem with this approach is that every function in the chain needs to handle the case where any function before it returned an error. It's clumsy, both because it isn't pretty and because it isn't flexible. Any new return type that we introduced has to be handled by all functions in the chain.

The pipe operator is great when all functions are acting on a consistent piece of data. It falls apart when we introduce variability. That's where with comes in. with is a lot like a |> except that it allows you to match each intermediary result.

Let's rewrite our code:

def create(params) do
  with {:ok, dob} <- parse_dob(params["dob"]),
       {:ok, name} <- parse_name(params["name"])
  do
    %User{dob: dob, name: name}
  else
    # nil -> {:error, ...} an example that we can match here too
    err -> err
  end
end

defp parse_dob(nil), do: {:error, "dob is required"}
defp parse_dob(dob) when is_integer(dob), do: {:ok, dob}
defp parse_dob(_invalid), do: {:error "dob must be an integer"}

defp parse_name(nil), do: {:error, "name is required"}
defp parse_name(""), do: parse_name(nil)
defp parse_name(name), do: {:ok, name}

Every statement of with is executed in order. Execution continues as long as left <- right match. As soon as a match fails, the else block is executed. Within the else block we can match against whatever WAS returned. If all statements match, the do block is executed and has access to all the local variables in the with block.

Got it? Here's a test. Why did we have to change our success cases so that they'd return {:ok, dob} and {:ok, name} instead of just dob and name?

If we didn't return {:ok, X}, our match would look like:

def create(params) do
  with dob <- parse_dob(params["dob"]),
       name <- parse_name(params["name"])
  do
    %User{dob: dob, name: name}
  else
    err -> err
  end
end
...

However, a variable on the left side matches everything. In other words, the above would treat the {:error, "BLAH"} as matches and continue down the "success" path.

If you found this useful, check out my short series Elixir, A Little Beyond The Basics to get more fundamental understanding of Elixir.