matching on maps

classic Classic list List threaded Threaded
3 messages Options
Reply | Threaded
Open this post in threaded view
|

matching on maps

Andreas Schultz-3
Hi,

In the OTP-23 blog entry, there is an example of a illegal map match:

  illegal_example(Key, #{Key := Value}) -> Value.

Can someone explain why the compiler couldn't automatically rewrite that to:

   legal_example(Key, Map) when is_map_key(Key, Map) -> maps:get(Key, Map).

Such a rewrite should be possible whenever there are further restrictions (matches or guards) on the content of Value, right?

Andreas
Reply | Threaded
Open this post in threaded view
|

Re: matching on maps

Fred Hebert-2
On 05/13, Andreas Schultz wrote:

>In the OTP-23 blog entry, there is an example of a illegal map match:
>
>  illegal_example(Key, #{Key := Value}) -> Value.
>
>Can someone explain why the compiler couldn't automatically rewrite that to:
>
>   legal_example(Key, Map) when is_map_key(Key, Map) -> maps:get(Key, Map).
>
>Such a rewrite should be possible whenever there are further restrictions
>(matches or guards) on the content of Value, right?

I don't think it would be impossible, but it just makes you hit another
barrier rather rapidly:

     tricky(Key, Val, #{Key := Val}) -> numberwang;
     tricky(Key, Val, #{Key := Value}) when Val > Value -> boom;
     tricky(Key, _,   #{Key := Val}) when is_number(Val) -> keep_trying;
     ...

Those can all feel like natural extensions that wouldn't work if the thing you
do is just matching the key through the guards. Anything that wants to do work
with the value can cause problems.

There's also ambiguous definitions of what it means for the `Key' variable to
be bound:

     weird(#{K := _}, #{K := _}) when K == 5 -> ???

Clearly this one represents a big search that doesn't make sense and won't
work the way you could match heads of a list. However `K` is bound in a guard.
Should the compiler be smart enough to handle it and propagate to the match
clause? What if there are more guard options?

Some cases would however be easier to deal with I guess, like:

     weird(Opt=concise, #{Opt := Val}) -> Val

It sounds like `Opt` is clearly bound and shouldn't be confusing!

In all cases I'm guessing here that it's been simpler to simply not allow the
patterns rather than add partial support for a small subset of the potential
matches that could exist.

Regards,
Fred.
Reply | Threaded
Open this post in threaded view
|

Re: matching on maps

Björn Gustavsson-4
In reply to this post by Andreas Schultz-3
On Wed, May 13, 2020 at 3:09 PM Andreas Schultz
<[hidden email]> wrote:
>
> In the OTP-23 blog entry, there is an example of a illegal map match:
>
>   illegal_example(Key, #{Key := Value}) -> Value.
>
> Can someone explain why the compiler couldn't automatically rewrite that to:
>
>    legal_example(Key, Map) when is_map_key(Key, Map) -> maps:get(Key, Map).
>

We actually tried to make your example legal. The transformation of
the code that we did was not to rewrite to guards, but to match
arguments or parts of argument in the right order so that variables
that input variables would be bound before being used. (We would do a
topological sort to find the correct order.) For your example, the
transformation would look similar to this:

legal_example(Key, Map) ->
  case Map of
     #{Key := Value} -> Value;
    _ -> error(function_clause, [Key, Map])
  end.

In the prototype implementation, the compiler could compile the
following example:

convoluted(Ref,
       #{ node(Ref) := NodeId, Loop := universal_answer},
       [{NodeId, Size} | T],
       <<Int:(Size*8+length(T)),Loop>>) when is_reference(Ref) ->
    Int.

Things started to fall apart when variables are repeated. Repeated
variables in patterns already have a meaning in Erlang (they should be
the same), so it become tricky to understand to distinguish between
variables being bound or variables being used a binary size or map
key. Here is an example that the prototype couldn't handle:

foo(#{K := K}, K) -> ok.

A human can see that it should be transformed similar to this:

foo(Map, K) ->
  case Map of
    {K := V} when  K =:= V -> ok
  end.

Here are few other examples that should work but the prototype would
refuse to compile (often emitting an incomprehensible error message):

bin2(<<Sz:8,X:Sz>>, <<Y:Sz>>) -> {X,Y}.

repeated_vars(#{K := #{K := K}}, K) -> K.

match_map_bs(#{K1 := {bin,<<Int:Sz>>}, K2 := <<Sz:8>>}, {K1,K2}) -> Int.

Another problem was when example was correctly rejected, the error
message would be confusing.

Because much more work would clearly be needed, we have shelved the
idea for now. Personally, I am not sure that the idea is sound in the
first place. But I am sure of one thing: the implementation would be
very complicated.

/Björn

--
Björn Gustavsson, Erlang/OTP, Ericsson AB