What is the preferred way to pattern match a Keyword list inside a Case?

Is there an idiomatic way in Elixir to write a case guard to pattern match on the value of a key inside a Keyword List?

The keyword list has an arbitrary number of keywords, so matching agains the entire list is not an option.

The best we can come up with is a nested case statement to match against Keyword.get, is there a cleaner way?

# Contrived example for discussion purposes.
def run_something(args) do 
  
  # calculate_something returns:
  #   {:ok, response}
  #   {error, [message: string, code: atom, ...] }

  case calculate_something(args) do
    {:ok, response} -> 
      response

    # How do you perform such a pattern match? 
    {:error, reason} when Keyword.get(reason, code:) == :timeout -> 
      # Maybe try again? 

    {:error, reason} when Keyword.get(reason, code:) == :auth_failed -> 
      # Maybe try to re-authenticate? 
    
    {:error, reason } -> 
      # Unknown reason, abort.
  end
end

Keyword list keys can be duplicated, and they can be at an arbitrary position as well, so the answer to your direct question is “no”. You can pattern match e.g. [{:desired_key, _value} | _rest] but that will only work if :desired_key is the key of the first element in a keyword list and in no other conditions.

I don’t view your code as boilerplate-y or clunky but if there is a chance that those error clauses can explode you can indeed just make a helper function i.e.

case stuff do
   {:ok, value} -> value
   {:error, anything} -> act_on_error(anything)
end

…and then use the Keyword module functions inside the helper function to manipulate the embedded error value further.

1 Like

Note too that you can’t invoke arbitrary functions in a guard. Guards need a constant time execution guarantee and therefore only a small set of functions are permitted.

If you really need structured results, consider using a map rather than a keyword list. Map keys can be used in guard clauses.

2 Likes

We’re calling into a third-party library, [Joken](https://hexdocs.pm/joken/Joken.html#t:error_reason/0), that returns the keyword list.

Our use case is decoding JWT tokens. If the token fails to validate, then there are certain conditions we can handle. (Example: the token has expired and we can refresh it.) Depending on the token we’re dealing with, we might handle things a bit differently, so we don’t have a single “decode token and handle all failures” codepath, yet.

1 Like

To expand on the implementation of @dimitarvp’s answer, in with cond may read a little tighter:

defp act_on_error(reason) do
  cond do
    {:code, :timeout} in reason ->
      # ...

    {:code, :auth_failed} in reason ->
      # ...

    # ...
  end
end

In which case I would have calculate_something(args) return a {:ok, code, result} or {:error, code, result} tuple so you can directly match on the code.

1 Like

Now that is a syntax we would not have considered as new Elixir developers. I guess this works because a keyword list is just a list of 2-item tuples. TIL, thanks!

2 Likes

Yep! I’m repeating myself from another recent post but it’s just a list with some very specific contents and nothing more! All the functions in List, Enum, and Stream work exactly as they would on any other list.

1 Like