Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Correctly passing a routine into an object variable

Tags:

class

raku

I need to pass some code from an external program into a class.

In a generic module I have (for simplicity reduced to silliness)

class A {
   has &.hl;
   submethod BUILD( :&!hl ) {}
}

Elsewhere in a program, I have

use A;
my &hl = -> $st { 
   my $p = shell "hl $st", :in,:out;
   $p.out.slurp
};
my $statement = 'my $raku-variable = "Helloooo";'
my $first = &hl($statement);
my A $a .= new(:&hl);
my $second = $a.hl( $statement );

$first will be processed and will contain the expected results.

At $second, I will get a runtime error

Too many positionals passed; expected 1 argument but got 2

Clearly the routine in the class is being provided both the invocant and the parameter $s.

Rewriting the class to provide a custom accessor:

class A {
   has &!hl;
   submethod BUILD( :&!hl ) {}
   method process-it( Str $s --> Str ) { &!hl( $s ) }
}
# elsewhere
my $second = $a.process-it( $statement );

Then both $first and $second run without error and will contain the same results.

When hl is accessed inside the class, no invocant is added, but if it is not declared as &.hl then it is not visible outside the class.

My question is therefore: Is there another way to create a public object code variable that does not automagically add the invocant as a variable to the code? Other than creating a separate accessor method.

Here is short bash script hl for illustration

#! /bin/bash
echo '<div class="statement">'$1'</div>'

Here is a full Raku program

use v6.c;

class A {
    has &!highlighter; # also tried with has &highlighter
    submethod BUILD( :&!highlighter ) {}
    method process-it( Str $s --> Str ) {
       &!highlighter( $s )
    }
}

sub MAIN() {
    my @strings = 'my $v = "Hello World";', 'my $w = $v.raku;';
    my $proc;
    my $proc-supply;
    my &highlighter = -> $s {
        my $p = shell "./hl '$s' ", :in,:out;
        $p.out.slurp
    }

    for @strings {
        say .&highlighter
    }
    my A $a .= new(:&highlighter);
    for @strings { say $a.highlighter($_) }
    # own accessor
    for @strings { say $a.process-it($_) }
}
like image 332
Richard Hainsworth Avatar asked Jan 04 '19 05:01

Richard Hainsworth


3 Answers

has $!hl declares a private attribute. has $.hl declares a public attribute.

By public I mean it creates a method of the same name that returns it, and it adds it to the BUILD/gist/perl/Capture [sub]methods.

class A {
   has &.hl;
}

This is effectively the same as:

class A {
  has &!hl;

  submethod BUILD ( :&!hl ){}

  method hl (){ &!hl } # return the code object

  method perl (){
    "A.new(hl => $!hl.perl())"
  }
  method gist (){ self.perl }

  method Capture () {
    \( :&!hl )
  }
}

So when you call A.hl it returns the code object that is stored in &!hl.


You can deal with this in a few ways.

  1. Just call it “twice”.

    $a.hl()(42)
    $a.hl().(42)
    $a.hl.(42)
    
  2. Have an additional method that uses it.

    method call-it ( |C ){
      &!hl( |C )
    }
    
    $a.call-it( 42 )
    my &hl = $a.hl;
    

    Note that I used |C to avoid dealing with signatures entirely.
    It might make sense for you to have a signature and deal with it like you have.

  3. Override the automatically generated method by adding it yourself.

    method hl ( |C ){
      &!hl( |C )
    }
    

    $a.hl( 42 )
    

    By overriding it, all of the other changes that making it a public attribute are still done for you.
    So there will be no need to create a BUILD submethod.


When you override it, that means that is rw has no effect. It also means that there is no way for outside code to retrieve the code object itself.

There are ways to deal with that if you need to.
If you don't ever need to return the value in &!hl then just leave it like it is above.

  1. If the code object is never called with zero positional arguments.

    multi method hl (){ &!hl }
    multi method hl ( |C ){
      &!hl( |C )
    }
    
    $a.hl;        # returns the value in $!hl
    $a.hl();      # returns the value in $!hl
    
    $a.hl( 42 );  # calls &!hl(42)
    

    Note that there is no way for a method to differentiate between .hl and .hl().

  2. You could also use a named argument.

    multi method hl ( :code($)! ){ &!hl }
    multi method hl ( |C ){
      &hl( |C )
    }
    
    $a.hl(:code); # returns the value in &!hl
    
    $a.hl;        # calls &!hl()
    $a.hl();      # calls &!hl()
    $a.hl( 42 );  # calls &!hl(42)
    
  3. You could do nothing to make it easier to get the code object, and just have them use subsignature parsing to get the attribute.
    (This is why the Capture method gets created for you)

    class A {
      has &.hl;
    
      method hl ( |C ){
        &!hl( |C )
      }
    }
    
    sub get-hl ( A $ ( :&hl ) ){ &hl }
    
    my &hl = get-hl($a);
    
    
    my &hl = -> A $ ( :&hl ){ &hl }( $a );
    
    my &hl = $a.Capture{'hl'};
    
like image 115
Brad Gilbert Avatar answered Sep 19 '22 11:09

Brad Gilbert


TL;DR There is no way to directly access an attribute outside the source code of the class in which it is declared. The only way to provide access is via a separate public accessor method. This answer hopefully clears up confusion about this. Other answers lay out your options.

Why you get a Too many positionals passed; error message

The code has &!hl; declares an attribute, &!hl.

The code has &.hl; does the same but also generates a method, .hl that's a public accessor to the attribute with the same name. Like all such generated accessors, it expects a single argument, the invocant, and no others.

my $second = $a.hl( $statement )

This code calls the method hl. Raku passes the value on the left of the dot ($a) as a first argument -- the invocant. But you've also added a $statement argument. So it passes that too.

Hence the error message:

Too many positionals passed; expected 1 argument but got 2

When hl is accessed inside the class, no invocant is added

It's not because it's accessed inside the class. It's because you don't call it as a method:

method process-it( Str $s --> Str ) { &!hl( $s ) }

The &!hl( $s ) code is a sub style call of the routine held in the &!hl attribute. It gets one argument, $s.

Is there another way to create a public object code variable that does not automagically add the invocant as a variable to the code?

The problem is not that Raku is automagically adding an invocant.

Other than creating a separate accessor method.

There is no way to directly access an attribute outside the source code of the class in which it is declared. The only way to provide access is via a separate public accessor method. This answer hopefully clears up confusion about this. Other answers lay out your options.

like image 25
raiph Avatar answered Sep 19 '22 11:09

raiph


The problem is that the accessor returns the attribute, that happens to be a Callable. Only then do you want to call the return value of the accessor with parameters. This is essentially what you're doing by creating your own accessor.

You don't have to actually create your own accessor. Just add a extra parentheses (indicating you're calling the accessor without any extra arguments), and then the parentheses for the values you actually want to pass:

class A {
    has &.a = *.say;  # quick way to make a Callable: { .say }
}
A.new.a()(42);        # 42

Or if you don't like parentheses so much, consider the method invocation syntax, as timotimo pointed out:

A.new.a.(42);         # 42
like image 43
Elizabeth Mattijsen Avatar answered Sep 22 '22 11:09

Elizabeth Mattijsen