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

Hi,

I discovered by accident last week this:

Example:

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

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

Thanks

3 Likes

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()
"test"

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.bar)
{{:., [], [{:foo, [], Elixir}, :bar]}, [], []}
iex(2)> quote(do: foo.bar())
{{:., [], [{: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()
"test"

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.

3 Likes

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()
"hello"
iex(5)> test.greeting  
"hello"
iex(6)> 

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

1 Like

Here’s the macro that’s being called: https://hexdocs.pm/elixir/Kernel.SpecialForms.html#./2
From the syntax perspective you’re calling greeting/0 on test quite similar to e.g.

mod = Node
mod.alive?()

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"}
a_bound_var_or_mod.hello()

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.

4 Likes

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:

a.b
# 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
end
3 Likes

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

2 Likes