What happened to map access syntax?

In the EEP for maps there’s access syntax. Specifically :

V = M#{ K }.

Whatever happened to this? I can imagine it was too hard to implement, perhaps along side records, but that’s an assumption.

5 Likes

There’s also the use of maps as generators in comprehensions, and map comprehensions. I guess maybe those ended up not making sense for one reason or other in the end?

[ {K, V} || K := V <- map() ].
#{ E => f(E) || E <- list() }.
4 Likes

That check is actually in the mail : Map comprehensions by seriyps · Pull Request #4856 · erlang/otp · GitHub

In which OTP team said they would accept the implementation barring a little more work that needs to be done, including in EEP, that has also been started : Map comprehensions EEP by seriyps · Pull Request #32 · erlang/eep · GitHub

:slight_smile:

7 Likes

If I recall correctly the original implementation did not include the access syntax because the proposed syntax was not intuitive enough and maps:get/find was seen as good enough for accessing single values. Also in many cases, the matching syntax can (and should be used) instead. For example:

Don’t write:

foo(M) ->
  bar(maps:get(f1,M), maps:get(f2,M), maps:get(f3,M)).

Do write:

foo(#{ f1 := F1, f2 := F2, f3 := F3 }) ->
  bar(F1, F2, F3).

Using the matching syntax allows the compiler to group reading of values to make that more efficient and is often clearer.

The implementation of an access syntax would be trivial to do, but deciding on good syntax and semantics is (IMO) hard.

9 Likes

Since maps look like records without a name, I was expecting that map access would look like record access… without a name. It’s a bit ugly, but consistent with records, the obvious difference being that keys can be non-atoms.

M = #{a => 1, 2 => b},
M#.a
M#.2
M#.(3 - 1)

Keyword “often” cleaner. I’ve developed an aversion to doing big pattern matches purely for variable assignments in function heads, due to some very real-life examples of functions with three arguments of different size 15-20 tuples and various combinations of guards in 7 different function clauses where the sizes and shapes of every element included differed subtly. It took hours to figure out what part of the expansion in the function head was significant for pattern matching, and how.

I hardly blame the language for the poor code writing skills of some unknown, long-gone prior colleagues, but it’d be nice to have a way to elegantly extract the variables just when I need them, instead of batch extraction in the head. #{some_key := V} = M works, but that the value is assigned to the variable in the middle of the expression often causes me a double-take, and it is not possible to do f(M#{some_key := V}) to call f(maps:get(some_key, M)), instead a simple and trivial function call needs a wrapper like in your example.

6 Likes

That makes sense to me. I agree M#{V} is not a good syntax and don’t know what would be either. This question comes up from time to time and figured I would ask on here to get some kind of official answer.

4 Likes

I wonder how efficient this code is:

foo(#{ f1 := F1, f2 := F2}) ->
    case something(F1) of
        one ->
           do_more(F2);
        two ->
           ok
    end.

Compared to (imaginary):

foo (#{ f1 := F1} = Map) ->
    case something(F1) of
        one -> 
            do_more(Map#{f2});
        two ->
            ok
    end.

Here the major difference is that f2 does not have to be bound at all, unless something(F1) returns one.

I understand its possible to use do_more(maps:get(f2, Map)), but it feels a bit clunky. Same would’ve applied to records, but they have reasonably convenient access syntax.

4 Likes

Well ,the thing is that those two examples are not exactly equivalent, because 1st one would always “validate” that the map contains f2 key, while 2nd one can potentially work on #{f1 => ok} as input.

But yep, foo(#{f1 := ..., f2 := ...}) would try to lookup both keys “eagerly” (still just one beam instruction get_map_elements though)

4 Likes

It depends on whether the map is small (has at most 32 elements) or large.

If it is small, the time for looking up both f1 and f2 should be about the same as looking up just f1, because a matching of a small map is done using a sequential search for all keys to be matched at once. Also, besides the cost of the search itself, there is a cost for JITted code to call a helper function or BIF, and you only have to pay that cost once if you look up both keys at the same time.

If the map happens to contain 30 keys between f1 and f2 (e.g. f10, f11, …), just looking up f1 is probably better.

The Efficiency Guide describes the how maps are implemented.

5 Likes

Good point about JIT to BIF call overhead. But what if F2 is fairly expensive to copy on the heap?

3 Likes

F2 already is on the heap. There will be no copying, except for copying a single machine word from within the map to a BEAM register.

5 Likes

Well, that’s not exactly true :slight_smile:

I agree with what @Gwaeron said as possible syntax, but I would shoot for M.atoms_only, but that gets a bit odd if you want to end a form with an invocation (i.e., M.atoms_only.).

I think what would work best for Erlang is one access syntax either M{key} or M[Key]. I think in that case M{Key} might feel the most natural, but would keep in mind [Key] is easier to type per not having to hit the shift key, but it might be best to save that for something later, and besides maps already use curly braces.

So :

%% Or one access method to rule them all

1> M = #{foo => bar, {baz, eh} => huh}.
#{foo => bar,{baz,eh} => huh}
2> M1 = M#{this => that}.
#{foo => bar,this => that,{baz,eh} => huh}
3> M1{this}.
that
4> Key = {bar, eh}.
{bar,eh}
5> M{Key}.
huh

Haven’t looked, but I can’t imagine that would be difficult to implement.

2 Likes

I’m not suggesting it should ever be an option, but it would make sense to reserve [] syntax for lists in case getting an index or slice is ever added (e.g. L[3] or L[2:5]).

Also the shift argument is only true for some keyboard layouts, but not most European ones.

5 Likes

As a caveat, this is only true on some keyboard layouts.
On the French-Canadian layout I use, [ is alt+[, ] is alt+], { is alt+', and } is alt+#.

None of these are actually less work than one or the other, and M<key> would actually be the simplest one to type, though it also requires hitting the shift key :wink:

3 Likes

haha Ok ok, uncle! Fair enough @MononcQc and @eproxus :slight_smile:

2 Likes