New EEP draft: Pinning operator ^ in patterns
Leonard B
leonard.boyce@REDACTED
Thu Jan 14 19:13:54 CET 2021
I've been mulling this over ever since it was first announced and I
have to admit I have a fairly strong reaction to the proposal.
I'm not a contributor, but I've been using Erlang in anger daily for
12 odd years now.
I appreciate the utility of the proposed pinning operator in two specific cases:
1. matching against a previously bound variable name in function heads of funs
2. matching against a previously bound variable name in list comprehensions
In both these cases, as I understand it, we end up saving a few
characters and a temp variable name.
As to eventually making it an error, that I'd be very much against.
Having to update every single piece of code ever written to now add an
extra character in every case we want to match against a previously
defined variable name seems like an excessive burden.
There is also the unfortunate side effect of it introducing confusion
thanks to the ties with Elixir syntax. I've been seeing this both here
on the list and in Slack discussions.
Feel free to ignore the following...
I ask myself, since there are already 'erlangy' scoping rules for
variables (esp with funs) would it not just be better to change those
to more fully allow defined variables from outer scope within
fun/comprehension scope?
IE, if a variable is previously assigned within the outer scope use
that variable within the inner scope.
This, in my addled mind, makes more sense
EG:
%% X is from outer scope, L is from outer scope, V is local to comprehension
X = 5.
L = [1,2,3,4,5].
[V || {X, V} <- L].
On Thu, Jan 14, 2021 at 8:14 AM Richard Carlsson
<carlsson.richard@REDACTED> wrote:
>
> The way I planned it is:
> 1. Even from the start, pinning will always be allowed, without requiring any flag to opt in. This does not tell you about existing uses of already-bound variables, but you can start using pinning right away for readability and for avoiding bugs when refactoring. The compiler will always tell you if a pinned variable doesn't exist, so you don't accidentally accept any value in that position.
> 2. You can enable warnings at your own pace in order to start cleaning up your code.
> 3. In a following major release, the warnings will be on by default, but you can disable them to compile old code.
> 4. In a distant future, it might become an error to not use ^ to mark already-bound variables.
>
> /Richard
>
>
> Den tors 14 jan. 2021 kl 13:33 skrev Raimo Niskanen <raimo+erlang-questions@REDACTED>:
>>
>> As others have said: for Elixir this operator is essential, since they
>> rebind variables without it.
>>
>> For Erlang, if using a pinning operator had been required from the start;
>> I think that would have been a bit better than the current "match
>> if already bound". It is hard to be sure by looking at the code
>> if the variable is already bound - you have to make a machine search.
>>
>> Introducing a pinning operator now is trickier...
>>
>> Having a compiler option to choose if pinning is allowed/required makes it
>> hard to know what to expect from the code. The compiler option is set in
>> some Makefile far away from the source code.
>>
>> I think I would prefer that instead there should be a compiler pragma
>> (I wish it would not be allowed from an include file but that is probably
>> impossible to enforce) so it is visible in the current module what to
>> expect about operator pinning. Without the pragma the pinning operator is
>> not allowed, with it pinning is mandatory; not a warning - an error if
>> a pinning operator is missing.
>>
>> You get the idea: it should be possible from the source code how to read
>> it, at least on the module level.
>>
>> How to take the next step i.e when code not using pinning is the exception,
>> to remove the compiler pragma, I have not thought about yet...
>>
>> Cheers
>> / Raimo Niskanen
>>
>>
>>
>> On Thu, Dec 24, 2020 at 09:10:17PM +0100, Richard Carlsson wrote:
>> > The ^ operator allows you to annotate already-bound pattern variables as
>> > ^X, like in Elixir. This is less error prone when code is being refactored
>> > and moved around so that variables previously new in a pattern may become
>> > bound, or vice versa, and makes it easier for the reader to see the intent
>> > of the code.
>> >
>> > See also https://github.com/erlang/otp/pull/2951
>> >
>> > Ho ho ho,
>> >
>> > /Richard & the good folks at WhatsApp
>>
>> > Author: Richard carlsson <carlsson.richard(at)gmail(dot)com>
>> > Status: Draft
>> > Type: Standards Track
>> > Created: 21-Dec-2020
>> > Erlang-Version: 24
>> > Post-History: 24-Dec-2020
>> > ****
>> > EEP XXX: Pinning operator ^ in patterns
>> > ----
>> >
>> >
>> > Abstract
>> > ========
>> >
>> > This EEP proposes the addition of a new unary operator `^` for
>> > explicitly marking variables in patterns as being already bound. This
>> > is known as "pinning" in Elixir - see [Elixir doc][the Elixir
>> > documentation].
>> >
>> > For example:
>> >
>> > f(X, Y) ->
>> > case X of
>> > {a, Y} -> ok;
>> > _ -> error
>> > end.
>> >
>> > could be written more explicitly:
>> >
>> > f(X, Y) ->
>> > case X of
>> > {a, ^Y} -> ok;
>> > _ -> error
>> > end.
>> >
>> > In Elixir, this operator is strictly necessary for being able to refer
>> > to the value of a bound variable as part of a pattern, because
>> > variables in patterns are always regarded as being new shadowing
>> > instances (like in Erlang's fun clause heads), unless explicitly
>> > pinned.
>> >
>> > In Erlang, they would be optional, but are still a good idea because
>> > they make programs more robust under edits and refactorings, and
>> > furthermore allow the use of pinned variables in fun clause heads and
>> > in comprehension generator patterns.
>> >
>> >
>> > Specification
>> > =============
>> >
>> > A new unary operator `^` is added to Erlang, called the "pinning
>> > operator". It may only be used in patterns, and only on variables.
>> > Its meaning is that the "pinned" variable is to be interpreted in the
>> > enclosing environment of the pattern, and its value used in its place
>> > for that position in the pattern.
>> >
>> > In current Erlang, this behaviour is what happens automatically in
>> > ordinary matching constructs if the variable is already bound in the
>> > enclosing environment. In the following example:
>> >
>> > f(X, Y) ->
>> > case X of
>> > {a, Y} -> {ok, Y};
>> > _ -> error
>> > end.
>> >
>> > the use of `Y` in the pattern is regarded as a reference to the
>> > function parameter `Y`, instead of as introducing a new variable, and
>> > the `Y` in the clause body is then that same parameter. Therefore,
>> > annotating the pattern variable as `^Y` in this case does not change
>> > the behaviour of the program, but makes the intent explicit:
>> >
>> > f(X, Y) ->
>> > case X of
>> > {a, ^Y} -> {ok, Y};
>> > _ -> error
>> > end.
>> >
>> > For fun expressions and list comprehension generator patterns, the
>> > pinning operator makes the language more expressive. Take the
>> > following Erlang code:
>> >
>> > f(X, Y) ->
>> > F = fun ({a, Y}) -> {ok, Y};
>> > (_) -> error
>> > end,
>> > F(X).
>> >
>> > Here, the occurrence of `Y` in the clause head of the fun `F` is a new
>> > variable instance, shadowing the `Y` parameter of `f(X, Y)`, and the
>> > fun clause will match any value in that position. The `Y` in the
>> > clause body is the one bound in the clause head. However, using the
>> > pinning operator, we can selectively match on variables bound in the
>> > outer scope:
>> >
>> > f(X, Y) ->
>> > F = fun ({a, ^Y}) -> {ok, Y};
>> > (_) -> error
>> > end,
>> > F(X).
>> >
>> > In this case, there is no new binding of `Y`, and the use of `Y` in
>> > the fun clause body refers to the function parameter. But it is also
>> > possible to combine pinning and shadowing in the same pattern:
>> >
>> > f(X, Y) ->
>> > F = fun ({a, ^Y, Y}) -> {ok, Y};
>> > (_) -> error
>> > end,
>> > F(X).
>> >
>> > In this case, the pinned field refers to the value of the function
>> > function parameter, but there is also a new shadowing binding of `Y`
>> > to the third field of the tuple. The use in the fun clause body now
>> > refers to the shadowing instance.
>> >
>> > Generator patterns in list comprehensions or binary comprehensions
>> > follow the same rules as fun clause heads, so with pinning we can for
>> > example write the following code:
>> >
>> > f(X, Y) ->
>> > [{b, Y} || {a, ^Y, Y} <- X].
>> >
>> > where the `Y` in `{b, Y}` is the shadowing instance bound to the third
>> > element of the pattern tuple.
>> >
>> > Finally, a new compiler flag `warn_unpinned_vars` is added, disabled
>> > by default, which if enabled makes the compiler emit warnings about
>> > all uses of already bound variables in patterns that are not
>> > explicitly annotated with the `^` operator. This allows users to
>> > migrate their code module by module towards using explicit pinning in
>> > all their code. If pinning becomes the norm in Erlang, this flag
>> > could be turned on by default, and eventually, the pinning operator
>> > could become strictly required for referring to already bound
>> > variables in patterns.
>> >
>> >
>> > Rationale
>> > =========
>> >
>> > The explicit pinning of variables in patterns make programs more
>> > readable, because the intent of the code becomes clear. When already
>> > bound variables are used in Erlang without any annotation, anyone
>> > reading a piece of code must first study it closely to understand
>> > which variables will be bound at the point of a pattern, before they
>> > can tell whether any pattern variable is a new binding or implies an
>> > equality assertion. This is easy to miss even for experienced
>> > Erlangers, be it during code reviews or while trying to understand a
>> > piece of poorly commented code.
>> >
>> > Perhaps more importantly, pinning also makes programs more robust
>> > under edits and refactorings. Take our previous example, and add a
>> > print statement:
>> >
>> > f(X, Y) ->
>> > io:format("checking: ~p", [Y]),
>> > case X of
>> > {a, Y} -> {ok, Y};
>> > _ -> error
>> > end.
>> >
>> > Suppose someone renames the function parameter from `Y` to `Z` and
>> > updates the print statement but forgets to update the use in the case
>> > clause. Without an explicit pinning annotation, the change would be
>> > quietly allowed, but the `Y` in the pattern would be interpreted as a
>> > new variable that will match any value, which will then be used in the
>> > body. This changes the behaviour of the program. If the use in the
>> > pattern had been annotated as `^Y`, the compiler would have generated
>> > an error "Y is unbound" and the mistake would have been caught.
>> >
>> > When code is being modified to add a feature or fix a bug, a
>> > programmer might want to introduce a new variable for a temporary
>> > result. In a long function body, this risks introducing a new bug.
>> > Consider the following:
>> >
>> > g(Stuff) ->
>> > ...
>> > Thing = case ... of
>> > {a, T} -> T;
>> > _ -> 0
>> > end,
>> > ...
>> > {ok, [Thing|Stuff]}.
>> >
>> > Here, `T` is a new variable, clearly intended as just a temporary and
>> > local variable for extracting the second element of the tuple. But
>> > suppose that someone adds a binding of the name `T` further up in the
>> > function body, without noticing that the name is already in use:
>> >
>> > g(Stuff) ->
>> > ...
>> > T = q(Stuff) + 1,
>> > io:format("~p", [p(T)]),
>> > ...
>> > Thing = case ... of
>> > {a, T} -> T;
>> > _ -> 0
>> > end,
>> > ...
>> > {ok, [Thing|Stuff]}.
>> >
>> > Now the first clause of the case switch will only match if the second
>> > element of the tuple has the exact same value as the previously
>> > defined `T`. Again, the compiler quietly accepts this change, while
>> > if it had been instructed to warn about all non-annotated uses of
>> > already bound variables in patterns, this mistake would have been
>> > detected.
>> >
>> >
>> > Shadowing in Funs and Comprehensions
>> > ------------------------------------
>> >
>> > In funs and comprehensions, pinning also lets us do things that
>> > otherwise requires additional temporary variables. Consider the
>> > following code:
>> >
>> > f(X, Y) ->
>> > F = fun ({a, Y}) -> {ok, Y};
>> > (_) -> error
>> > end,
>> > F(X).
>> >
>> > Since the `Y` in the clause head of the fun is a new shadowing
>> > instance, the pattern will match any value in that position. To match
>> > only the value passed as `Y` to `f`, a clause guard must be added, and
>> > a temporary variable be used to access the outer `Y`:
>> >
>> > f(X, Y) ->
>> > OuterY = Y,
>> > F = fun ({a, Y}) when Y =:= OuterY -> {ok, Y};
>> > (_) -> error
>> > end,
>> > F(X).
>> >
>> > We could instead rename the inner use of `Y` to avoid shadowing, but
>> > the equality test must still be written as an explicit guard:
>> >
>> > f(X, Y) ->
>> > F = fun ({a, Z}) when Z =:= Y -> {ok, Y};
>> > (_) -> error
>> > end,
>> > F(X).
>> >
>> > With the help of the pinning operator, such things are no longer a
>> > concern, and we can simply write:
>> >
>> > f(X, Y) ->
>> > F = fun ({a, ^Y}) -> {ok, Y};
>> > (_) -> error
>> > end,
>> > F(X).
>> >
>> > Furthermore, in the odd case that the pattern would both need to
>> > access the surrounding definition of `Y` as well as introduce a new
>> > shadowing binding, this can be easily written using pinning:
>> >
>> > f(X, Y) ->
>> > F = fun ({a, ^Y, Y}) -> {ok, Y};
>> > (_) -> error
>> > end,
>> > F(X).
>> >
>> > but in current Erlang, two separate temporary variables would be
>> > required:
>> >
>> > f(X, Y) ->
>> > OuterY = Y,
>> > F = fun ({a, Temp, Y}) when Temp =:= OuterY -> {ok, Y};
>> > (_) -> error
>> > end,
>> > F(X).
>> >
>> > As explained before, the same goes for patterns in generators of
>> > comprehensions.
>> >
>> >
>> >
>> > Backwards Compatibility
>> > =======================
>> >
>> > The addition of a new and previously unused operator `^` does not
>> > affect the meaning of existing code, and the compiler will not emit
>> > any new warnings or errors for existing code, unless explicitly
>> > enabled with `warn_unpinned_vars`. This change is therefore fully
>> > backwards compatible.
>> >
>> >
>> >
>> > Implementation
>> > ==============
>> >
>> > The implementation can be found in [PR #2951][pr].
>> >
>> >
>> >
>> > Copyright
>> > =========
>> >
>> > This document has been placed in the public domain.
>> >
>> >
>> > [Elixir doc]: https://elixir-lang.org/getting-started/pattern-matching.html#the-pin-operator
>> > "Elixir pattern matching - the pin operator"
>> >
>> > [pr]: https://github.com/erlang/otp/pull/2951
>> > "#2951: Add a new operator ^ for pinning of pattern variables"
>> >
>> >
>> >
>> > [EmacsVar]: <> "Local Variables:"
>> > [EmacsVar]: <> "mode: indented-text"
>> > [EmacsVar]: <> "indent-tabs-mode: nil"
>> > [EmacsVar]: <> "sentence-end-double-space: t"
>> > [EmacsVar]: <> "fill-column: 70"
>> > [EmacsVar]: <> "coding: utf-8"
>> > [EmacsVar]: <> "End:"
>> > [VimVar]: <> " vim: set fileencoding=utf-8 expandtab shiftwidth=4 softtabstop=4: "
>>
>>
>> --
>>
>> / Raimo Niskanen, Erlang/OTP, Ericsson AB
More information about the erlang-questions
mailing list