13

In Haskell (and Rust, and others) I can have instances that are constrained by other instances:

data Pair a b = Pair a b

instance (Eq a, Eq b) => Eq (Pair a b) where 
    Pair a b == Pair a' b' = a == a' && b == b'

With Java interfaces I can't. I must require the type parameters of Pair to always implement Eq, or I can't implement Eq<Pair<A, B>> at all:

interface Eq<A> {
    public boolean eq(A other);
}

class Pair<A extends Eq<A>, B extends Eq<B>> implements Eq<Pair<A, B>> {
    A a;
    B b;
    public boolean eq(Pair<A, B> other){
        return a.eq(other.a) && b.eq(other.b);
    }
}

I'd like to have something like:

class Pair<A, B> implements Eq<Pair<A, B>> if (A implements Eq<A> && B implements Eq<B>) {...}

So far the Internet has told me that my desired functionality isn't directly supported in Java. Nevertheless I find this a rather critical factor in the (re)usability of interfaces. I'd like to know if there are workarounds or solutions that approximately cover the same ground.

András Kovács
  • 29,931
  • 3
  • 53
  • 99

3 Answers3

7

One canonical solution that comes to my mind is to make the comparator external to the class. This is the approach that Scala takes, while making it easier digestible with the help of implicits. And they you'd have methods for constructing comparators, such as

public <A, B> Comparator<Pair<A,B>> pairCmp(Comparator<A> ca, Comparator<B> cb) { ...

This is however quite cumbersome to work with. Haskell does the same thing internally, passing dictionaries of type-classes implementitions under the hood, but the type-class interface together with type inference makes it much more pleasant.

As far as I know, in Java there is no way how to declare conditional instances. But a more OO approach would be to have a sub-class for pairs that allow equality:

class PairEq<A extends Eq<A>, B extends Eq<B>>
  extends Pair<A,B>
  implements Eq<Pair<A, B>> {

...

There is again some manual process involved, as you need to decide when to use Pair and when PairEq. But with method overloading, we could make it easier to use by declaring smart constructors. As Java always picks the most specific one, whenever we need to create a pair, we just use mkPair and Java will pick the one returning PairEq, if the arguments implement Eq appropriately:

public static <A,B> Pair<A,B> mkPair(A a, B b) {
    return new Pair<A,B>(a, b);
}

public static <A extends Eq<A>, B extends Eq<B>> PairEq<A,B> mkPair(A a, B b) {
    return new PairEq<A,B>(a, b);
}

Complete sample code:

interface Eq<A> {
    public boolean eq(A other);
}

public class Pair<A,B> {
    public final A a;
    public final B b;

    public Pair(A a, B b) {
        this.a = a;
        this.b = b;
    }

    public static <A,B> Pair<A,B> mkPair(A a, B b) {
        return new Pair<A,B>(a, b);
    }

    public static <A extends Eq<A>, B extends Eq<B>> PairEq<A,B> mkPair(A a, B b) {
        return new PairEq<A,B>(a, b);
    }
}

class PairEq<A extends Eq<A>, B extends Eq<B>>
    extends Pair<A,B>
    implements Eq<Pair<A,B>>
{
    public PairEq(A a, B b) {
        super(a, b);
    }

    @Override
    public boolean eq(Pair<A,B> that) {
        return a.eq(that.a) && b.eq(that.b);
    }
}
Petr
  • 62,528
  • 13
  • 153
  • 317
3

You're translating the typeclasses wrong.

Typeclasses have nothing to do with inheritance. They implement Ad-hoc polymorphism, i.e. they are used to extend the functionality of the types which are already defined.

The thing can be translated into Java as a pattern, but you must take into notice that whilst in Haskell the instances are provided implicitly, in Java you'll have to explicitly pass them around as parameters:

interface Eq<A> {
  public boolean eq(A left, A right);
}

class EqInstances {
  public static Eq<Pair<A, B>> pair (final Eq<A> aInstance, final Eq<B> bInstance) {
    return new Eq<Pair<A, B>> {
      public boolean eq(Pair<A, B> left, Pair<A, B> right) {
        return aInstance.eq(left.a, right.a) && bInstance.eq(left.b, right.b);
      }
    };
  }
}

And here's how you can write the polymorphic functions using this pattern:

boolean someMethodThatUsesAnEqInstance<A>(final Eq<A> aEqInstance, A a1, A a2) {
  return aEqInstance.eq(a1, a2);
}
Nikita Volkov
  • 42,792
  • 11
  • 94
  • 169
2

Well... no.

But we can have a convenience method, that "casts" a Pair<A,B> to an Eq<Pair<A,B>>, if A,B are both Eq too. This requires an extra step on use site, and the programmer must do an explicit cast of type when it is needed; but that's probably not too bad

class Pair<A, B>
{
    A a;
    B b;
}

static
<A extends Eq<A>, B extends Eq<B>>
Eq<Pair<A,B>> cast(Pair<A,B> p1)
{
    return p2->( p1.a.eq(p2.a) && p1.b.eq(p2.b) );
}

--

    Pair<X,Y> pair = ...;

    Eq<Pair<X,Y>> eq = cast(pair);

Preferably, we'd like the cast method as an instance method, something like

    Eq<Pair<X,Y>> eq = pair.asEq();

However, we don't want this to compile,unlessX,Y satisfy the constraints. One way to enforce that

class Pair<A, B>
{
    A a;
    B b;

    <A2 extends Eq<A>, B2 extends Eq<B>>
    Eq<Pair<A2,B2>>
    asEq()
    {
        return p2->( p2.a.eq(a) && p2.b.eq(b) );
    }
}
ZhongYu
  • 19,446
  • 5
  • 33
  • 61