How to use the "with" keyword in Elixir and what is it for?
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
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).
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 , %{}})