Overloading operators for objects that are used to instantiate parameterized roles
Asked Answered
S

2

6

In C++ you can create templated classes that use a particular operator on the templated objects and the class from which these objects are instantiated must overload that particular operator for its objects to work with the templated class. For example, the insertion method for a BST implementation might rely on the < operator and thus any object to be stored in a BST must implement that operator.

If possible, how can I do the same with parameterized roles in Raku?


In order to provide some context, take for example the following parameterized role defined as its own module:

role BST[::T] {
    my role BinaryNode[::T] {
        has T $.item           is rw;
        has BinaryNode $.left  is rw;
        has BinaryNode $.right is rw;
    }

    has BinaryNode $!root;

    method insert( BST:D: T $x --> Nil ) {
        self!rec-insert($x, $!root)
    }

    method !rec-insert( T $x, BinaryNode $node is rw --> Nil ) {
        if !$node.defined     { $node = BinaryNode[$(T)].new(item => $x) }
        elsif $x < $node.item { self!rec-insert($x, $node.left) }
        elsif $node.item < $x { self!rec-insert($x, $node.right) }
        else                  { } # Duplicate; do nothing
    }
}

Then, it could be used to store integers:

use BST;
my $bst = BST[Int].new;

$bst.insert($_) for 6, 3, 2, 1, 4;

However, trying some user-defined type I haven't been able to make it work. Suppose we've defined a Point2D class and the less-than relationship between two Point2D objects is defined by their distance to the center (e.g., Point2D.new(:3x, :4x) is less than Point2D.new(:6x, :8y)):

use BST;

class Point2D {
    has $.x;
    has $.y;

    multi method distance {
        (($!x - 0) ** 2 ($!y - 0) ** 2).sqrt
    }
}

multi infix:«<»( Point2D:D $lhs, Point2D:D $rhs --> Bool ) {
    return $lhs.distance < $rhs.distance
}

my $bst = BST[Point2D].new;

$bst.insert(Point2D.new(:1x, :4y));
$bst.insert(Point2D.new(:3x, :4y));

=begin comment
Cannot resolve caller Real(Point:D: ); none of these signatures match:
    (Mu:U \v: *%_)
  in method rec-insert ...
  in method insert ...
=end comment

My not-so-educated guess is that the operator < for Point2D is lexical, and thus BST doesn't pick it up. If overloading an operator in a module, it's recommended to export it so that it will be available to users who use or import the module. However, I don't think that makes much sense with BST since objects of a particular class will define their relational relationship differently. Furthermore, I'm not even sure if that would work with type captures.

Situla answered 26/2, 2020 at 17:45 Comment(1)
One way I've done in the past is to define a comparison sub inside your class, say, sub cmp (Point2D \a, Point2D \b). You can then use that in your exported infixes to avoid code duplication (something like sub infix:«<»( Point2D \a, Point2D \b) { Point2D.cmp(a,b) == Order::Less } ). In parameterization, you can check that such a routine exists and use it for comparison, and failing that, use the standard cmp function. Or allow parameterization with a second optional argument: a comparison method (and note to users that it will fallback to the default cmp)Unyielding
O
5

I'm not 100% sure this is a long-term solution, but why not make the custom type Cool?

class Point2D is Cool {
    has $.x;
    has $.y;

    multi method distance {
        (($!x - 0) ** 2 + ($!y - 0) ** 2).sqrt
    }
    method Numeric() { self.distance }
    method Int() { self.Numeric.Int }
    method Str() { "$!x,$!y" }
}

Then you get all of the regular comparison goodies for free, if you supply the proper methods, like Numeric, Int and Str, to name a few.

Odine answered 26/2, 2020 at 19:58 Comment(1)
Thanks, Liz! This fits the bill perfectly and like always, in Raku things are a lot simpler than they seem to be at first.Situla
B
5

The infix < operator is for comparing real numbers.

You want it to numerically compare the value of .distance.

Perhaps it makes sense that if you try to use the object as a real number for it to automatically coerce to the distance.

class Point2D {
    has $.x;
    has $.y;

    method distance {
        (($!x - 0) ** 2 + ($!y - 0) ** 2).sqrt
    }

    method Real { self.distance } # <-----
}

Then the built-in < automatically does the right thing.


Personally I would add a few types and other annotations.
Which also makes ($!x - 0) and its equivalent (+$!x) pointless.

class Point2D {
    has Real ( $.x, $.y ) is required;

    method distance (--> Real) {
        sqrt( $!x² + $!y² );
    }

    method Real (--> Real) { self.distance }
}

It might make sense to add a feature to Raku for the generic comparisons (cmp, before, after)

Currently those call .Stringy on both values and compare them.

You can abuse this currently, by having a .Stringy method which returns something that doesn't do the Stringy role.

Perhaps it can be made to work like this:

role Comparable {
    method COMPARE () {…}
}

class Point does Comparable {
    has Real ( $.x, $.y ) is required;

    method distance (--> Real) {
        sqrt( $!x² + $!y² );
    }

    method COMPARE () {
        ($.distance, $!x, $!y)
    }
}

multi sub infix:<cmp> ( Comparable \left, Comparable \right ) {
    nextsame unless left.WHAT =:= right.WHAT;

    return Same if left =:= right;
    return Same if left eqv right;

    left.COMPARE() cmp right.COMPARE()
}

The above would compare by .distance then .x then .y.
(Of course that probably doesn't make that much sense in this case.)

Bainite answered 26/2, 2020 at 23:37 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.