Re-visiting EEP-0055

I fully support what Loïc Hoguin wrote in mailing list:

If annotations are generalized we could instead have

f(X, Y) →
case X of
{a, ^match Y} → {ok, Y};
_ → error
end.

and we could use them for other things such as

^remote Pid ! Msg

which can then be used by other tools to help better understand the
code. I don’t think we would end up with too many annotations ultimately
so using a single word for this is probably enough. No opinion on the ^
character.

Since this is annotations, this ought to be optional, although of course
teams may decide to force the use of ^match in their own projects along
with the related compiler warnings.

but I would go even further to say that we don’t need any special notation for this
and annotations could be any erlang term. For example:

remote Pid ! msg

{remote, "some other useful information"} Pid ! Msg

{provider, <<"serialization">>}
f(X) -> ....

then these can be used by parse transformations (one way how to implement and enforce pinning)
or other tools that require more context form programmer. This of course leads to “meta” representations/programming which i know lot of people dislike.
To discourage blatantly annotating everything with nonsence and/or clutter code, compiler could make error for all “unconsumed” annotations.
I can see this kind of proposal to be met with a lot of push back from the community and some possible arguments could be:

  • parse transformations or transformational tooling is not transparent
  • adding annotations changes spirit of erlang
  • it diminishes readability (do not clutter)

but when I see this kind of argument i remember quote from Bjarne Stroustrup:

“Every new powerful feature will be overused and misused”

moderation and care should eventually prevail and features like this could be very useful for library and frameworks authors.

IMO pinning operator is too narrow feature to be worth while, but it can be a good opportunity to start thinking about how to provide more information to tools that we already have.

2 Likes

I think {ok, ^Y, Y} -> Y isn’t the problem is appears to be at first. It would be preferable if there wasn’t the shadowing, but that is Erlang today. ^Y clearly says “this is different”.

But more importantly, the common use case is not with generic variable names but names that mean something useful and in which case you aren’t wanting to bind a new value for it.

4 Likes

I think this can indeed be an interesting path to explore but then there is a big question regarding shadowing here, which today allows “rebinding”. So while shadowing is phased out, during the next N years, fun(X) -> ... end cannot mean match on X.

Also, if Erlang chooses to signal rebinding, then it may be worth picking up another operator. Perhaps ~. Or perhaps a non-sigil based approach, since sigils are not very common in Erlang. In any case, I would agree the topic merits more discussion than accepting EEP 55 as is. :slight_smile:

EDIT: I also wrote sometime ago an article comparing variables in Erlang and Elixir and how the different approaches impact how much context a developer have to keep in mind when writing code: Comparing Elixir and Erlang variables - Dashbit Blog - the article argues that no rebounding + the pin operator would require the least context.

6 Likes

Yeah, I honestly really would prefer it to be all scoped to the main function, so the Y in f would be the same Y as in the fun, no pinning needed. Now if only that could be done in a backwards compatible way… Compiler switch? Or perhaps an opt-in -compile(something). or so per file? There really is no need for pinning.

I’m still for this ^.^

4 Likes

Same. I feel this is the way it should have always been. It seems more in line with single assignment.

4 Likes

I just wanted to point out that already today we have the case very similar to the “weird” {ok, ^Y, Y} patterns, except there’s no indication that something strange is going on.

This compiles:

foo(A) -> 
    fun(A, #{A + 2 := 3 + 4}) -> #{} end.

But the two As in the function head are distinct - the first argument is a new one that shadows the outer A, while the A inside the map key is the outer A.

In this case ^ would be a useful visual clue that something “strange” is going on:

foo(A) -> 
    fun(A, #{^A + 2 := 3 + 4}) -> #{} end.
4 Likes

I know Erlang’s scope rules are weird at times, but having two occurrences of A in the fun head denote entirely different things smells like a bug to me.

3 Likes

Agree. To me it seems like the way that should work, given single assignment everywhere in erlang, is for all the As in the outer and inner function heads to denote exactly the same thing - i.e. the value already bound to A when it’s received by foo/1 - and for the anonymous function’s A params to match the same value as A in foo/1.

I realise this may have a bunch of implementation implications, and probably backwards compatibility with some codebases, so I don’t know if it’s really realistic, but it seems so much more obvious a fix to the problem than to introduce e.g. a ^ and still leaving scope (pun intended :grimacing:) for confusion, by making another rule and piece of syntax that seems to just make more work for the reader, rather than enforcing the simplest possible reading. I could be missing plenty, though.

4 Likes

fully agree. I’m clueless as to the amount of work required to “fix scoping” as described, but maybe this is a candidate for slow inclusion via the new feature flag mechanism? This should allow it to be introduced over time eventually leading to deprecation of the current behavior.

2 Likes

That’s why I think it being an opt-in -compile(...). option in the file is best, and it could perhaps by swapped to be default sometime way later, or perhaps via a compiler flag or so, then just completely and outright fix it in a backwards incompatible way when that compile option is set.

5 Likes