6

According to the documentation, sort compares using infix:<cmp>.

But:

class Point
{
    has Int $.x;
    has Int $.y;
    method Str { "($!x,$!y)" }
    method gist { self.Str }
}
multi sub infix:<cmp>(Point $a, Point $b) { $a.y cmp $b.y || $a.x cmp $b.x }

my @p = Point.new(:3x, :2y), Point.new(:2x, :4y), Point.new(:1x, :1y);
say @p.sort;

gives output:

((1,1) (2,4) (3,2))

When I use:

say @p.sort(&infix:<cmp>);

it does give the proper output:

((1,1) (3,2) (2,4))

Is this a bug, a feature, or a flaw in the documentation? And is there a way to make .sort() on a list of a Points use a custom sort order without specifying a routine?

mscha
  • 6,509
  • 3
  • 24
  • 40
  • If I insert a `say` in that cmp method, it does not get called at all. It must be a matter of getting the signature right. Let me see if I can do that. – jjmerelo Dec 13 '18 at 11:56

2 Answers2

5

I think that's a case of Broken By Design. Consider the following snippet:

my $a = Point.new(:3x, :2y);
my $b = Point.new(:2x, :4y);

say &infix:<cmp>.WHICH;
say $a cmp $b;

{
    multi sub infix:<cmp>(Point $a, Point $b) { $a.y cmp $b.y || $a.x cmp $b.x }
    say &infix:<cmp>.WHICH;
    say $a cmp $b;
}

say &infix:<cmp>.WHICH;
say $a cmp $b;

The definition of the new multi candidate will generate a new proto sub that is only visible lexically. As the sort method is defined in the setting (conceptionally, an enclosing scope), it won't see your new multi candidate.

It might be possible to make sort look up &infix:<cmp> dynamically instead of lexically, though I suspect such a change would have to wait for 6.e even if we decided that's something we want to do, which isn't a given.

As a workaround, you could do something like

constant CMP = &infix:<cmp>;
multi sub infix:<cmp>(Point $a, Point $b) { ... }
BEGIN CMP.wrap(&infix:<cmp>);

for now, though I wouldn't necessarily recommend it (messing with global state considered harmful, and all that jazz)...

Christoph
  • 164,997
  • 36
  • 182
  • 240
  • Thanks, that is clear but unfortunate. It might be nice if the documentation for `sort` would warn about this. – mscha Dec 13 '18 at 13:27
3

The cmp that is being used is the cmp that is in lexical scope within sort, not the one you have defined. If you change a few lines to:

multi sub infix:<cmp>(Point $a, Point $b) { 
  say "Hey $a $b";
  $a.y cmp $b.y || $a.x cmp $b.x 
}
my @p = Point.new(:3x, :2y), Point.new(:2x, :4y), Point.new(:1x, :1y);
say @p.sort( { $^b cmp $^a } );

Since the cmp that is being used is the one that is in actual lexical scope, it gets called correctly printing:

Hey (2,4) (3,2) Hey (1,1) (2,4) Hey (1,1) (3,2) ((2,4) (3,2) (1,1))

As was required.

jjmerelo
  • 22,578
  • 8
  • 40
  • 86
  • 1
    Thanks, that's clear. `@p.sort({ $^a cmp $^b })` doesn't really add anything over `@p.sort(&infix:)` though. – mscha Dec 13 '18 at 13:29
  • 3
    Well, the order of $a and $b was reversed in @jjmerelo's answer. Not sure that was intended. If it wasn't then, you could also use `&[cmp]` as a shortcut for `&infix:`. Either of which will do a Schwartzian transform under the hood. Which shows up in this example as only 2 Hey's instead of 3. – Elizabeth Mattijsen Dec 13 '18 at 13:32
  • 1
    Thanks @Elizabeth, `&[cmp]` is a little bit nicer. – mscha Dec 13 '18 at 13:38
  • @ElizabethMattijsen, I'm not sure about the Schwartzian transform, though. That works with a one-argument function, not a comparison function, right? Note that `@p.sort({ $^a cmp $^b })` also says only two Hey's, in contrast with `@p.sort({ $^b cmp $^a })`'s 3 Hey's... – mscha Dec 13 '18 at 13:46
  • @mscha: you're right: it looks at the cardinality of the Callable. Duh. :-) – Elizabeth Mattijsen Dec 13 '18 at 13:52