www.digitalmars.com         C & C++   DMDScript  

digitalmars.D.learn - Equality Methods

I have already asked about this topic here months ago, but now I know a bit
more OOP, so I can raise the bar for myself a bit. So now I consider class
inheritance too. I am trying to write "correct" equality Methods.

I have read this nice article about equality in Java, the things it says can be
used equally in D too:
http://www.artima.com/lejava/articles/equality.html

Even comparing really simple objects can be not easy to do.
So do you see troubles/ bugs/ things that can be improved in the following
code? (D1, Phobos, putr is writefln):
 

import std.string: format;
import std.stdio: putr = writefln;


/// Used by all exceptions
template ExceptionTemplate() {
    this() {
        super(this.classinfo.name);
    }

    this(string msg) {
        super(this.classinfo.name ~ ": " ~ msg);
    }
}


/// Thrown by user-defined opCmp() of classes
class UncomparableException: Exception {
    mixin ExceptionTemplate;
}


class Point {
    // in practice for this purpose, instead of re-defining opEquals,
    // opCmp, opHash, etc, I use a Record (Tuple in Phobos2).

    alias typeof(this) TyThis;
    private final int x, y;

    this(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int opEquals(Object other) {
        auto that = cast(TyThis)other;
        return that !is null && that.canEqual(this) &&
                    this.x == that.x && this.y == that.y;
    }

    public bool canEqual(Object other) {
        return cast(TyThis)other !is null; // ?
    }

    public hash_t toHash() {
        return (41 * (41 + this.x) + this.y); // ugly, but it doesn't matter now
    }

    public int opCmp(Object other) {
        auto that = cast(TyThis)other;
        if (that is null || !that.canEqual(this)) // ?
            throw new UncomparableException("");
        if (this.x == that.x)
            return this.y - that.y;
        else
            return this.x - that.x;
    }

    public string toString() {
        return format("Point(%d, %d)", this.x, this.y);
    }
}


enum Color { RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET }


public class ColoredPoint : Point {
    alias typeof(this) TyThis;
    private final Color color;

    this(int x, int y, Color color) {
        super(x, y);
        this.color = color;
    }

    // define this if you want this uncomparable to the base class
    override public bool canEqual(Object other) {
        return cast(TyThis)other !is null; // ?
    }

    override public int opEquals(Object other) { // bug: not symmetric
        auto that = cast(TyThis)other;
        return that !is null && that.canEqual(this) &&
               this.color == that.color && super.opEquals(that); // ?
    }

    override public int opCmp(Object other) {
        auto that = cast(TyThis)other;
        if (that is null || !that.canEqual(this)) // ?
            throw new UncomparableException("");
        // coordinates are more important than color
        int super_cmp = super.opCmp(that);
        if (super_cmp == 0)
            return this.color - that.color;
        else
            return super_cmp;
    }

    public string toString() {
        return format("Point(%d, %d)", this.x, this.y);
    }
}


class Point2 : Point {
    this(int x, int y) {
        super(x, y);
    }
}


class Point3 : Point {
    alias typeof(this) TyThis;

    this(int x, int y) {
        super(x, y);
    }

    public bool canEqual(Object other) {
        return cast(TyThis)other !is null; // ?
    }
}


void main() {
    auto p1 = new Point(1, 1);
    auto p2 = new Point(1, 1);
    auto p3 = new Point(1, 2);
    putr(p1 == p2, " ", p2 == p1); // 1 1
    putr(p1 == p3, " ", p3 == p1); // 0 0
    putr(p2 == p3, " ", p3 == p2); // 0 0
    int[Point] aa = [p1:1, p2:2, p3:3];
    putr(aa);
    auto p4 = new Point(1, 0);
    auto p5 = new Point(2, 0);
    auto p6 = new Point(0, 2);
    putr(p1 > p3, " ", p3 > p1); // false true
    putr(p1 > p4, " ", p4 > p1); // true false
    putr(p1 > p5, " ", p5 > p1); // false true
    putr(p1 > p6, " ", p6 > p1); // true false
    putr();

    Point p8 = new Point(1, 2);
    ColoredPoint cp1 = new ColoredPoint(1, 2, Color.RED);
    putr(p8 == cp1); // prints 0
    putr(cp1 == p8); // prints 0
    //putr(p8 > cp1); // uncomparable exception
    //putr(p8 > cp1); // uncomparable exception
    //putr(p8 < cp1); // uncomparable exception
    //putr(cp1 < p8); // uncomparable exception
    //putr(cp1 > p8); // uncomparable exception
    putr();

    putr(cast(Point)p1 !is null, " ", cast(Point)cp1 !is null);              
// true true
    putr(cast(ColoredPoint)p1 !is null, " ", cast(ColoredPoint)cp1 !is null);
// false true
    putr(p1.classinfo == p2.classinfo, " ", p1.classinfo == p6.classinfo);   //
1 1
    putr(cp1.classinfo == p1.classinfo, " ", p1.classinfo == cp1.classinfo); //
0 0
    putr();

    auto pp1 = new Point2(1, 1);
    putr(p1 == pp1); // 1
    putr(p1 > pp1, " ", p1 < pp1); // false false
    auto pp2 = new Point3(1, 1);
    putr(p1 == pp2); // 0
    //putr(p1 > pp2, " ", p1 < pp2); // uncomparable exception
}


opCmp is necessary in D even for associative arrays because hash collisions are
resolved with a search tree that needs opCmp. If D AAs become simpler, and
adopt a simpler collision management strategy (see for example Python dicts, or
C# hash maps), the opCmp isn't necessary anymore, and the code in this page
gets simpler.

Some people in the comments of that article talk about an hypothetical
Equalator interface, but I am not expert enough yet about OOP to tell if the
Equalator idea is good.

Bye and thank you,
bearophile
Jun 09 2009