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

Обзор Java 8 Stream API дизайн, я был удивлен общей неизменности на 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);

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

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

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

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

2 ответа

Решение

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

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

  • Интерфейсы потока прошли несколько итераций и рефакторингов. В одной из самых ранних версий 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 с BiFunction в конце концов был дан в ответе на это утверждение, в той же теме:

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

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

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

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

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

Другие вопросы по тегам