Raku / problem-solving

🦋 Problem Solving, a repo for handling problems that require review, deliberation and possibly debate
Artistic License 2.0
70 stars 16 forks source link

`when` doesn't support smartmatching against a pointy block #421

Closed arunvickram closed 9 months ago

arunvickram commented 9 months ago

Pretty simple problem. This doesn't work:

class Point { has Int $.x; has Int $.y } 
my $point = Point.new(:1x, :2y);

given $point {
    # this `when` isn't able to smart match against a pointy block literal for some reason
    when -> Point:D (Int :$x, Int :$y) {  
         # we never get here
    }
}

The really odd thing is if i assign -> Point (Int :$x, Int :$y) { ... } to a variable &f, this entire thing works:

class Point { has Int $.x; has Int $.y } 
my $point = Point.new(:1x, :2y);
my &f = -> Point:D (Int :$x, Int :$y) { $x + $y };

given $point {
    when &f { say &f($_) } 
}

In theory both of these snippets of code I think should be equivalent, but I'm not able to actually do it.

vrurg commented 9 months ago

This is because it doesn't work as what happens here is not what you expect to have. First of all, one has to remember that the expression between when keyword and opening brace of its block is a right side of a smartmatch. Essentially, a when block is something like

if $_ ~~ <when_expression> { ... when block ... }

except that when has side effects that we love it for. One of them is the fact that if a when matches its block can use proceed and succeed to manage control flow. But these are only working within a bare block and are not compatible with pointy.

You can see what happens on your own if you add debug prints into the body of &f. They'll be seen twice in the output.

Also, I wonder wether you noticed the following error from the compiler for the first example:

===SORRY!===
Expression needs parens to avoid gobbling block

Because the right code would be looking like this:

class Point { has Int $.x; has Int $.y }
my $point = Point.new(:1x, :2y);

given $point {
    # this `when` isn't able to smart match against a pointy block literal for some reason
    when -> Point:D (Int :$x, Int :$y) {
        note "THIS IS MATCHER for $x,$y";
    }
    { # the when block, actually
        note "and this is the actual when block for ", $_;
    }
}

And it is essentially the same, as the working example, except that the matcher code is used literally, without assigning it to a variable first.

arunvickram commented 9 months ago

Interesting. The reason I asked is that I've been trying to get something like this working in one of my programs:

class Point { has Int $.x; has Int $.y }
my $point = Point.new(:1x, :2y);

given $point {
    # this `when` isn't able to smart match against a pointy block literal for some reason
    matches -> Point (Int :$x, Int :$y) {
        say $x + $y;
    }
}

I figured that both pointy blocks and subroutines/multimethods are capable of structural pattern matching, but I found it really weird that you couldn't do the same in when blocks. I had this working:

sub matches(&f) {
  $_ = CALLERS::<$_>;
  try when &f { f |$_ };
}

but the problem is when I tried applying it here:

class Point { has Int $.x; has Int $.y }
class Circle { has Int $.radius }
my $point = Point.new(:1x, :2y);

given $point {
    # this `when` isn't able to smart match against a pointy block literal for some reason
    matches -> Point (Int :$x, Int :$y) {
        say $x + $y;
    }

    matches -> Circle (Int :$radius) {
        say $radius;
    }

    default { say "Hello"; }
}

It actually ends up testing all the various statements and runs the default case as well. I was trying to figure out a way to get that structural pattern matching and destructuring within a given block with the ability to auto-break like when, but I kept hitting a brick wall.

@lizmat suggested maybe a slang might be an option here, but I feel like the structural pattern matching in given that I'm trying to achieve is a logical extension of an already existing feature in Raku: the pattern matching in argument lists in subroutines, and therefore it might be best suited as a standard feature of Raku. Thoughts?

vrurg commented 9 months ago

Makes no sense. Local destructuring can be done with my (:$x, :$y) := $point if necessary. But your case is best covered by multi-dispatching:

proto sub matches(|) {*}
multi sub matches(Point:D (:$x, :$y)) {...}
multi sub matches(Circle:D (:$radius)) {...}
multi sub matches(Shape:D $other-shape) {...} # Shape is a role or a base class
multi sub matches($) { die "Hey, I wanna do some geometry here!" }

# ... somewhere in a galaxy far away ...
$shape = point_or_whatever_it_can_be();
matches($shape);
arunvickram commented 9 months ago

Let me see how this works in my code and get back to you!

raiph commented 9 months ago

See also comment by jnthn on an SO answer to a related question.

arunvickram commented 9 months ago

Ah that makes perfect sense, you're basically saying that topicalization makes destructuring unnecessary, right?

CIAvash commented 9 months ago

You can also use PatternMatching or _::Pattern::Match.

arunvickram commented 9 months ago

Alright, I'll close this issue because it looks like there are alternatives, but I may revisit this later!