I agree with the idea of a try() syntax, but i don't think we need more 
interfaces.

Yes, John is right about the fact that the TWR Aucloseable does not work well 
with checked exceptions,
but the issue is more that there is nothing that works well with checked 
exceptions because Java has no way to compose them correctly
(remainder: we should remove the idea of checked exceptions from the language 
given that it's a backward compatible change and Kotlin, C# are safer than Java 
because users of those languages are not used to write a 
try/catch/printStackTrace in those languages unlike in Java).

So for me, AutoCloseable is enough.

The problem is more than a fluent API are expression oriented and a 
try-with-resources is statement oriented, hence the mismatch.

Like we have introduced the switch expression, here we need a 
try-with-resources expression.

There are good reasons to have a try-with-resources expression
1) it can be used to track a fluent chain that should close() the resources
2) the compiler can guarantee that the stream/reader/etc around the resources 
does not leak outside.

In term of syntax, i don't think that the arrow syntax (the one used for a case 
of a switch case or a lambda) should be used here, because using a block 
expression seems to be always a mistake.

I like the proposal from John, a try(expression) or a try expression (without 
the parenthesis).

regards,
Rémi


----- Original Message -----
> From: "John Rose" <john.r.r...@oracle.com>
> To: "Paul Sandoz" <paul.san...@oracle.com>, "Brian Goetz" 
> <brian.go...@oracle.com>, "Tagir F.Valeev"
> <tval...@openjdk.java.net>
> Cc: "core-libs-dev" <core-libs-dev@openjdk.java.net>
> Sent: Lundi 11 Octobre 2021 20:42:20
> Subject: Re: RFR: 8274412: Add a method to Stream API to consume and close 
> the stream without using try-with-resources

> So the purpose of TWR is to hold an object with a “close-debt”
> (debt of a future call to close) and pay it at the end of a block,
> sort of like C++ RAII (but also sort of not).
> 
> But fluent syntaxes (which I like very much and hope to see
> more of in the future!) don’t play well with blocks, so if a
> fluent chain (any part of that chain:  It’s multiple objects)
> incurs a “close-debt”, it’s hard to jam a TWR block into it.
> 
> Hence the current proposal.  I agree with Brian and Paul
> that we haven’t examined all the corners of this problem
> yet.  And I’d like to poke at the concept of “close-debt” to
> help with the examination.
> 
> Just for brain storming, I think we could model “close-debt”
> outside either fluent API structure or TWR block structure.
> Java O-O APIs are the pre-eminent way to model things in
> Java, and they work exceedingly well, when used with skill.
> 
> AutoCloseable models close-debt of course.  But it has two
> weaknesses:  It doesn’t model anything *other* than the
> debt, and its (sole) method skates awkwardly around the
> issue of checked exceptions.  (It requires an override with
> exception type narrowing to be used in polite company.)
> AC is more of an integration hook with TWR, rather than
> a general-purpose model for close-debt.  Therefore it doesn’t
> teach us much about close-debt in a fluent setting.
> 
> Surely we can model close-debt better.  Let’s say that an
> operation (expression) with close-debt *also* has a return
> value and (for grins) *also* has an exception it might throw.
> This gets us to an API closer to Optional.  (If you hear the
> noise of a monad snuffling around in the dark outside
> your tent, you are not wrong.)
> 
> interface MustClose_1<T,X> {
>   T get() throws X;  //or could throw some Y or nothing at all
>   void close() throws X;
> }
> 
> (I wish we had an easier way to associate such an X
> with such a T, so that Stream<T throw X> could be more
> interoperable with simple Stream<T>.  It’s a pain to
> carry around those X arguments.  So I’ll drop X now.)
> 
> interface MustClose_2<T> {
>   T get();
>   void close() throws Exception;
> }
> 
> An expression of this type requires (in general) two
> operations to finish up:  It must be closed, and the result
> (type T) must be gotten.  There’s an issue of coupling between
> the two methods; I would say, decouple their order, so that
> the “get” call, as with Optional, can be done any time,
> and the “close” call can be done in any order relative
> to “get”.  Both calls should be idempotent, I think.
> But that’s all second-order detail.
> 
> A first-order detail is the apparent but incorrect 1-1 relation
> between T values and close-debts.  That’s very wrong;
> a closable stream on 1,000 values has one close-debt,
> not 1,000.  So maybe we need:
> 
> interface MustClose_3<T,U,S> {
>   S map(Function<T,U> value);
>   void close() throws Exception;
> }
> 
> That “map” method looks a little like Remi’s apply
> method.  Did I mention this design requires skill
> (as well as flexibility, with one hand already tied
> by checked exceptions)?  I’m at the edge of my own
> skill here, but I think there’s good ground to explore
> here.
> 
> In a fluent setting, a Stream<T> that incurs a close-debt
> might be typed (after incurring the debt, perhaps in a
> transform) as Stream<MustClose<T>>, and somehow
> all consumers of the MustClose<T>, such as map and
> collect operations on the Stream, would correctly
> unpack each T from the MC<T>, and then repack
> the result into the MC<.> wrapper.
> 
> var res = openAFileStream().map(…).collect(…);
> 
> Here the first method call returns a Stream with
> close-debt mixed into its type.  The map and collect
> calls would wire both parts:  The T values flowing
> through, and the close-debt.  Who takes responsibility
> for paying the close debt?  Maybe an extra call
> at the end:  …map(…).collectAndClose(…).
> Or maybe the stream “knows” internally that since
> its type has a close debt, all of its terminal operations
> have to pay off that debt as they collect the payloads.
> So it would be automatic, somehow, inside of
> collect, forEach, etc.
> 
> To make the parts hook up right, you might need
> reified generics, or maybe an amended type
> MustCloseStream<T> <: Stream<MustClose<T>>,
> like the LongStream <: Stream<Long> we dislike.
> I’m only proposing as a thought exercise for now.
> 
> Maybe the MustCloseStream takes an explicit close
> method which produces a regular stream over the
> base type.  The explicit close method would release
> resources and buffer anything necessary to produce
> an in-memory Stream<T>.  You’d want to call it
> late in the fluent chain, after filtering and flat-mapping
> is done, just before a collect for forEach.
> 
> Here’s a streamlined version of MustClose that
> I like, which sidesteps the problem of mutual ordering
> of two methods:
> 
> interface MustClose_4<R> {
>   R getAndClose() throws Exception;
>   default void close() throws Exception { getAndClose(); }
> }
> 
> Here, R is not an element type of a stream, but rather
> the final result (maybe Void) of some terminal operation.
> 
> Such an interface could interact with syntax, too.  For
> example, it might help with TWR expressions (if we
> wanted to think about that):
> 
>    var res = try (expr);
>    // sugar for:
>    X res; try (MustClose_4<X> $t = expr) { res = $t.getAndClose(); };
> 
> It might help with other auto-close notations or APIs we could
> cook up.  For example, suppose “var x=y” were artificially restricted
> from inferring a MustClose<T> type from y onto var x.  Instead,
> the var type would be inferred as T, and a getAndClose call would
> be added to the initializer (y) to unwrap the value before binding
> to x.  Perhaps that would stretch “var” too far; maybe we would
> choose to mark such “var”s specially, as “try var x = y;”.  My
> point here is that an O-O model of close-debt as a wrapper for
> other computations is helpful for evaluating our options.
> 
> (Another kind of wrapper for try-var would be an exception
> wrapper, a direct-sum thingy which holds *either* a normal
> T result or an exceptional X result.  That could model things
> we don’t have good models for today.  In “try var x = y;” if
> y is of type ValueOrException<T,X>, then var would be inferred
> from T, and any X present would pop out when the initializer
> were executed.  You could use such a type, and such a notation,
> to more easily wire exceptions through all sorts of combinators
> where exceptions are not welcome today.)
> 
> To summarize:  We can (and should) try to model “close-debt”
> using interfaces.  Doing so opens up the usual cans of worms
> with interoperability and exceptions, but still gives us a
> model we can contemplate.  We can (and should) contemplate
> how such a model would give us leverage for further syntax
> sugar and/or combinatorial API points for handling close-debt
> at various points in the language and our APIs.

Reply via email to