What are the rules for re-binding?
Asked Answered
P

3

7

[NOTE: I asked this question based on an older version of Rakudo. As explained in the accepted answer, the confusing output was the result of Rakudo bugs, which have now been resolved. I've left the original version of the Q below for historical reference.]

Raku sometimes prohibits re-binding; both of the following lines

sub f($a) { $a := 42 }
my \var = 'foo'; var := 'not-foo';

produce a compile-time error:

===SORRY!=== Error while compiling 
Cannot use bind operator with this left-hand side

However, Raku allows rebinding in many, many situations – including many that came as a large surprise to me. All of the following successfully rebind; every say outputs not-foo.

my Any \a = 'foo';
say a := 'not-foo';
my Any $b := 'foo';
say $b := 'not-foo';
my @c := ('foo', 'foo');
say @c := ('not-foo', 'not-foo');
my @d is List = ('foo', 'foo');
say @d := ('not-foo', 'not-foo');
my %e := (:foo<foo>);
say %e := (:not-foo<not-foo>);

sub fn1(Any \a) { a := 'not-foo';  say a  }
fn1 'foo';
sub fn2(Any $b) { $b := 'not-foo'; say $b }
fn2 'foo';
sub fn3(@c) {  @c := ('not-foo', 'not-foo'); say @c }
fn3 ('foo', 'foo');
sub fn4(+@d) { @d := ('not-foo', 'not-foo'); say @d }
fn4 ('foo', 'foo');
sub fn5(@d is raw) { @d := ('not-foo', 'not-foo'); say @d }
fn5 ('foo', 'foo');

my ($one-foo, $two-foo) := ('foo', 'foo');
$one-foo := 'not-foo';
say $one-foo;

my \foo = 'foo';
say MY::<foo> := 'not-foo';
sub foo-fn { 'foo' }
MY::<&foo-fn> := { 'not-foo' }
say foo-fn;

my $absolutely-foo = 'foo';
sub fn6 { CALLER::<$absolutely-foo> := 'not-foo';}
fn6;
say $absolutely-foo;

Thus, it appears that rebinding is currently allowed to any name, regardless of the sigil or lack of sigil, if either of the following conditions are met:

  1. The name has any explicit type constraint (including Any and the type constraints imposed by the @ or % sigils), or
  2. The rebinding uses a qualified name.

This rebinding currently happens for both declared variables and parameters, and includes parameters that are not rw or copy. It even, as the last example indicates, allows re-bindings in ways that (seem to?) violate lexical scope. (That example was based on a Roast test that's annotated with the comment -- legal?, which suggests that I may at least not be alone in finding this behavior surprising! Though the test re-binds a is dynamic variable – in some ways, the behavior above is even more surprising).

As far as I can tell, the only names that cannot be re-bound using one of these approaches are those declared as constant.

So four questions:

  1. Am I correctly describing the current behavior? [edit: that is, do the two rules I listed above correctly describe current behavior, or does a correct description require other/additional rules?]
  2. Is that behavior correct/intentional/in line with the spec? (Despite the presence of S03-binding, I've found remarkably little on rebinding).
  3. If this behavior is not intentional, what are the rules about rebinding supposed to be?
  4. Is there any way to tell Raku "don't rebind this name to a new value, no-really-I-mean-it"?

(This question supersedes my earlier question, which I asked before I realized how easy it is to re-bind name; I'm closing it in favor of this one. Another related question: Is there a purpose or benefit in prohibiting sigilless variables from rebinding?, which discusses some of the design tradeoffs from the assumption that sigilless variables cannot be re-bound, contrary to several of the examples above.)

Polypary answered 18/9, 2021 at 4:10 Comment(0)
P
0

The inconsistent behavior I asked about in the question was a result of a Rakudo bug – Rakudo was allowing rebinding in some situations where it should not have been. This bug was resolved in Rakudo/Rakudo#4536.

After this resolution, the rules for rebinding are:

  • Sigilless "variables" cannot be rebound (and can't be reassigned, so they aren't really "variables")
  • Variables with a sigil generally can be rebound, subject to the exception below.
  • If a sigiled variable is part of a Signature, then it cannot be rebound unless it is declared to be rebindable via the is copy or is rw traits.
    • This applies to a function's Signature (thus sub f($a) { $a := 42 } is illegal)
    • It also applies to Signatures that are destructured as part of variable declaration with :=. E.g., in my ($var1, $var2) := ('foo', 'bar'), the right-hand side is a Signature and thus $var1 and $var2 cannot be rebound.

Applying these rules means that all of the following rebindings (which were allowed when the question was asked) are now forbidden:

my Any \a = 'foo';
say a := 'not-foo';

sub fn1(Any \a) { a := 'not-foo';  say a  }
fn1 'foo';
sub fn2(Any $b) { $b := 'not-foo'; say $b }
fn2 'foo';
sub fn3(@c) {  @c := ('not-foo', 'not-foo'); say @c }
fn3 ('foo', 'foo');
sub fn4(+@d) { @d := ('not-foo', 'not-foo'); say @d }
fn4 ('foo', 'foo');
sub fn5(@d is raw) { @d := ('not-foo', 'not-foo'); say @d }
fn5 ('foo', 'foo');

my ($one-foo, $two-foo) := ('foo', 'foo');
$one-foo := 'not-foo';
say $one-foo;

Conversely, applying these rules means that some of the other rebindings shown in the question are (correctly) allowed:

my Any $b := 'foo';
say $b := 'not-foo';
my @c := ('foo', 'foo');
say @c := ('not-foo', 'not-foo');
my @d is List = ('foo', 'foo');
say @d := ('not-foo', 'not-foo');
my %e := (:foo<foo>);
say %e := (:not-foo<not-foo>);

Finally, some of the examples shown in the question involved modifying the contents of a (pseudo) package. This allows rebindings that would otherwise be forbidden by the rules above:

my \foo = 'foo';
say MY::<foo> := 'not-foo';
sub foo-fn { 'foo' }
MY::<&foo-fn> := { 'not-foo' }
say foo-fn;

my $absolutely-foo = 'foo';
sub fn6 { CALLER::<$absolutely-foo> := 'not-foo';}
fn6;
say $absolutely-foo;

However, just as with using the Metaobject Protocol to access private methods/attributes, using pseudo packages to break Raku's normal rules should be an extreme last resort and does not enjoy the same stability guarantees as other aspects of Raku.

Polypary answered 10/2, 2022 at 18:29 Comment(2)
"Sigilless "variables" cannot be rebound (and can't be reassigned, so they aren't really "variables")" Are you saying my \foo = my$ = 42; foo = 99; no longer works?Campground
@Campground no, I'm not saying that that no longer works – it still does. I guess I'd describe that as binding a mutable container to foo and then assigning a new value to the container (just like with my \bar = [1, 2]). And foo is always bound to that container; it's never rebound or reassigned, even though the value in that container changes.Polypary
C
4

A decidedly non-authoritative answer. Curiously, like jnthn in your prior Q, it feels natural to answer your questions in reverse order:

Is there any way to tell Raku "don't rebind this name to a new value, no-really-I-mean-it"?

As far as I can tell so far -- purely by testing variations, not by checking roast or the compiler source -- you can and must declare a sigil free symbol, and it must not be one declared with my \symbol ...:

constant foo = 42;
class bar {}
enum baz <a b>;
subset boo;

As far as I can tell, the above declarations permanently bind their symbols to their declared values during compilation of their enclosing compunit, and attempts to modify these symbols, eg MY::<foo> := 99;, are silently ignored.


Note that use of constant doesn't address whether the bound value is immutable:

constant foo = [42];
foo[1] = 99;
say foo; # [42 99]

If you want absolute immutability, an absolutely immutable binding isn't enough. You must make sure the bound object/value is also absolutely immutable.


Note that this declares a sigil'd identifier, so you can modify what it's bound to:

sub f { say 'foo' }
MY::<&f> := { say 'boo' }
f; # boo

If this behavior is not intentional, what are the rules about rebinding supposed to be?

My guess, for what that's worth, is that everything is as it's supposed to be.


A couple weeks ago, in response to you showing that one could rebind a sigilless variable declared with my Int \foo = 42;, I wrote:

Right now what you've shown in this Q smashed my mental model

But that that was then. And the "Right now" hints at something I've learned to cherish in my experience of Raku, still now, 10 years after first seriously getting into it. Each time I experience such a surprise, I remember I would be well advised to remain fundamentally open to changing my mind. And sure enough, as I wrote in an earlier version of this answer:

I've concluded in retrospect that it all makes perfect Rakunian sense to me.

Huh?

  • What happened to smash my mental model? I formed (and, worse, promulgated /o) an incorrect mental model that sigilless variables were necessarily Static Single Assignment. Then I confronted some code that fun da mentally didn't fit that model. Smashing!

  • What happened since then? I concluded that the SSA aspect is only true for sigilless variables declared with constant. And that my "smashing" reaction was due to my having long held the wrong mental model. Any that, while I must now atone for my sin of cooperating with other misinformed Rakoons in promulgating that wrong mental model to innocent folk like yourself, I needed to get over it fast and completely. So then I opened my eyes and realized Raku was just being Rakunian.

  • What do I mean by "Rakunian"? I think Larry Wall gets the late, great John Shutt's point about Irregularity in language:

    Irregularity is a natural consequence of the impedance mismatch between the formal structure of language and the sapient semantics communicated through it ... large parts of a language, relatively far from its semantic core, may be tolerably regular, but the closer things get to its semantic core, the more often they call for variant structure. It may even be advantageous for elements near the core to be just slightly out of tune with each other, so they create (to use another physics metaphor) a complex interference pattern that can be exploited to slip sapient-semantic notions through the formal structure.

    (Even if Larry does not see things as John did, I do, and think it's one of the principles underlying Raku's semantic core. Well, not Raku's semantic core in the sense of the underlying single semantic model. But here we're focusing on Raku's current standard surface syntactic dialect, so I'm talking about "semantic core" in that context.)

    I would say that declaring and varying (or not) the value of identifiers is close to the semantic core of Raku's standard dialect, and that Raku's related variants are slightly out of tune with each other in useful ways. Specifically, in the context of declaring sigilless identifier variables:

    • = always binds rather than assigns.

    • constant foo = ...; Immutably binds to a value at compile time. (I do wonder at the huffman coding. I've long pondered new declarators a, an, and the, where a and an are aliases for my constant and the is an alias for our constant, which is what a constant means if written without a scope declarator prefix.)

    • my \foo = ...; Binds at run-time; may be rebound by using := with a MY reference.

    • my Type \foo = ...; Like my \foo...; but may also be rebound by using :=.


Is that behavior correct/intentional/in line with the spec? (Despite the presence of S03-binding, I've found remarkably little on rebinding).

The official definition of "spec" is roast (repository of all specification tests).

Part of officially releasing a version of the Rakudo compiler is to ensure it has passed roast.

So if you've compiled and run code with an officially released Rakudo, the behavior is officially per spec.


Am I correctly describing the current behavior?

You've provided code, which compiles and runs, so by that definition, which is the only sane one imo, that code indeed correctly describes the current behavior, where "current" implies the version of the compiler being used (which in turn will have been tested against some particular version of roast, which version should iirc have been git tagged to correspond to a particular official revision of the Raku language).

Campground answered 18/9, 2021 at 19:29 Comment(8)
I'm not quite sure how serious/tongue in cheek you're being when you say "if you've compiled and run code with an officially released Rakudo, the behavior is officially per spec". That is, of course, true in a certain narrow and literal sense – but it's also extremely common to discover a bug in Roast at the same time we discover a bug in Rakudo (and to fix both with coordinated PRs). And a bug in Roast is a time when the spec wasn't specifying what it meant to, and thus where behavior that isn't in accord with the spec still passes. And I think that's what has happened here.Polypary
You mentioned that "I've concluded in retrospect that [the rebinding behavior] all makes perfect Rakunian sense to me". Could you expand on that a bit? Unless I'm missing something, your answer doesn't explain why this behavior makes perfect Rakunian sense, and it isn't making sense of any sort in my head! :)Polypary
I was being fully serious about roast being the official spec. "extremely common to discover a bug in Roast". Do you mean an omission or something that's just wrong and which is changed in a PR rather than adding new tests?Campground
"Do you mean [something] which is changed in a PR [or] adding new tests?" I think both are pretty common, but I was mostly thinking of adding new tests. Looking at the Roast commit history, the most recent commit adds a test related to callsame. And I wonder if tests about re-binding similarly need to be added. Right now, allowing rebinding (like Rakudo) successfully passes Roast. From reading the tests, I believe that prohibiting (most) rebinding would also pass, so I'd think that Roast could (should?) specify one way or the other.Polypary
Changing tests breaks backwards compatibility relative to the official language. That's a huge deal. Adding new tests also changes the official language and is still a big deal. We can deprecate, but even just adding a roast test, let alone changing one, should be viewed in principle as a decades long backwards compatibility commitment. My general view is we are best off leaving things unspecified (unroasted) unless there's a really compelling case to nail them down plus a very high quality intense review process to ensure they really reflect what we want in principle and detail.Campground
"rebinding (like Rakudo) successfully passes Roast." I've edited my answer to cover what I meant by "Rakunian". In short, I'm currently pro current Rakudo behavior as you've shown and as far as I've explored it in addition. That doesn't mean I'm pro roast nailing down the current behaviour as spec.Campground
"Changing tests breaks backwards compatibility relative to the official language. That's a huge deal." In theory, that's 100% correct. In practice, it's pretty common for someone to notice that a test was written in such a way that it didn't test what it meant to, and to fix that discrepancy (I'm not arguing either way, just describing the practice I've noticed). For (re)binding, I think the question is very different when we're talking about parameters – and I'd rather see that nailed down. But SO comments probably aren't the place to figure that out.Polypary
(I replied to your comment in the chat.) WTF? Now it's deleted the chat. :( OK, recovered it from my history: chat.stackoverflow.com/rooms/237343/… Aiui it'll be deleted in a few days, but please read it while you can. Or see gist.github.com/raiph/faf0bfbbf593c8bc1b1f3385d2f815deCampground
B
3

The name has any explicit type constraint (including Any and the type constraints imposed by the @ or % sigils), or

I don't think the type constraint should be relevant here. Every container in Raku has an implicit or explicit type constraint (even if it's Mu). Making things rebindable by putting an Any in front of it reeks of a bug.

Instead, I think the rule is that you can rebind a name unless the compiler knows that it's read-only.

Off the top of my hat, the following cases are known to be ready only:

  • block/routine parameters
  • lexical subs (ie &f after sub f ...)
  • any sigilless name (my \x = ..., class A { ... }, constant a = ...)
Bathy answered 23/9, 2021 at 5:51 Comment(5)
Right now, sub f($a) { } and sub g(Mu $b) { } both have readonly parameters (as shown by the .readonly method on the parameter object), but $b is rebindable while $a is not. Based on your edit, it sounds like you think there's a Rakudo bug and that neither $a nor $b should be rebindable. Is that correct?Polypary
@Polypary correct, I consider that a bug.Bathy
"Right now, sub f($a) { } and sub g(Mu $b) { } both have readonly parameters (as shown by the .readonly method on the parameter object)" The word "readonly" is talking about a radically different concept from binding, namely assignment. (Someone might argue the distinction between assignment and binding is unhelpful, but that ship sailed 20 years ago.) If one chooses to interpret "readonly" in an English sense, then presumably one needs to consider change in a general sense, including eg constant a = []; a = 42; say a; # [42]or sub foo(\a = []) { a = 42 }; say foo; # [42].Campground
"you can rebind a name unless the compiler knows that it's read-only." What does "read-only" mean? In constant a = [] is a "read-only"? Is $a "read-only" in sub foo ($a) {}? What about @a in sub foo (@a) {}? There's a related ambiguity problem about the word "bug". I think we all agree there must be one or more "bugs" here, where "bug" means something's surprising. But is that due to our current mental models, or undisciplined use of "read-only" in discussion, method names, error messages, and doc, or one or more problems in Raku behaviour, or some combination, or something else?Campground
Once I got over the shock I experienced due to having the wrong mental model about my \foo...;, I concluded that "it all makes perfect Rakunian sense to me", to wit: should identifiers called "variables" be variable? To help ease newbies into understanding this, Raku makes sub foo ($a) { $a = 42 } an error (at run-time when foo is called), and sub foo($a) { $a := 42 } an error (at compile-time). Similarly, after class a {}, a, which isn't called a "variable", can't be redeclared. I consider those helpful newbie guides on the way to considering and learning what "variable" means.Campground
P
0

The inconsistent behavior I asked about in the question was a result of a Rakudo bug – Rakudo was allowing rebinding in some situations where it should not have been. This bug was resolved in Rakudo/Rakudo#4536.

After this resolution, the rules for rebinding are:

  • Sigilless "variables" cannot be rebound (and can't be reassigned, so they aren't really "variables")
  • Variables with a sigil generally can be rebound, subject to the exception below.
  • If a sigiled variable is part of a Signature, then it cannot be rebound unless it is declared to be rebindable via the is copy or is rw traits.
    • This applies to a function's Signature (thus sub f($a) { $a := 42 } is illegal)
    • It also applies to Signatures that are destructured as part of variable declaration with :=. E.g., in my ($var1, $var2) := ('foo', 'bar'), the right-hand side is a Signature and thus $var1 and $var2 cannot be rebound.

Applying these rules means that all of the following rebindings (which were allowed when the question was asked) are now forbidden:

my Any \a = 'foo';
say a := 'not-foo';

sub fn1(Any \a) { a := 'not-foo';  say a  }
fn1 'foo';
sub fn2(Any $b) { $b := 'not-foo'; say $b }
fn2 'foo';
sub fn3(@c) {  @c := ('not-foo', 'not-foo'); say @c }
fn3 ('foo', 'foo');
sub fn4(+@d) { @d := ('not-foo', 'not-foo'); say @d }
fn4 ('foo', 'foo');
sub fn5(@d is raw) { @d := ('not-foo', 'not-foo'); say @d }
fn5 ('foo', 'foo');

my ($one-foo, $two-foo) := ('foo', 'foo');
$one-foo := 'not-foo';
say $one-foo;

Conversely, applying these rules means that some of the other rebindings shown in the question are (correctly) allowed:

my Any $b := 'foo';
say $b := 'not-foo';
my @c := ('foo', 'foo');
say @c := ('not-foo', 'not-foo');
my @d is List = ('foo', 'foo');
say @d := ('not-foo', 'not-foo');
my %e := (:foo<foo>);
say %e := (:not-foo<not-foo>);

Finally, some of the examples shown in the question involved modifying the contents of a (pseudo) package. This allows rebindings that would otherwise be forbidden by the rules above:

my \foo = 'foo';
say MY::<foo> := 'not-foo';
sub foo-fn { 'foo' }
MY::<&foo-fn> := { 'not-foo' }
say foo-fn;

my $absolutely-foo = 'foo';
sub fn6 { CALLER::<$absolutely-foo> := 'not-foo';}
fn6;
say $absolutely-foo;

However, just as with using the Metaobject Protocol to access private methods/attributes, using pseudo packages to break Raku's normal rules should be an extreme last resort and does not enjoy the same stability guarantees as other aspects of Raku.

Polypary answered 10/2, 2022 at 18:29 Comment(2)
"Sigilless "variables" cannot be rebound (and can't be reassigned, so they aren't really "variables")" Are you saying my \foo = my$ = 42; foo = 99; no longer works?Campground
@Campground no, I'm not saying that that no longer works – it still does. I guess I'd describe that as binding a mutable container to foo and then assigning a new value to the container (just like with my \bar = [1, 2]). And foo is always bound to that container; it's never rebound or reassigned, even though the value in that container changes.Polypary

© 2022 - 2024 — McMap. All rights reserved.