57

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?

diogovk
  • 2,108
  • 2
  • 19
  • 24

3 Answers3

86

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

c80609a
  • 5
  • 4
Gazler
  • 83,029
  • 18
  • 279
  • 245
  • 3
    Indeed the "with version" looks much better. This is very useful. – diogovk Dec 11 '15 at 12:19
  • I think the body of `defp read_chunks` should just read `:beam_lib.chunks(binary, :abstract_code)` (i.e., without the initial `{:ok,`) – Grandpa Oct 16 '16 at 17:47
  • I feel like ok_jose (https://github.com/vic/ok_jose) looks much cleaner (apart from the name). Is there any reason not to use that instead? – Joe Eifert Nov 14 '16 at 21:23
  • 1
    Your example doesn't show how to handle the error. You say with will return the error, does that mean we should assign with to a variable for further checks if error occurred? Can you show an example of how the error is handled with a with statement – Terence Chow Dec 07 '16 at 12:06
  • 2
    The result of with is the error if there is an error. You can assign and check on that, however if you are using Elixir 1.3, then you can use with..else as covered in http://elixir-lang.org/docs/v1.3/elixir/Kernel.SpecialForms.html#with/1 – Gazler Dec 07 '16 at 12:08
18

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

leandrocp
  • 181
  • 1
  • 4
4

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 , %{}})
chris
  • 2,761
  • 17
  • 24