Подтвердить что ты не робот

Ограниченная реализация интерфейса

В Haskell (и Rust и другие) у меня могут быть экземпляры, которые ограничены другими экземплярами:

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'

С интерфейсами Java я не могу. Я должен потребовать, чтобы параметры типа Pair всегда реализовывали Eq, или я вообще не могу реализовать Eq<Pair<A, B>>:

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);
    }
}

Я хотел бы иметь что-то вроде:

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

До сих пор Интернет сказал мне, что моя желаемая функциональность напрямую не поддерживается на Java. Тем не менее, я считаю это довольно важным фактором в (re) удобстве использования интерфейсов. Я хотел бы знать, есть ли обходные пути или решения, которые примерно охватывают одно и то же основание.

4b9b3361

Ответ 1

Одно каноническое решение, которое приходит мне на ум, состоит в том, чтобы сделать компаратор внешним по отношению к классу. Это подход, который Scala принимает, упрощая его усвоение с помощью implicits. И у вас были бы методы построения компараторов, такие как

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

Это, однако, довольно громоздко для работы. Haskell делает то же самое внутри, передавая словари реализаций типов классов под капотом, но интерфейс типа класса вместе с типом вывода делает его намного приятнее.

Насколько я знаю, в Java нет способа объявить условные экземпляры. Но более подход OO состоял бы в том, чтобы иметь подкласс для пар, которые допускают равенство:

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

...

Снова задействован какой-то ручной процесс, так как вам нужно решить, когда использовать Pair и PairEq. Но с перегрузкой метода мы могли бы упростить использование, объявив интеллектуальные конструкторы. Поскольку Java всегда выбирает наиболее конкретную, всякий раз, когда нам нужно создавать пару, мы просто используем mkPair, а Java выбирает возвращающий PairEq, если аргументы реализуют Eq соответственно:

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);
}

Полный пример кода:

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);
    }
}

Ответ 2

Ну... нет.

Но у нас может быть метод удобства, который "отличает" Pair<A,B> до Eq<Pair<A,B>>, если A,B тоже Eq. Для этого требуется дополнительный шаг на сайте использования, и программист должен сделать явный тип при необходимости; но это, вероятно, не так уж плохо.

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);

Желательно, чтобы метод cast использовался как метод экземпляра, что-то вроде

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

Однако мы не хотим, чтобы это компилировалось, если X,Y не удовлетворяет ограничениям. Один из способов обеспечить соблюдение этого

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) );
    }
}

Ответ 3

Вы неправильно переводите эти типы.

Typeclasses не имеют ничего общего с наследованием. Они реализуют Ad-hoc-полиморфизм, т.е. Используются для расширения функциональности уже определенных типов.

Вещь может быть переведена на Java как шаблон, но вы должны заметить, что в то время как в Haskell экземпляры предоставляются неявно, в Java вам придется явно передавать их как параметры:

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);
      }
    };
  }
}

И вот как вы можете написать полиморфные функции, используя этот шаблон:

boolean someMethodThatUsesAnEqInstance<A>(final Eq<A> aEqInstance, A a1, A a2) {
  return aEqInstance.eq(a1, a2);
}