Accessing a map element and adding parens after it might be confusing?


I discovered by accident last week this:


quote do %{hello: "test"}.hello() end == quote do %{hello: "test"}.hello end

I get that the AST is the same – with parens or the absence of parens, however why this is an allowed syntax?



It is simply because the language allows functions without parameters to be called without the parens. In your case .hello() is canonical Elixir while .hello is just a convenience shorthand syntax.

Sure, but here we are accessing a map that would return a binary in this case.
For functions it might make sense when arity is 0, however I guess this behaviour might generate confusion since

iex(1)> %{hello: "test"}.hello()

When we access hello we expect the value back and not a pleonastic () function call

The AST for both things is exactly the same. There’s no way for the compiler to differentiate between the two:

iex(1)> quote(do:
{{:., [], [{:foo, [], Elixir}, :bar]}, [], []}
iex(2)> quote(do:
{{:., [], [{:foo, [], Elixir}, :bar]}, [], []}

Thank you michal, I undestand that the AST is the same. See initial post regarding map access.
Since you are saying that the compiler does not differentiate, I guess we should live with that.

However having in code redundant parentheses (when accessing a map value) seems confusing to me.

iex(1)> %{hello: "test"}.hello()

is not a function call.

1 Like

Eh, technically it is a function call, it lowers down to become :maps.get(:hello, %{hello: "test"}). On the beam VM there is no such thing as a map extraction operator like ., rather you can extract only by matching out the value via a hardcoded key or by calling the built in internal functions in the :maps module, and Elixir lowers to them.


Thanks for elaborating @OvermindDL1
However it think that it might be confusing from the syntax perspective. In the end that’s what I mean.

For example:

iex(1)> defmodule Test do
...(1)> defstruct [:greeting]
...(1)> end
{:module, Test,
 <<70, 79, 82, 49, 0, 0, 5, 184, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 181,
   0, 0, 0, 18, 11, 69, 108, 105, 120, 105, 114, 46, 84, 101, 115, 116, 8, 95,
   95, 105, 110, 102, 111, 95, 95, 7, 99, ...>>, %Test{greeting: nil}}
iex(2)> %Test{greeting: "hello"}
%Test{greeting: "hello"}
iex(3)> test = %Test{greeting: "hello"}
%Test{greeting: "hello"}
iex(4)> test.greeting()
iex(5)> test.greeting  

Not sure if I am too “picky” on this one :wink:

1 Like

Here’s the macro that’s being called:
From the syntax perspective you’re calling greeting/0 on test quite similar to e.g.

mod = Node

which calls alive?/0 on mod. The AST alone just knows about mod the variable, but does not know if the variable will be a map or a alias or whatever.

1 Like

@LostKobrakai sure and thanks for the pointers, however don’t you find confusing that accessing to a map/struct and putting a () is the same as not putting it?
As a dev, especially after long hours of work, one may wonder if:

a_bound_var_or_mod = %{hello: "test"}

this is a function that a dev defined somewhere or just accessing a map/struct

Maybe is it just me that find this confusing? :woman_shrugging: :wink:

I suspect this is a transitory concern. I suspect you’re coming from an OO world (like most) where it is common to call methods on objects. As a functional language, Elixir has no methods at all. It’s functions. This . syntax just looks like a method/member access, but it’s really a function. Once you’ve spent more time with Elixir and internalized this information, the confusion should subside.


Not just you, but sometimes that ambiguity lets you override things too. At one time we had tuple calls too that worked like {SomeModule, whatever, else, you, want} and if you did blah = {SomeModule, whatever, else, you, want} then did blah.bloop(42) then it ended up calling SomeModule.bloop(42, {SomeModule, whatever, else, you, want}), which was crazy useful for emulating first-class modules on the BEAM, but thanks to recent developments that is now broken… :frowning:

Yep, exactly this.

Conceptually this:

# expands to:
:"."(a, b)
# expands to:
case a do
  mod when is_atom(mod) -> apply(mod, b, [])
  %{} -> apply(:maps, :get, [b, a, nil])
  # among some other things

Thanks to everyone, hope this thread will be useful to someone else.

I marked @OvermindDL1 post as “solution” because it contains also the explanation of @gregvaughn and it’s not possible to mark multiple solutions