Reviewing Java 8 Stream
API design, I was surprised by the generic invariance on the Stream.reduce()
arguments:
<U> U reduce(U identity,
BiFunction<U,? super T,U> accumulator,
BinaryOperator<U> combiner)
A seemingly more versatile version of the same API might have applied covariance / contravariance on individual references to U
, such as:
<U> U reduce(U identity,
BiFunction<? super U, ? super T, ? extends U> accumulator,
BiFunction<? super U, ? super U, ? extends U> combiner)
This would allow for the following, which isn't possible, currently:
// 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);
Workaround, use method references to "coerce" the types into the target type:
double sum = stream.reduce(0.0, numberAdder::apply, numberAdder::apply);
C# doesn't have this particular problem, as Func(T1, T2, TResult)
is defined as follows, using declaration-site variance, which means that any API using Func
gets this behaviour for free:
public delegate TResult Func<in T1, in T2, out TResult>(
T1 arg1,
T2 arg2
)
What are the advantages (and possibly, the reasons for EG decisions) of the existing design over the suggested design?
Or, asked differently, what are the caveats of the suggested design that I might be overlooking (e.g. type inference difficulties, parallelisation constraints, or constraints specific to the reduction operation such as e.g. associativity, anticipation of a future Java's declaration-site variance on BiFunction<in T, in U, out R>
, ...)?
In my opinion it's just that there's no real use case for the proposed enhancement. The proposed Javadoc has 3 more type parameters and 5 more wildcards. I guess it's enough to simplify the whole thing to the official API because regular Java developers don't want (often are not even able) to lose their mind trying to make the compiler happy. Just for the record, your
reduce()
has 165 characters in the type signature only.Also, arguments to
.reduce()
are often supplied in the form of lambda expressions, so there's no real point in having more versatile versions when such expressions often contain no or very simple business logic and are therefore used only once.For example I'm a user of your fantastic jOOQ library and also a curious Java developer that loves generics puzzles, but often I miss the simplicity of SQL tuples when I have to put wildcards in my own interfaces because of the type parameter in
Result<T>
and the kind of troubles it generates when dealing with interfaces of the record types - not that it's a jOOQ faultCrawling through the history of the lambda development and isolating "THE" reason for this decision is difficult - so eventually, one will have to wait for one of the developers to answer this question.
Some hints may be the following:
The stream interfaces have undergone several iterations and refactorings. In one of the earliest versions of the
Stream
interface, there have been dedicatedreduce
methods, and the one that is closest to thereduce
method in the question was still calledStream#fold
back then. This one already received aBinaryOperator
as thecombiner
parameter.Interestingly, for quite a while, the lambda proposal included a dedicated interface
Combiner<T,U,R>
. Counterintuitively, this was not used as thecombiner
in theStream#reduce
function. Instead, it was used as thereducer
, which seems to be what nowadays is referred to as theaccumulator
. However, theCombiner
interface was replaced withBiFunction
in a later revision.The most striking similarity to the question here is found in a thread about the
Stream#flatMap
signature at the mailing list, which is then turned into the general question about the variances of the stream method signatures. They fixed these in some places, for exampleBut noticed that in some places, this was not possible:
(emphasis by me).
The only justification that I found for not replacing the
BinaryOperator
with aBiFunction
was eventually given in the response to this statement, in the same thread:Maybe someone can dig out a perticular reference of the vote of the Expert Group that governed this decision, but maybe this quote already sufficiently answers the question of why it is the way it is...