ScopedValue polyfill follow-up [try-with-resources]
Chapman Flack
chap at anastigmatix.net
Mon Mar 31 17:59:09 UTC 2025
Hi,
On 02/09/25 06:58, hrgdavor at gmail.com (Davor Hrg) wrote:
> OT Context can be used as autocloseable in try with resources.
> ScopedValue is from what I can see meant to be wrapped in Runnable or
> Callable.
>
> My concern may be misguided, but I do want to ask if there is any overhead
> to worry about with creating a lambda to create a scope,
>
> runWhere(CTX, value,()->{
> //do something
> });
>
> versus try with resources
>
> try (Scope ignored = Context.current().with(CTX.KEY, value).makeCurrent()) {
> //do something
> }
I would like to add my voice in favor of also offering a try-with-resources
friendly method. My concern is not with overhead, but simply with enabling
more flexibility in API design around the new feature.
I am very pleased with the addition of the CallableOp functional interface
in the third preview. Unless I messed up my quick search in jshell just now,
even in Java 24, CallableOp still seems to be the very first, solitary,
only, public, unqualified-exported, functional interface with a generic
thrown exception type in the whole transitive closure of java.se.
That means even if there were nothing else good about scoped values,
just having CallableOp land as a standard functional interface in Java
immediately makes cleaner designs possible when designing APIs for other
things. (Maybe it could even be introduced in a less obscure place,
right in java.util.function, perhaps?)
While it's easy enough to roll my own such interface and I've done so
for countless internal uses, I've always been reluctant to expose it in
an API that I design, because then I'd just be contributing to a
proliferation of APIs expecting differently-named equivalent functional
interfaces while waiting for Java to provide a standard one, and consigning
anyone who needs make two or more such APIs interoperate to the drudgery
of converting one lambda to another via method reference.
I note in passing that there is still an opportunity for Java to provide
a subinterface of AutoCloseable with a generic thrown exception type.
My search in Java 24 this morning turned up exactly zero of those. Again,
I've rolled those too for my own use but never wanted to clutter an API
with a one-off version.
When scoped values land, CallableOp will not be only convenient for code
directly manipulating scoped values. It will also allow API design where
library A can pass a CallableOp to library B to be executed in a certain
mode of B's operation, which B may well implement using a scoped value
under the hood but that's none of A's business.
The trouble is, in polyfilling such a design before non-preview scoped
values land, there isn't yet any available standard functional interface
that can stand in for CallableOp. A polyfill would have to roll its own
and there would be disruption in switching later.
And that is why I would like to suggest offering the choice of two modes
of operation for scoped values:
ScopedValue.where(V, foo).call(() -> {
... do ...
... some ...
... work ...
return thing;
});
or
try (var _ = ScopedValue.where(V, foo).makeCurrent() ) {
... do ...
... some ...
... work ...
result = thing;
}
The two are roughly equivalent in convenience and expressiveness, and
could easily both be offered. (I've used Davor Hrg's name makeCurrent
above, but others might work ... inScope() ?)
But while it is trivial to implement an API that works the first way
on top of one that works the second way, it's impossible to do the reverse.
If there is now some library B whose API for "be in such-and-such mode
for this bit of work I have to do" already has the first pattern, and
that library now wants to migrate to using ScopedValue under the hood,
it easily can. Even if B's API exposes some differently-named functional
interface to the caller, it can easily cast a method reference to
CallableOp under the hood and no client code disruption results.
On the other hand, if there is a library whose API for "be in such-and-such
mode for this bit of work" already has the second pattern, and now that
library wants to migrate to using ScopedValue under the hood ... IT CAN'T.
And in the present state of affairs, I would suspect that more existing
libraries use that second pattern than the first, because of the long-
standing lack of a standard functional interface like CallableOp.
Some discussion I've seen online elsewhere suggested there might have
been a concern about supporting a try-with-resources pattern because
nothing actually forces the programmer to call the method in a try-
with-resources, and that could make it 'unsafe'.
But it seems to me that 'safety' divides into at least three categories:
1. Safe - this construct can't be used in any way that would let
Bad Things happen. The call(CallableOp) pattern fits here.
2. Unsafe - this construct's very existence could allow Bad Things
to get you even if you use it right. Nothing proposed fits here.
3. So just do it right. Yeah, Bad Things could happen if you don't.
But you're the one writing the code, you know what a try-with-resources
looks like, and you're happy when your code works, so you'll do it right.
Case 3 to me seems benign enough that I wouldn't use it as a reason
to make one whole pattern of API design unimplementable over scoped values.
I was not able to find a lot of earlier discussion in the list archives
(I've only looked back a year). I did see a point made that a ScopedValue
.get() can now be assured to produce the same value anywhere in a method,
where a try-with-resources option would allow it to have a different
value within a try block. That may perhaps complicate optimization, but
maybe not insurmountably? In the archived discussion I saw, there was
some talk of allowing arbitrary in()/out() of the scope, but I do not
propose that. All I suggest is returning an AutoCloseable subtype with
one close() method that closes it once and for all.
There may also be a simplifying consideration. In the example I outlined
above where a library B uses scoped values under the hood, and may wrap
a ScopedValue AutoCloseable in some AutoCloseable of its own that it
returns to its caller A for use in a try-with-resources, then yes,
different bytecode ranges in that A method may correspond to different
scoped values for B. But chances are A has no access to B's scoped values
anyway. And in any method of B called within that try block in A,
B's scoped values will be unchanging.
Thanks for considering my suggestion, and I hope it has contributed
something constructive to the discussion.
Regards,
Chapman Flack
More information about the loom-dev
mailing list