Thomas Browne
Thomas Browne

Reputation: 24938

Idiomatic handling of missing fields when deconstructing an Elixir map?

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

Answers (3)

Aleksei Matiushkin
Aleksei Matiushkin

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

Adam Millerchip
Adam Millerchip

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

Everett
Everett

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

Related Questions