Search code examples
immutabilityrakulexical-scoperakudovariable-binding

What are the rules for re-binding?


[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.)


Solution

  • 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.