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