Cannot pattern-match a key in struct using `in`

Hello,
I have come across a weird (to me) situation.
When I receive response from DB I want to pattern match it like so:

response = {:ok, [%SomeStruct{hidden: true}]}
case response do
  {:ok, result} when result in [[], [%{hidden: true}]] ->
    :do_something
  _ ->
    :do_something_else
end

Basically I’m trying to route code execution to first branch if the result is either [] or has one item, struct, that has key :hidden set to true (e.g. [%{hidden: true}]).
But the match does not succeeds… Of course I can write it out without the use of when result in ... (resulting in 2 branches with identical code) but I would like to know why is this happening…

Thanks,
Have a nice day

The problem is the fact that when you match on a map, you don’t have to list all the keys.

case %{a: 1} do
  %{} -> will succeed
end

But when you use in you have to match on the exact term.

4 Likes

Since in checks exact match, instead of pattern matching, you have to pass the whole expected struct to be matched.

iex(1)> [%{some: 5}] in [[%{some: 5, other: 7}]]
false
3 Likes

Oh I see. I had the feeling it would be something like that… Didnt know that it exactly matches the right-hand side… :slight_smile:

Thanks guys!

1 Like

Avoid duplicating the branch though. You can do something like that:

    response = {:ok, %{hidden: true, other: "field"}}

    case response do
      {:ok, result} when result == [] when :erlang.map_get(:hidden, result) == true ->
        :do_something

      _ ->
        :do_something_else
    end

Edit: reading the first and last posts of this topic I would believe Elixir had a map_get function but it does not seem so.

1 Like

I think proposal #1 in that thread is that we should now use

defguard is_hidden(%{hidden: value}) when value == true

and then

case response do
  {:ok, result} when is_hidden(result) ->
    :do_something

which allows us to benefit from :erlang.map_get/2 without having to explicitly call it.

4 Likes

So you are saying that before the proposal we could not use a property in a matched map in defguard? Because I believe def some_func(%{hidden: value}) when value == true would work in early Elixir versions already.

defguard is available since 1.6 and matching on map keys in guards is available since 1.10 (as this is earliest version that support OTP 21+ only).

4 Likes

Wow. Didn’t know that I can create own guards… that’s pretty neat… It will reduce the function heads’ sizes significantly.

Just a clarification: The result is always an array. Either it empty or it has single map with hidden: bool.
So is there any function callable from guard that can extract enum’s item at index? I don’t know if Enum.at/1 is callable from guard.
Something like {:ok, res} when res == [] or is_hidden(Enum.at(res, 0)) ->.
(I’m not with the PC right now, so cannot try it…)

You can use hd/1/tl/1 tango if it is a list. In general sense it is not possible. And you always could define your own guards, just in past it was more troublesome as you needed to write whole macro on your own.

For example this:

defguard is_hidden(%{hidden: value}) when value == true

Would need to be written as:

defmacro is_hidden(map) do
  quote do
    :erlang.map_get(:hidden, map) == true
  end
end

You can do it like:

{:ok, res} when res == [] or is_hidden(hd(res))
2 Likes