How to use the "with" keyword in Elixir and what is it for?

In elixir 1.2 they've included the keyword "with", but it's not completely clear to me what it is for.

How and in which situation would I use it?


In versions of Elixir prior to 1.2 when using functions in a pipeline, you would either have to use a monad library or nest case statements (which could be refactored using private functions, but still would end up being verbose). with/1 allows a different way to solve this problem.

Here is an example from the original proposal:

case File.read(path) do
  {:ok, binary} ->
    case :beam_lib.chunks(binary, :abstract_code) do
      {:ok, data} ->
        {:ok, wrap(data)}
      error ->
        error
    end
  error ->
    error
end

Here is the same thing refactored to use functions:

path
|> File.read()
|> read_chunks()
|> wrap()

defp read_chunks({:ok, binary}) do
  {:ok, :beam_lib.chunks(binary, :abstract_code)}
end
defp read_chunks(error), do: error

defp wrap({:ok, data}) do
  {:ok, wrap(data)}
end
defp wrap(error), do: error

And the same code using with:

with {:ok, binary} <- File.read(path),
     {:ok, data} <- :beam_lib.chunks(binary, :abstract_code),
     do: {:ok, wrap(data)}

This works because with will only keep chaining if the value matches the pattern on the left. If not then the chain is aborted and the first non-matching result is returned. For example if the file does not exist then File.read(path) will return {:error, :enoent} - this does not match {:ok, binary} so the with/1 call will return {:error, :enoent}.

It is worth noting that with can be used with any pattern, not just {:ok, foo} and {:error, reason} (although it is a very common use case).


You can also chain "bare expressions", as the doc says:

with {:ok, binary} <- File.read(path),
     header = parse_header(binary),
     {:ok, data} <- :beam_lib.chunks(header, :abstract_code),
     do: {:ok, wrap(data)}

The variable header will be available only inside the with statement. More info at https://gist.github.com/josevalim/8130b19eb62706e1ab37


One thing to mention, you can use when guard in with statement. E.g,

defmodule Test do
  def test(res) do
    with {:ok, decode_res} when is_map(decode_res) <- res
    do
      IO.inspect "ok"
    else
      decode_res when is_map(decode_res) -> IO.inspect decode_res
      _ ->
        IO.inspect "error"
    end
  end
end
Test.test({:ok , nil})
Test.test({:ok , 12})
Test.test({:ok , %{}})