8

I've run into something I don't understand with binding a hash in an EVAL. Binding the hash outside of EVAL works as expected. An unbound hash in an EVAL works as expected. But binding a hash inside of an EVAL doesn't work as I would expect. (My expectations may be wrong.) Here's the code:

This works:

#!/usr/bin/env raku

class Hash::Test does Associative {
  has %.hash;

  multi method STORE(@pairs) {
    for @pairs -> $pair {
      self.STORE: $pair
    }
  }

  multi method STORE(Pair $pair) {
    %!hash{$pair.key} = $pair.value;
  }
}

no strict;
%hash-test := Hash::Test.new;
%hash-test = foo => 'bar', baz => 'quux';
say %hash-test;

Output:

$ ./hash-binding-works.raku 
Hash::Test.new(hash => {:baz("quux"), :foo("bar")})

And this works:

#!/usr/bin/env raku

class Foo {
  use MONKEY-SEE-NO-EVAL;

  method eval(Str $code) {
    EVAL $code;
  }
}

my $code = q:to/END/;
  no strict;
  %hash = foo => 'bar', baz => 'quux';
  END

Foo.eval: $code;
say %Foo::hash;

Output:

$ ./hash-EVAL-works.raku 
{baz => quux, foo => bar}

But this does not work:

#!/usr/bin/env raku

class Hash::Test does Associative {
  has %.hash;

  multi method STORE(@pairs) {
    for @pairs -> $pair {
      self.STORE: $pair
    }
  }

  multi method STORE(Pair $pair) {
    %!hash{$pair.key} = $pair.value;
  }
}

class Foo {
  use MONKEY-SEE-NO-EVAL;

  method eval(Str $code) {
    EVAL $code;
  }
}

my $code = q:to/END/;
  no strict;
  %hash-test := Hash::Test.new;
  %hash-test = foo => 'bar', baz => 'quux';
  say %hash-test;
  END

no strict;
Foo.eval: $code;
say %Foo::hash-test;

Output:

$ ./hash-EVAL-does-not-work.raku 
Hash::Test.new(hash => {:baz("quux"), :foo("bar")})
{}

Hash::Test is not the real class I am using, but is what I golfed it down to. Can anyone explain what's going on here? Thanks!

JustThisGuy
  • 1,109
  • 5
  • 10
  • Why are you using `no strict`? What happens if you remove it? – Elizabeth Mattijsen Feb 18 '21 at 21:37
  • @ElizabethMattijsen For me, if I run the last code but without the `no strict;` I get "Variable '%hash-test' is not declared". – raiph Feb 19 '21 at 00:09
  • @raiph, thanks for catching that. Yes, that was copy/paste fail. I've corrected it. – JustThisGuy Feb 19 '21 at 01:07
  • @ElizabethMattijsen, What @raiph said is correct. I'm using it to stay consistent with the last block of code that doesn't work. Since EVAL can't create a lexical in the surrounding scope, I can't use `my`. – JustThisGuy Feb 19 '21 at 01:10

1 Answers1

6

TL;DR no strict; autodeclares package variables via an implicit our declarator. our declares package variables via an implicit my lexical variable declarator that binds to an implicit package symbol with the same name. Your code breaks that binding, and that breaks your code. To fix it, say the same thing another way.

Solution

no strict; doesn't help so we get rid of that. Same goes for our. Instead we declare a my lexical variable, do everything we need/can do with that, and then, at the end of the code that will be EVALd, create a package variable and bind it to the value stored in the lexical.

my $code = q:to/END/;
  my %hash is Hash::Test; 
  %hash = foo => 'bar', baz => 'quux';
  OUR::<%hash-test> := %hash;
  END

Foo.eval: $code;
say %Foo::hash-test; # Hash::Test.new(hash => {:baz("quux"), :foo("bar")})

Explanation of the surprises

Variables declared without an explicit declarator under no strict; implicitly declare our variables:

no strict;
%hash-test = :a;
say MY::<%hash-test>;  # {a => True}
say OUR::<%hash-test>; # {a => True}

In other words, the net effect of just the first two lines above are equivalent to:

our %hash-test = :a;

In turn, our variables implicitly declare my variables and follow the logic shown in this SO. So this code:

no script;
%hash-test := ...;

Is doing this:

(my %hash-test := $?PACKAGE.WHO<%hash-test>) := ...;

which creates a lexical %hash-test symbol, and a package %hash-test symbol, and binds them -- which binding is essential to proper functioning of our variables -- and then immediately breaks that essential binding.

Thereafter, whatever the rest of your code does, it does it to just the lexical %hash-test version of the variable, leaving the package symbol version of %hash-test high and dry so that it just later autovivifies to an empty hash.


As jnthn says in a comment below the SO I linked at the start:

We certainly could warn that binding to an our variable is pointless

But there is currently no warning.


As you explain in your comment below, when you tried to use %hash-test is Hash::Test the compiler mysteriously decides that you've written "two terms in a row". As I explain in my comment, that's due to the above chicanery when you declare an our variable using the usual syntax (or implicitly by using no strict;).


To fix all of the above, forget no strict;, forget use of our, and instead:

  • Use a lexical to do the work setting up the value;

  • Wrap up by creating the package symbol with OUR::<%hash-test> and binding that to the value of the lexical.

raiph
  • 31,607
  • 3
  • 62
  • 111
  • 1
    In the last example I was originally trying to do this: `%hash-test is Hash::Test = foo => 'bar', baz => 'quux';` but get `Two terms in a row at /development/raku/VTS-Template/EVAL_0:2 ------> %hash-test⏏ is Hash::Test = foo => 'bar', baz => 'q expecting any of: infix infix stopper statement end statement modifier statement modifier loop` am I correct in assuming that `is` is binding under the hood? – JustThisGuy Feb 19 '21 at 01:25
  • 1
    An `is` is a trait routine, and different traits do different things. I don't know what `is` on a variable, when followed by a class or role, does behind the scenes, but I do know it does it to an object of type [`Variable`](https://docs.raku.org/type/Variable) at compile time, which is not the same thing as one normally thinks of as a variable. But the error you're getting is unrelated to that. The error is because of what I show in my answer. It's like you've written `(my %hash-test := $?PACKAGE.WHO<%hash-test>) is Hash::Test = ...` and that is two terms in a row. – raiph Feb 19 '21 at 01:36