Подпись Java 'reduceLeft' / Аргументы типа с нижней границей

Следующая подпись действительна и обычно используется в Scala:

trait Collection[A] {
    def reduceLeft [B >: A] (f: (B, A) => B): B
}

Однако, поскольку >: является Scala-эквивалентом super в Java, моей первой идеей преобразовать эту подпись (заменив тип функции на BiFunction и используя аннотации дисперсии Use-Site, также известные как Bounded Wildcards), будет

interface Collection<A> {
    <B super A> B reduceLeft(BiFunction<? super B, ? super A, ? extends B> mapper)
}

Но о нет! Компилятор жалуется на токен super в <B super A>, потому что у вас не может быть переменных типа с нижней границей! Теперь, как мне написать этот метод в коде Java, не возвращаясь во времени к тому времени, когда в мире Java не существовало дженериков?


Да, я знаю, что вы думаете, что я мог бы использовать B extends A, но это не одно и то же, как показывает моя реализация:

public <R extends E> R reduceLeft(BiFunction<? super R, ? super E, ? extends R> mapper)
{
    if (this.isEmpty())
    {
        return null;
    }

    Iterator<E> iterator = this.iterator();
    R first = iterator.next(); // doesn't work, but would if R was a super-type of E (R super E)
    while (iterator.hasNext())
    {
        mapper.apply(first, iterator.next());
    }

    return first;
}

Вместо этого мне пришлось использовать эту немного более ограниченную версию:

public E reduceLeft(BiFunction<? super E, ? super E, ? extends E> mapper)
{
    if (this.isEmpty())
    {
        return null;
    }

    Iterator<E> iterator = this.iterator();
    E first = iterator.next();
    while (iterator.hasNext())
    {
        first = mapper.apply(first, iterator.next());
    }

    return first;
}

person Clashsoft    schedule 14.07.2015    source источник
comment
Во-первых, вы не объявляете параметр типа A. Поэтому я переписал это на <A, B super A> B reduceLeft(BiFunction<? super B, ? super A, ? extends B> mapper). Но это все еще не работает. Однако это работает с extends. Тогда я подумал, B super A это то же самое, что и A extends B? Я думаю, что они одинаковы... так что вы можете написать <B, A extends B> B reduceLeft(BiFunction<? super B, ? super A, ? extends B> mapper)   -  person xp500    schedule 15.07.2015


Ответы (3)


Ограничение B >: A в определении метода Scala необходимо, потому что:

  1. Scala использует вариантность сайтов объявлений, а неизменяемые коллекции являются ковариантными по типу содержащихся в них элементов.
  2. Концептуально reduceLeft должен возвращать значения типа A, но использование A в качестве возвращаемого типа означает использование его в ковариантной позиции, что противоречит уже объявленной дисперсии, т. е. A должно быть ковариантным.

Хитрость, позволяющая обойти этот конфликт дисперсии, состоит в том, чтобы ввести этот универсальный тип B.

Теперь, как вы упомянули, в Java используется вариантность использования сайта, поэтому любая коллекция, написанная на Java, будет инвариантной. Это также означает, что нет проблем с использованием A в качестве возвращаемого типа метода, т. е. в контравариантной позиции. Итак, приведенного ниже определения должно быть достаточно — нет необходимости в типе B:

interface Collection<A> {
  A reduceLeft(BiFunction<? super A, ? super A, ? extends A> reducer);
}

Однако, как вы можете видеть, чистый эффект от того, что A один раз будет нижней границей, а затем верхней границей, заключается в том, что A в основном инвариантен — невозможно извлечь выгоду из границ подстановочных знаков без использования понижающего приведения. Это означает, что мы можем упростить подпись (что очень похоже на Stream.reduce):

interface Collection<A> {
  A reduceLeft(BiFunction<A, A, A> reducer);
}

Кроме того, тип BiFunction<A, A, A> уже присутствует в Java 8 под именем BinaryOperator<A>.

person Ionuț G. Stan    schedule 15.07.2015
comment
«использование A в качестве возвращаемого типа означает использование его в контравариантной позиции» — разве возвращаемый тип не является ковариантной позицией? - person Clashsoft; 15.07.2015
comment
Кроме того, вы уверены, что подстановочные знаки на самом деле бесполезны? Я мог бы представить ситуации, когда вы хотите вызвать что-то вроде Collection<Number>.reduceLeft((Object, Object) -> Integer) (хотя взамен вы получаете Number, но у вас может быть функция с этим типом, и вы хотите использовать ее повторно) - person Clashsoft; 16.07.2015
comment
@Clashsoft верно, я имел в виду ковариант в этом предложении, спасибо. Что касается вашего второго замечания... не уверен. Пример lmm был лучшим вариантом использования, но в остальном я не могу придумать типобезопасный (т. е. без отражения, instanceof проверки или приведения) метод, который жонглирует такими типами. Но вы правы, есть место для лучшей подписи. - person Ionuț G. Stan; 16.07.2015
comment
Хм, я не уверен, что вы подразумеваете под в основном инвариантным. Я имею в виду, я понимаю, что вы имеете в виду в случае ковариантного возвращаемого типа, но как насчет контравариантных типов аргументов? Я показал пример этот вопрос, где добавленная дисперсия поможет повторно использовать определенные функции в разных reduce() вариантах использования, спасибо к несуществующей в настоящее время контравариантности по типам аргументов функций. - person Lukas Eder; 28.02.2016

Вы не можете; Java не считает эту функциональность достаточно полезной. Возможно, это изменится сейчас, когда Java все больше использует функции высшего порядка.

Я бы эмулировал эту функциональность, приняв функцию, которая «свидетельствует» о том, что A <: B:

interface Collection<A> {
  static class Witness {
    static <B, A extends B> Function<A, B> witness() {
    return new Function<A, B> {
      public B apply(A value) {
        return value;
      }
    };
  }
  //Could take a custom type instead of Function if we want to enforce
  //that arbitrary functions aren't passed.
  //Could also just use foldLeft rather than reduceLeft
  <B> B reduceLeft(BinaryOperator<B, B, B> mapper, Function<A, B> witness);
}

Collection<Double> collectionOfDoubles = ...
BinaryOperator<Number, Number, Number> numberFunction = ...
//I haven't tested whether we need to pass explicit type arguments
collectionOfDoubles.reduceLeft(numberFunction, Collection.Witness.witness());

Но мы идем по пути гринсупаннинга.

Обратите внимание, что в дисперсии есть реальная ценность; с напр. Решение @Ionut, вызов collectionOfDoubles.reduceLeft невозможен, поскольку Double и Number не одного типа.

person lmm    schedule 15.07.2015
comment
Это кажется мне излишним + накладные расходы. Я, вероятно, полностью избавлюсь от параметра типа B, учитывая, что в любом случае супертипизация не так много применений. - person Clashsoft; 16.07.2015

Простое решение — прибегнуть к static методам. Затем вы можете объявить как тип элемента Collection, так и возвращаемый тип сокращения и, таким образом, легко объявить отношение E extends R с ним:

public static <R, E extends R> R reduceLeft(
    Collection<? extends E> c, BiFunction<? super R, ? super E, ? extends R> mapper) {
    if(c.isEmpty()) return null;
    Iterator<? extends E> iterator = c.iterator();
    R value = iterator.next();
    while(iterator.hasNext()) value=mapper.apply(value, iterator.next());
    return value;
}

непосредственным преимуществом этого подхода является то, что он работает со всеми коллекциями, а не только с типом, в котором вы объявляете метод.

Обратите внимание, что вы также можете полностью удалить взаимосвязь между типом элемента и типом результата, если запросите соответствующее значение идентификатора для функции, что также поддерживается API потока. Это также устраняет недостаток вашей реализации возврата null для пустой коллекции:

public static <R, E> R reduceLeft(Collection<? extends E> c,
                      R identity, BiFunction<? super R, ? super E, ? extends R> mapper) {
    if(c.isEmpty()) return identity;
    R value=identity;
    for(E e: c) value=mapper.apply(value, e);
    return value;
}

Теперь, когда не требуется объявление отношения между E и R, эта версия также может быть методом экземпляра вашего типа коллекции:

public <R> R reduceLeft(R identity, BiFunction<? super R, ? super E, ? extends R> mapper) {
    if(isEmpty()) return identity;
    R value=identity;
    for(E e: this) value=mapper.apply(value, e);
    return value;
}

Но если вам не нравится требование предоставлять значение идентификатора, вы также можете предоставить функцию для начального преобразования первого элемента, что тривиально для случая, когда R является супертипом E:

public <R> R reduceLeft(Function<? super E, ? extends R> cast,
                        BiFunction<? super R, ? super E, ? extends R> mapper) {
    if(isEmpty()) return null;
    Iterator<E> it=iterator();
    R value=cast.apply(it.next());
    while(it.hasNext()) value=mapper.apply(value, it.next());
    return value;
}

Таким образом, если R является супертипом E, достаточно передать t->t или Function.identity() в качестве параметра cast. Но это также позволяет использовать больше случаев, когда не существует связи между R и E.

person Holger    schedule 15.07.2015
comment
Мой метод reduceLeft является частью моего собственного API коллекции, поэтому статический метод не требуется. reduceLeft с параметром identity — это то, что я бы назвал foldLeft, и я уже добавил + реализовал его. Что касается последней идеи, я не вижу ситуации, когда вы бы передали что-то кроме t -> t (или Function.identity())... - person Clashsoft; 16.07.2015