6

Suppose I have the following code:

my constant @suits = <Clubs Hearts Spades Diamonds>;
my constant @values = 2..14;

class Card {
    has $.suit;
    has $.value;

    # order is mnemonic of "$value of $suit", i.e. "3 of Clubs"
    multi method new($value, $suit) {
        return self.bless(:$suit, :$value);
    }
}

It defines some suits and some values and what it means to be a card.

Now, to build a deck, I essentially need to take the cross product of the suits and the values and apply that to the constructor.

The naiive approach to do this, would of course be to just iterate with a loop:

my @deck = gather for @values X @suits -> ($v, $c) {
    take Card.new($v, $c);
}

But this is Raku, we have a cross function that can take a function as an optional argument!, so of course I'm gonna do that!

my @deck = cross(@values, @suits, :with(Card.new));
# Unexpected named argument 'with' passed
# in block <unit> at .\example.raku line 36

... wait no. What about this?

my @deck = cross(@values, @suits):with(Card.new);
# Unexpected named argument 'with' passed
# in block <unit> at .\example.raku line 36

Still nothing. Reference maybe?

my @deck = cross(@values, @suits):with(&Card.new);
# ===SORRY!=== Error while compiling D:\Code\Raku/.\example.raku
# Illegally post-declared type:
#    Card used at line 36

I read somewhere I can turn a function into an infix operator with []

my @deck = cross(@values, @suits):with([Card.new]);
# Unexpected named argument 'with' passed
# in block <unit> at .\example.raku line 36

That also doesn't work.

If classes are supposed to just be modules, shouldn't I then be able to pass a function reference?

Also why is it saying 'with' is that's unexpected? If I'm intuiting this right, what it's actually complaining about is the type of the input, rather than the named argument.

Electric Coffee
  • 11,733
  • 9
  • 70
  • 131
  • 1
    Was kind of answered already [here](https://stackoverflow.com/questions/47994213/pointer-to-constructor-to-a-class-in-perl6). It also mentions more general methods to obtain a pointer to a method, which might also help you in this case. – jjmerelo Jun 05 '20 at 15:18

2 Answers2

8

The error message is indeed confusing.

The :with parameter expects a Callable. Card.new is not a Callable. If you write it as :with( { Card.new($^number, $^suit) } ), it appears to work.

Note that I did not use $^value, $^suit, because they order differently alphabetically, so would produce the values in the wrong order. See The ^ twigil for more information on that syntax.

The error is LTA, this makes it a little bit better.

To get back to your question: you can find the code object that corresponds to Card.new with ^find_method. However, that will not work, as Card.new actually expects 3 arguments: the invocant (aka self), $value and $suit. Whereas the cross function will only pass the value and the suit.

Electric Coffee
  • 11,733
  • 9
  • 70
  • 131
Elizabeth Mattijsen
  • 25,654
  • 3
  • 75
  • 105
  • sorry, that `Cards` thing is a typo, I didn't copypaste the code and slipped it in by accident. It's meant to say `Card` – Electric Coffee Jun 04 '20 at 21:18
  • Regarding `:with( { Card.new($^number, $^suit) } )`, that was basically also the solution I found after enough experimenting. I do find it a bit unintuitive that I can't make `new` a callable the way I would any other routine – Electric Coffee Jun 04 '20 at 21:22
  • @raiph yeah but notice what the error actually says `Card`, because unlike the code-snippet, I **DID** actually copypaste the error message. – Electric Coffee Jun 04 '20 at 21:24
  • I see that being a problem without an obvious solution besides redesigning the compiler to be multi-pass – Electric Coffee Jun 04 '20 at 21:43
  • @ElectricCoffee I just realized the error message isn't because you'd written code mentioning `Card` before it was declared. Instead it's [this](https://github.com/rakudo/rakudo/issues/1965). And given that it's a compile-time error, someone could presumably turn it into valid syntax, at least initially in userland (slang, macro, or slacro), and then one day, if found both popular and without being a trap in some way, then shipped in the standard distro, or perhaps even built in if that was considered wise. – raiph Jun 04 '20 at 22:19
  • It certainly seems like it would more naturally fit into what people expect would work. I mean **I** did. – Electric Coffee Jun 04 '20 at 22:22
  • 1
    A quickie helper -- ugly, unintuitive, error prone, and worse, but illustrating the underlying approach if not the desirable sugar and, perhaps, robustness -- would be `sub prefix:<&> ([$invocant, $method]) { $invocant.^find_method($method).assuming: $invocant }; my @deck = cross @values, @suits, :with(& [Card, 'new']);`. But I imagine a slang could make a nice job of it sugar-wise -- as the ideal, `&Card.new` -- and in all other aspects. – raiph Jun 04 '20 at 22:33
  • "I see that being a problem without an obvious solution besides redesigning the compiler to be multi-pass" The *compiler* is multipass. For example, as I noted, it lets one call a sub and then define it later. But for *parsers* written to participate well *in the Raku language braid*, their *primary nature* will *always be* single pass. It's a key part of what makes Raku Rules **Godfood**. Anyhow, once it became clear the "Illegally post-declared type" error you got wasn't due to you wanting to refer to a type before it was declared, my point about the compiler fixing that up became moot. – raiph Jun 04 '20 at 22:47
  • I would disagree with the compiler being multi-pass: it only passes through the code once and compiles nouns to be subroutine calls, and keeps a list of subs not defined yet. Then at the end of compilation, issues errors for subs that were not defined. – Elizabeth Mattijsen Jun 05 '20 at 09:20
  • 1
    @ElizabethMattijsen Yes about subs. But isn't the compiler multi-pass? What am I wrong about in the following? After parsing's done there's an AST. It's then optimized. Then that's turned into bytecode, which is optimized again, etc. Also, backing up to the parser, it's constructed so a `TOP` method could do whatever it wanted in terms of passing over the entire AST as many times as it wishes at the end. This isn't appropriate for Raku braid langs, but Raku could be used as a compiler for non-braid langs, like Andrew's, and then even the parser single-pass pragmatic requirement disappears. No? – raiph Jun 05 '20 at 23:56
  • Indeed, a TOP method can do what it wants. But that doesn't mean it is multi-pass in my mind. But maybe my mind is too stuck in what multi-pass means or does not mean :-) – Elizabeth Mattijsen Jun 06 '20 at 09:41
4

The title of your question is “How do I take a reference to new?”, but that is not really what you want to do.

Raku being Raku, you can actually get a reference to new.

my $ref = Card.^lookup('new');

You can't use it like you want to though.

$ref(2,'Clubs'); # ERROR

The problem is that methods take a class or instance as the first argument.

$ref(Card, 2,'Clubs');

You could use .assuming to add it in.

$ref .= assuming(Card);
$ref(2,'Clubs');

But that isn't really any better than creating a block lambda

$ref = { Card.new( |@_ ) }
$ref(2,'Clubs');

All of these work:

cross( @values, @suits ) :with({Card.new(|@_)})   # adverb outside
cross( @values, @suits,  :with({Card.new(|@_)}) ) # inside at end
cross( :with({Card.new(|@_)}), @values, @suits )  # inside at beginning


@values X[&( {Card.new(|@_)} )] @suits # cross meta-op with fake infix op


do {
    sub new-card ($value,$suit) { Card.new(:$value,:$suit) }
    @values X[&new-card] @suits
}

do {
    sub with ($value,$suit) { Card.new(:$value,:$suit) }
    cross(@values,@suits):&with
}
Brad Gilbert
  • 33,846
  • 11
  • 78
  • 129