55
votes

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?

3

3 Answers

83
votes

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).

18
votes

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

4
votes

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 , %{}})