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

Каковы веские причины выбора инвариантности в API, например Stream.reduce()?

Рассмотрение дизайна API Java 8 Stream, я был удивлен общей инвариантностью аргументов Stream.reduce():

<U> U reduce(U identity,
             BiFunction<U,? super T,U> accumulator,
             BinaryOperator<U> combiner)

Казалось бы, более универсальная версия того же API могла бы применять ковариацию/контравариантность для отдельных ссылок на U, например:

<U> U reduce(U identity,
             BiFunction<? super U, ? super T, ? extends U> accumulator,
             BiFunction<? super U, ? super U, ? extends U> combiner)

Это позволило бы сделать следующее, что невозможно в настоящее время:

// Assuming we want to reuse these tools all over the place:
BiFunction<Number, Number, Double> numberAdder =
    (t, u) -> t.doubleValue() + u.doubleValue();

// This currently doesn't work, but would work with the suggestion
Stream<Number> stream = Stream.of(1, 2L, 3.0);
double sum = stream.reduce(0.0, numberAdder, numberAdder);

Обходной путь, используйте ссылки на методы для "принуждения" типов к целевому типу:

double sum = stream.reduce(0.0, numberAdder::apply, numberAdder::apply);

С# не имеет этой конкретной проблемы, поскольку Func(T1, T2, TResult) определяется следующим образом, используя дисперсию объявления-сайта, что означает, что любой API, использующий Func, получает это поведение бесплатно:

public delegate TResult Func<in T1, in T2, out TResult>(
    T1 arg1,
    T2 arg2
)

Каковы преимущества (и, возможно, причины решений EG) существующего дизайна над предлагаемым дизайном?

Или, по-другому, какие предостережения предлагаемого дизайна, которые я мог бы игнорировать (например, трудности ввода вывода, ограничения параллелизации или ограничения, характерные для операции сокращения, такие как, например, ассоциативность, предвидение будущего сайта объявления Java дисперсия на BiFunction<in T, in U, out R>,...)?

4b9b3361

Ответ 1

Сканирование истории развития лямбда и выделение "причины" для этого решения затруднено - поэтому в конечном итоге придется подождать, пока один из разработчиков ответит на этот вопрос.

Некоторые подсказки могут быть следующими:

  • Интерфейсы потока подверглись нескольким итерациям и рефакторингам. В одной из ранних версий интерфейса Stream были выделены методы reduce, а тот, который ближе всего к методу reduce в вопросе, все еще назывался Stream#fold. Этот уже получил BinaryOperator как параметр combiner.

  • Интересно, что в течение долгого времени предложение лямбда включало выделенный интерфейс Combiner<T,U,R>. Контринтуитивно это не использовалось как combiner в функции Stream#reduce. Вместо этого он использовался как reducer, который, как представляется, теперь называется accumulator. Однако интерфейс combiner был заменен на BiFunction в более поздней версии.

  • Самое яркое сходство с вопросом здесь находится в потоке о подписи Stream#flatMap в списке рассылки, который затем превращается в общий вопрос о дисперсиях сигнатур метода потока. Они фиксировали их в некоторых местах, например

    Как Брайан поправил меня:

    <R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);

    вместо:

    <R> Stream<R> flatMap(Function<T, Stream<? extends R>> mapper);

    Но заметил, что в некоторых местах это было невозможно:

    T reduce(T identity, BinaryOperator<T> accumulator);

    и

    Optional<T> reduce(BinaryOperator<T> accumulator);

    Невозможно исправить, потому что они использовали "BinaryOperator", но если "BiFunction" используется, тогда мы обладаем большей гибкостью

    <U> U reduce(U identity, BiFunction<? super U, ? super T, ? extends U> accumulator, BinaryOperator<U> combiner)

    Вместо:

    <U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);

    Тот же комментарий относительно 'BinaryOperator'

    (выделение мной).


Единственное оправдание, которое я нашел для не, заменив BinaryOperator на BiFunction, было в конечном итоге приведено в ответе этот оператор в том же потоке:

BinaryOperator не будет заменен BiFunction, даже если, как вы сказали, он вводит большую гибкость, BinaryOperator спрашивает, что два параметра и тип возврата будет таким же, чтобы он имел концептуально больший вес (EG уже голосует за это).

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

Ответ 2

По-моему, это просто отсутствие реального варианта использования для предлагаемого улучшения. В предлагаемом Javadoc есть еще 3 типа параметров и еще 5 подстановочных знаков. Я думаю, этого достаточно, чтобы упростить все это до официального API, потому что обычные разработчики Java не хотят (часто даже не способны) потерять рассудок, пытаясь сделать компилятор счастливым. Только для записи ваш reduce() имеет только 165 символов в сигнатуре типа.

Кроме того, аргументы .reduce() часто передаются в виде лямбда-выражений, поэтому нет реальной точки в использовании более универсальных версий, когда такие выражения часто не содержат или очень простой бизнес-логики и поэтому используются только один раз.

Например, я являюсь пользователем вашей фантастической библиотеки jOOQ, а также любопытным разработчиком Java, который любит головоломки с универсалами, но часто я пропускаю простоту SQL-кортежей, когда мне приходится ставить подстановочные знаки в свои собственные интерфейсы из-за параметр типа в Result<T> и типы проблем, которые он генерирует при работе с интерфейсами типов записей - не то, что это ошибка jOOQ