13

As I understand, a named argument to a method goes to %_ if not found in the signature (not sure why!). To detect this, I do

die "Extra args passed" if %_;

for the methods I remember to do and that are worth doing. Is there a way to automate this for example with some decorator kind of thing? Or I miss and %_ is actually a very useful thing in object oriented Raku programming?

  • 4
    Rationale is **Interface Consistency**: [Apocalypse #12](https://raku.org/archive/doc/design/apo/A12.html#Interface%20Consistency), [Synopsis #12](https://design.raku.org/S12.html#Interface_Consistency). – raiph Feb 10 '22 at 22:19
  • 2
    I can’t answer your actual question. But I can offer that `%_` is probably spelled like that to rhyme with Perl’s default argument, `$_` (aka the *topic*). The `%` part is simply because it’s a hash (and must be - for possible extra names arguments). – Dan Bron Feb 10 '22 at 22:28

3 Answers3

8

Is there a way to automate this for example with some decorator kind of thing?

Because this is Raku where there's always More Than One Way To Do It, there are in fact multiple ways to fairly easily prevent the automatic *%_ method parameter. I'll show you two (and explain how they work) and then try to convince you that, in fact "%_ is actually a very useful thing in object oriented Raku programming".

The answer: rejecting unexpected named parameters (the *%_)

Consider this code:

class C0           { method m        { say 'm called' } }
class C1           { method m(*% ()) { say 'm called' } }
class C2 is hidden { method m        { say 'm called' } }

my $c = C0.new;
$c.m(:unexpected); # OUTPUT: «m called»

C0 is a standard class and, as you've already noted, it happily accepts unexpected named arguments. But if we try the same thing with C1, we'd get this error:

Unexpected named argument 'unexpected' passed in sub-signature

And, if we tried it with C2, we'd get this very similar one:

Unexpected named argument 'unexpected' passed

The biggest difference for our current purposes is that C1 just disallowed unexpected named arguments for m – we could create other C1 methods that do accept unexpected named arguments. Conversely, with C2, we prevent any of the classes methods from accepting unexpected named arguments (and do a bit more, but more on that soon).

How do these each work?

C1

C1 prevents m from accepting any unexpected arguments by using named slurpy parameter and then destructuring that parameter into a sub-signature – in this case, a sub-signature that happens to consist of an empty list.

Breaking that down a bit, consider this code:

sub f(:$group-name!, *@ids (Int, Int?, Int?, Int?) {}

That signature is saying "I have to have a :group-name parameter and some number of @ids. And those @ids must have at least one Int, and could have up to four Ints – but no more than four, and no non-Ints)". So we can call f with :group-name<foo>, 1. Or with :group-name<foo>, 1, 42, 55. But if we tried to call it with :group-name<foo>, 1, 42, 55, 9, 9, 9, we'd get the error: Too many positionals passed to 'f'; expected 1 to 4 arguments but got 6 in sub-signature of parameter @ids.

So what's the signature in C1's m method saying. Well, the *% part says "I'll take a list of absolutely any named argument at all" and then the () part adds "so long as that list doesn't have anything in it!".

And that gets us exactly what you were looking for: a method that rejects unexpected named arguments (and expected named arguments don't pose any problem at all – you'd declare those before the *%, and they'd be handled before ever reaching the sub-signature.

C2

C2 is both simpler and, at the same time, a bit more overkill. C2 uses the hidden trait which stops C2 from participating in the redistpatch process – and, as a side effect, prevents C2's methods from being given an automatic *%_ parameter.

Explaining the full scope of Raku's (re) dispatch process is beyond the scope of this answer but I'll just say that, depending on what you want, that may be exactly what you're looking for or it may be an unwanted consequence.

But you should (maybe) ignore those answers

Let's go back to how you ended your ques ion. You asked whether you missed something that makes *%_ "actually a very useful thing in object oriented Raku programming". I'd argue that, yes, the automatic addition of *%_ is in fact very helpful.

There are a bunch of different ways to come at why that's the case but, since you mentioned object-oriented Raku, I'll come at it from that angle (I personally write more functional-programming Raku, but the both have there place).

So from an OOP perspective, the addition of *%_ makes it much easier to write Raku methods that follow the Liskov_substitution_principle (aka, the L in SOLID OOP).

I'm not sure how familiar you are with that principle, so I'll say just a few words about it (apologizes if I'm belaboring the obvious). The LSP says that "Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program".

That can be a bit abstract, so here's a Raku example that's … ok, still kind of abstract, but bear with me.

So, consider the Raku Stash class. It doesn't matter what it does right now; I'm bringing it up because of its type graph:

Type graph for Stash

So a Stash isa Hash and a Hash isa Map and a Map isa Cool and a Cool isa Mu. So that means that any function that's type-constrained with any of Stash, Hash, Map, Cool, or Mu will accept a Stash – and it's up to Stash to make sure that it can be validly passed to any such function without breaking anything. Of course, for the most part a Stash handles this through inheritance: most of the methods that you can call on a Stash aren't actually defined on the Stash – they're inherited and thus are automatically compatible with the superclass method (because they are the superclass method).

But, for methods that Stash chooses to overwrite, it's part of Stash's job to make sure that it doesn't break any superclasses expected behavior. And the same goes for, say, Map – but even more so. If there's a method on Cool that Map overrides, Map needs to think not only about Map's callers but also about Stash's callers: after all, Stash might not override that method. So Map needs to make sure that it can handle anything Cool wants/needs to handle – and that's something that, in theory, could happen at any time. (In practice, of course, Cool is pretty stable and both it and Map are part of Rakudo. But the same thing would apply for 3rd-party classes, where APIs change more rapidly and you may not have the ability to coordinate changes.)

In particular, "handling anything Cool can handle" means being able to be called with the arguments callers of Cool passed without throwing type errors – even when those arguments might change.

And that's where *%_ comes in. Unless one of them opts out, every member of the method chain accepts any named parameter. As a result, Cool can freely add new named parameters, and Map/Stash's callers (who, after all, aren't supposed to care or need to care whether which object they're calling) can pass in the corresponding named arguments. And, even if those method calls get resolved by a method that's never heard of the new argument, absolutely nothing will break.

Of course, if it just gets eaten by *%_, the new argument won't actually do anything, which (depending on the class/method) might not be ideal. But that's where re-dispatching comes back in and lets the intervening class act on the method and still pass it on to a superclass. But, again, that's getting out of scope.


So, after a long answer to a short question, the bottom line is that you can prevent a method from accepting unexpected named arguments. But, depending on your approach to programming/Raku, there are some very good reasons to consider not doing so.

codesections
  • 8,900
  • 16
  • 50
  • 3
    (oh, and all of the above was basically just an elaboration of the two links @raiph posted in the comments to the question. So check those out if you want more more info.) – codesections Feb 12 '22 at 02:12
  • thanks do you know why a method like `CALL-ME` won't issue "Unexpected name argument $extra passed" but fail with "Cannot resolve caller..."? Both `is hidden` and `*% ()` make it fail and that's fine but they fail differently (not from each other but from "Unexpected..." message). [here is a MRE](https://glot.io/snippets/g6za46ytms). Looked at the apocalypse and synopsis links but couldn't find answer. FWIW also confirmed its not due to `multi` because tried with a "normal" multi method and it errs "Unexpected arg...". –  Feb 12 '22 at 07:36
  • @doesitmatter because it's a multi method. Without the `multi`, that code produces the error with "Unexpected..." – codesections Feb 12 '22 at 07:48
  • oh and here I'm saying "confirmed not due to multi" etc. what do I know sorry for the waste of time there –  Feb 12 '22 at 08:21
5

Is there a way to automate this for example with some decorator kind of thing?

I'm not aware of a way of doing that currently.

I once developed a method trait to remove the implicit *%_ from the signature of a method. In the hopes I could simplify dispatching on multi methods that take many different (combinations) of named arguments.

It did not end well. I don't recall exactly why anymore, but I decided to postpone trying to do that until after the RakuAST branch has landed.

Elizabeth Mattijsen
  • 25,654
  • 3
  • 75
  • 105
5

Or I miss and %_ is actually a very useful thing in object oriented Raku programming?

I think, it is useful for something like following:

class F {
    has $.x;
    has $.y;
    multi method w-sum (:$x,:$y) { $x*$!x + $y*$!y };
}

class F3 is F {
        has $.z;
        method w-sum (:$z) { $z*$!z + callsame }
        # vs method w-sum (:$x, :$y, :$z) { $z*$!z + callwith( :$x, :$y ) }
}

my F3 $point3D .= new: :2x, :3y, :4z, ;

$point3D.w-sum: :1x, :2y, :3z, ;

Explanation:

I use %_ twice:

  1. I call method w-sum from F3 with x y z, so %_={x => 1, y => 2}
  2. I call method w-sum from F by callsame with x y z, so %_={z => 3}

try debug version:

class F {
    has $.x;
    has $.y;
    multi method w-sum(:$x,:$y) { say 'from F: ', %_; $x*$!x + $y*$!y }
}

class F3 is F {
        has $.z;
        method w-sum (:$z) { say 'from F3: ', %_; $z*$!z + callsame }
        # vs method w-sum (:$x, :$y, :$z) { $z*$!z + callwith( :$x, :$y) }
}

my F3 $point3D .= new: :2x, :3y, :4z, ;

say $point3D.w-sum: :1x, :2y, :3z, ;
from F3: {x => 1, y => 2}
from F: {z => 3}
20
wamba
  • 3,883
  • 15
  • 21