Hi Anthony, It's a good point.
You're correct in saying that `Stream.Builder` is a superset of `Consumer`, however `Stream.Builder`'s usual usage is in the creation of new streams. To use it in this context brings with it the connotation that we are going to create a new stream, something we don't want to suggest. In its current state, we push directly downstream and don't create an intermediate stream (like in `flatMap`, for example). With regards to the build operation, I have to disagree. The build method is central to the `Stream.Builder`'s use case. In your example, I think it's obvious that close shouldn't be called unless you wish the Stream to be closed. However, in the case with build the opposite is true: it's not obvious that a call to build shouldn't be made when you want to finish building your stream. Both operations are similar, but `Consumer` doesn't bring this baggage with it and therefore seems more fitting. Kind regards, Patrick > On 4 Jul 2020, at 17:23, Anthony Vanelverdinghe <d...@anthonyv.be> wrote: > > On 02/07/2020 16:38, Patrick Concannon wrote: >> Hi Anthony, > Hi Patrick >> Thanks for your suggestion of using a Stream.Builder instead of a Consumer. > Thanks for your response. Since Stream.Builder extends Consumer, I don't see > it as "instead of": the proposed signature is a strict superset of the actual > signature. For example, it would accept a BiConsumer<Object, > Stream.Builder<String>>, whereas the current signature doesn't. >> However, one of the goals for mapMulti is not to create an additional Stream. > Would you mind to elaborate on this? Given that Stream.Builder extends > Consumer, I don't see why using Stream.Builder would imply having to create > any more Streams than when using Consumer. >> Also, I fear that using a builder, but throwing an exception on a call to >> build, is counter-intuitive and perhaps confusing to the user. > Point taken, but I beg to differ: to me this is no different than a method > `void foo(Consumer<InputStream> bar)` which specifies that the given Consumer > must not invoke `close` on the InputStream. > Moreover, with the build step being the sole difference with `flatMap`, and > the elimination of intermediary Streams being a rather obvious optimization, > I believe most users would find it intuitive not to invoke `build` themselves > (or they would have read the Javadoc and know not to :)). > Finally, using Stream.Builder makes it intuitive for me what the method does, > even without considering its name. > > Given the above, I still hold that Stream.Builder is a better choice to use > in the signature. Even more so, I believe there are several things that hint > at the need for an additional abstraction: your point about the potential > confusion, the analogy with Collector (as already mentioned by Paul w.r.t. > the order of the BiConsumer's type arguments), and the natural similarity > between e.g. BiConsumer<T, Stream.Builder<R>> and Function<T, Stream<R>>. > > Therefore I'd like to propose the following (complete declarations below): > > * introduce a new interface: Transformer<T, I, R> extends Function<T, R> > * adapt Collector to extend it: Collector<T, A, R> extends > Transformer<Stream<T>, A, R> > > So a Transformer is nothing more than a Function which passes via an > intermediary I to map T to R according to a certain protocol. > > Then a method with the following signature could be added to Stream: > <R> Stream<R> flatMap(Transformer<? super T, ? super Stream.Builder<R>, ? > extends Stream<R>> mapper) > > Such a Transformer could readily be created from a given BiConsumer (see > Tranformer::of below). > > Since Transformer is a subinterface of Function, there wouldn't be any > ambiguities w.r.t. overload resolution, so we could just name the new method > `flatMap` (I assume such ambiguities are the reason why this currently isn't > the case, since Function and BiConsumer are unrelated?). > > Below is a self-contained "demo", containing a simple Demo class, the > declaration of Transformer, and the adapted declaration of Collector. > > Thanks in advance for any feedback. > > Kind regards > Anthony > > > > import java.util.ArrayList; > import java.util.List; > import java.util.Set; > import java.util.function.BiConsumer; > import java.util.function.BinaryOperator; > import java.util.function.Function; > import java.util.function.Supplier; > import java.util.stream.Stream; > > public class Demo { > > public static void main(String[] args) { > BiConsumer<String, Stream.Builder<String>> anagrams = (word, builder) > -> { > if(word.equals("listen")) { > builder.accept("silent"); > builder.accept("enlist"); > } > }; > > Transformer.of(anagrams::accept).apply("listen").forEachOrdered(System.out::println); > } > > } > > interface Transformer<T, I, R> extends Function<T, R> { > > Supplier<? extends I> supplier(); > > BiConsumer<? super T, ? super I> transformer(); > > Function<? super I, ? extends R> finisher(); > > @Override > default R apply(T t) { > var intermediary = supplier().get(); > transformer().accept(t, intermediary); > return finisher().apply(intermediary); > } > > static <U, V> Transformer<U, Stream.Builder<V>, Stream<V>> > of(BiConsumer<? super U, ? super Stream.Builder<V>> transformer) { > return new Transformer<>() { > @Override > public Supplier<Stream.Builder<V>> supplier() { > return Stream::builder; > } > > @Override > public BiConsumer<U, Stream.Builder<V>> transformer() { > return transformer::accept; > } > > @Override > public Function<Stream.Builder<V>, Stream<V>> finisher() { > return Stream.Builder::build; > } > }; > } > > } > > interface Collector<T, A, R> extends Transformer<Stream<T>, A, R> { > > @Override > Supplier<A> supplier(); > > BiConsumer<A, T> accumulator(); > > BinaryOperator<A> combiner(); > > @Override > Function<A, R> finisher(); > > Set<Characteristics> characteristics(); > > enum Characteristics { > CONCURRENT, > UNORDERED, > IDENTITY_FINISH > } > > @Override > default BiConsumer<Stream<T>, A> transformer() { > return (stream, intermediary) -> { > var accumulator = accumulator(); > stream.forEachOrdered(t -> accumulator.accept(intermediary, t)); > }; > } > > } > >> >> Kind regards, >> >> Patrick >> >>> On 25 Jun 2020, at 18:12, Anthony Vanelverdinghe <d...@anthonyv.be> >>> <mailto:d...@anthonyv.be> wrote: >>> >>> Hi >>> >>> Given the signature of flatMap is: >>> <R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> >>> mapper) >>> >>> I'd like to propose the following signature for the new method: >>> <R> Stream<R> builderMap(BiConsumer<? super T, ? super Stream.Builder<R>> >>> mapper) >>> >>> This way both methods are named "...Map", and the name "builderMap" follows >>> naturally from the argument's type. >>> If the given mapper invokes Stream.Builder::build, an IllegalStateException >>> should be thrown. >>> >>> Kind regards, >>> Anthony >>> >>> On 25/06/2020 02:58, Paul Sandoz wrote: >>>> Hi, >>>> >>>> We traded CPS style for reusing existing functional interfaces. Originally >>>> the signature (my first choice) was as you indicate. >>>> >>>> By chance it just so happens that the current signature is the same shape >>>> as that for the accumulating argument type of the three arg collect >>>> terminal operation: >>>> >>>> Stream >>>> <R> R collect(Supplier<R> supplier, >>>> BiConsumer<R, ? super T> accumulator, >>>> BiConsumer<R, R> combiner); >>>> >>>> IntStream >>>> <R> R collect(Supplier<R> supplier, >>>> ObjIntConsumer<R> accumulator, >>>> BiConsumer<R, R> combiner); >>>> >>>> Same for the accumulator of a Collector too. >>>> >>>> However, I suspect you would argue these terminal accumulation cases are >>>> different from the intermediate case, as we are not accumulating but >>>> passing or accepting (loosely returning) zero or more elements that >>>> replace the input element. >>>> >>>> It’s my hope that generic specialization will allow the primitive stream >>>> types to fade into the background, along with the primitive functional >>>> interfaces. In that respect the addition of three functional interfaces >>>> for use on the primitive stream types is not so terrible. >>>> >>>> >>>> Regarding the name, you should have seen the first one :-) it was terrible. >>>> >>>> Here’s my few brush strokes on the bike shed. I wonder what people think >>>> of mapAccept. The specification talks about accepting elements, because >>>> that is the operative method name on Consumer. So we can say "T is >>>> replaced with the elements accepted by the Consumer<R>", or “ The >>>> Consumer<R> accepts the elements that replace T" >>>> >>>> Paul. >>>> >>>> >>>> >>>>> On Jun 24, 2020, at 1:01 PM, John Rose <john.r.r...@oracle.com> >>>>> <mailto:john.r.r...@oracle.com> wrote: >>>>> >>>>> I like this new API point a lot; it allows flexible, local, temporary >>>>> control inversion in the context of one stream transform. >>>>> >>>>> What’s the performance model? It seems like the SpinedBuffer >>>>> implementation makes a worst-case assumption about the number >>>>> of pending values, that there will be many instead of zero or one. >>>>> >>>>> But I guess the pipeline stuff already works in terms of pushes, so >>>>> the good news might be that this is really just a drill-down from the >>>>> user API into the kinds of operations (push-flavored) that go on >>>>> most of the time. >>>>> >>>>> OK, so I like the function but I have a beef with its bike shed >>>>> color. First of all, this is a control-inversion (CPS) pattern, >>>>> which is very powerful but also notoriously hard to read. >>>>> I think that in Java APIs, at least in Stream APIs, code is >>>>> easier to read if the logical data flow is from left to right. >>>>> >>>>> (This is a language-specific observation. Apart from varargs, >>>>> Java method APIs read favorably when extra control arguments >>>>> are added onto the end of the argument list. Also, the convention >>>>> for generic functional interfaces is that the return value type >>>>> goes to the right, e.g., R in Function<A,R>.) >>>>> >>>>> So the BiConsumer is backwards, because the logical return >>>>> should be written, if not as a true return (which would appear >>>>> at the end of type parameter lists), at the end of the incoming >>>>> parameters (and in the last type parameter). >>>>> >>>>> I also think “multi” is needlessly “learned” sounding. A simple >>>>> spatial preposition would work well: mapThrough, mapAcross, etc. >>>>> I think I prefer mapAcross because the term “across” can be read >>>>> adverbially: “we are mapping T across to Consumer<R>”. >>>>> >>>>> So: >>>>> >>>>> mapAcross(BiConsumer<? super T, Consumer<R>> mapper) >>>>> mapAcrossToInt(BiConsumer<? super T, IntConsumer> mapper) >>>>> mapAcross(IntObjConsumer<IntConsumer> mapper) >>>>> >>>>> This does require additional FI’s like IntObjConsumer, but >>>>> I think that is a negligible cost. Making the control inversion >>>>> *readable* is the high order bit here, not minimizing the number >>>>> of trivial FIs. >>>>> >>>>> (I almost hear David Bowman, in his space suit, saying, “My API… >>>>> It’s full of bikesheds!” There’s a meme for that.) >>>>> >>>>> — John >>>>> >>>>> On Jun 24, 2020, at 3:57 AM, Patrick Concannon >>>>> <patrick.concan...@oracle.com> <mailto:patrick.concan...@oracle.com> >>>>> wrote: >>>>>> Hi, >>>>>> >>>>>> Could someone please review myself and Julia's RFE and CSR for >>>>>> JDK-8238286 - 'Add new flatMap stream operation that is more amenable to >>>>>> pushing’? >>>>>> >>>>>> This proposal is to add a new flatMap-like operation: >>>>>> >>>>>> `<R> Stream<R> mapMulti(BiConsumer<Consumer<R>, ? super T> mapper)` >>>>>> >>>>>> to the java.util.Stream class. This operation is more receptive to the >>>>>> pushing or yielding of values than the current implementation that >>>>>> internally assembles values (if any) into one or more streams. This >>>>>> addition includes the primitive variations of the operation i.e. >>>>>> mapMultiToInt, IntStream mapMulti, etc. >>>>>> >>>>>> issue: https://bugs.openjdk.java.net/browse/JDK-8238286 >>>>>> <https://bugs.openjdk.java.net/browse/JDK-8238286> >>>>>> <https://bugs.openjdk.java.net/browse/JDK-8238286> >>>>>> <https://bugs.openjdk.java.net/browse/JDK-8238286> >>>>>> csr: https://bugs.openjdk.java.net/browse/JDK-8248166 >>>>>> <https://bugs.openjdk.java.net/browse/JDK-8248166> >>>>>> <https://bugs.openjdk.java.net/browse/JDK-8248166> >>>>>> <https://bugs.openjdk.java.net/browse/JDK-8248166> >>>>>> >>>>>> webrev: >>>>>> http://cr.openjdk.java.net/~pconcannon/8238286/webrevs/webrev.00/ >>>>>> <http://cr.openjdk.java.net/~pconcannon/8238286/webrevs/webrev.00/> >>>>>> <http://cr.openjdk.java.net/~pconcannon/8238286/webrevs/webrev.00/> >>>>>> <http://cr.openjdk.java.net/~pconcannon/8238286/webrevs/webrev.00/> >>>>>> specdiff: >>>>>> http://cr.openjdk.java.net/~pconcannon/8238286/specdiff/specout.00/overview-summary.html >>>>>> >>>>>> <http://cr.openjdk.java.net/~pconcannon/8238286/specdiff/specout.00/overview-summary.html> >>>>>> >>>>>> <http://cr.openjdk.java.net/~pconcannon/8238286/specdiff/specout.00/overview-summary.html> >>>>>> >>>>>> <http://cr.openjdk.java.net/~pconcannon/8238286/specdiff/specout.00/overview-summary.html> >>>>>> >>>>>> >>>>>> Kind regards, >>>>>> Patrick & Julia