Lists, Maps and Commas

This will be a super-quick post about something that came up at least twice in the last few months within the Erlang community. It’s related to the syntax of maps within a list.

(original image from Good Free Photos)

How Many Maps?

Quick question: how many maps do we have in the following list?

[#{a => 1},
#{b => 2}
#{c => 3}].
  • 1
  • 2
  • 3
  • That doesn’t even compile!

People following the erlang-questions mailing list might know already, but the correct answer is not 3, nor a compile error. It’s 2.

Why not 3?

Well, there is a missing comma in the second row.

But why does it compile, then?

Because what’s written there is a perfectly valid expression, of course. And it matches the syntax used for records, as it’s pointed out in this thread:

-module(sample).

-record (a, {field1, field2}).

-export([bug/0]).

bug() ->
[
#a{field1 = 1, field2 = "foof"} %% COMMA IS MISSING
#a{field1 = 2},
#a{field1 = 3}
].

that function on the shell returns a list with 2 elements, as expected…

1> rr(sample).
[a]
2> sample:bug().
[#a{field1 = 2,field2 = "foof"},
#a{field1 = 3,field2 = undefined}]
3>

What’s going on here?

What these expressions with the missing commas are expressing is understandable. In our example with maps what we are basically doing is:

[#{a => 1}, #{b => 2}#{c => 3}].

It looks weird but, if we would’ve used a variable there…

B = #{b => 2},
[#{a => 1}, B#{c => 3}].

On the other hand, you can’t just put anything on the left of the # sign there, check this out…

3> maps:new() #{b => 2}.
* 1: syntax error before: '#'

In that case, you must use parentheses for this to work…

3> (maps:new())#{b => 2}.
#{b => 2}

So, even when the original expression semantics are understandable, are they useful? Many of us will prefer the compiler/parser to warn us about this or straight up reject the code, but as Jesper L. Andersen points out

In short, regarding this as an invalid expression is to a certain extent
possible, and certainly desirable. But we run into subtle problems when we
want to reject it too, which is what complicates matters.

On the other hand, Dmytro had a great idea: Elvis can check for this. That’s why I opened issue #440 (link below), if you feel like contributing ;)


Bonus Track

While researching for this article I made a couple of extra tests on the console, just to see what would happen if… Check them out:

4> A = #{b => 3} #{b => 4}.
#{b => 4}
5> A.
#{b => 4}
6> (A = #{b => 3}) #{b => 4}.
** exception error: no match of right hand side value #{b => 3}
7> f(A).
ok
8> B = (A = #{b => 3}) #{b := 4}.
#{b => 4}
9> A.
#{b => 3}
10> B.
#{b => 4}

So, in case you were wondering, you can’t use the wacky syntax to do some strange pattern matching. You still have to use parentheses for that.