Reputation: 24938
What is the best way to get a "default" value in Elixir, or to trap an error, in the case of a pattern matching error in a map deconstruction?
iex(1)> %{"a" => a} = %{"a" => 1, "b" => 2}
%{"a" => 1, "b" => 2}
iex(2)> a
1
iex(3)> %{"m" => m} = %{"a" => 1, "b" => 2}
** (MatchError) no match of right hand side value: %{"a" => 1, "b" => 2}
Has to work cleanly in deeply nested cases too:
iex(4)> %{"b" => %{"c" => %{"e" => myvar}}} = %{"a" => 1, "b" => %{"c" => %{"d" => 4, "e" => 5}}}
%{"a" => 1, "b" => %{"c" => %{"d" => 4, "e" => 5}}}
iex(5)> myvar
5
iex(6)> %{"b" => %{"c" => %{"e" => myvar}}} = %{"a" => 1, "f" => 6}
** (MatchError) no match of right hand side value: %{"a" => 1, "f" => 6}
So in cases above I like a
and myvar
to fallback to a default, or else some kind of clean way to branch to a handler function. If possible, I'd prefer a solution which does not involve an error handler though.
Upvotes: 0
Views: 1115
Reputation: 121010
It obviously cannot be done with match because there is literally impossible to fit the default with the match syntax.
If you know that the map has no nil
values you might do somewhat like get_in/2
, and then use default if the outcome is nil
.
Also, it’s not as complicated to produce a function yourself.
safe_get_in = fn keys, input, default ->
Enum.reduce_while(keys, input, fn key, acc ->
case acc do
%{^key => level_down} -> {:cont, level_down}
_ -> {:halt, default}
end
end)
end
safe_get_in.(~w|a b c d|, %{"a" => 1, "f" => 6}, 42)
#⇒ 42
safe_get_in.(~w|b c d|, %{
"a" => 1, "b" => %{"c" => %{"d" => 4, "e" => 5}}}, 42)
#⇒ 4
This is not very common to get to deeply nested value without knowing whether it’s there or not, that’s why there is no implementation for the case in the standard library.
Upvotes: 0
Reputation: 23147
From the deleted comment to Aleksei's answer:
yes I can write a function, but I'm looking for an idiomatic answer.
The idomatic thing to do in Elixir is to fail if the match fails, or to ensure that the match always succeeds.
Depending on the context there are multiple ways to provide "default" clauses that will always match. Here are 3 examples using case
, with
, and multiple function clauses.
Case:
case value do
%{"a" => a} -> a
_ -> "default value"
end
With:
with %{"a" => a} <- value do
a
else
_ -> "default value"
end
Function clauses:
def default_function_clause(%{"a" => a}), do: a
def default_function_clause(_), do: "default value"
In all cases, providing a map with an "a"
key will return the value, otherwise "default value"
.
Upvotes: 4
Reputation: 9638
I'll let others with more experience weigh in on what's more idiomatic, but I will often provide optional defaults via a module attribute and then allow overrides via a merge function. Something like this (using Map.merge/2 or Keyword.merge/2 for keyword lists):
defmodule Something do
@defaults %{"a" => "alpha", "b" => "beta"}
def foo(input) do
input_or_defaults = Map.merge(@defaults, input)
# ...
end
end
If your question is really delving into error trapping, then here's an example of an "implicit rescue" that would handle the case when there is no match:
def risky_stuff(input) do
do_match(input)
rescue e in UndefinedFunctionError ->
{:error, "Unable to match"}
end
defp do_match(%{"a" => a}), do: "something with a"
defp do_match(%{"b" => b}), do: "something with b"
Have a look at the "implicit try" for some explanation of this.
However, I think you'll find the functionality you need with a merge.
Upvotes: 1