ScopedValue polyfill follow-up [try-with-resources]

Andrew Haley aph-open at littlepinkcloud.com
Tue Apr 1 09:46:07 UTC 2025


Thank you for your comments. Mine are inline.

On 3/31/25 18:59, Chapman Flack wrote:

 >> 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
 >> }

True, but I think that the C2 compiler can scalar replace the lambda
in many cases.

 > ... 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.

We did try this, and rejected it in favour of what we have now.

I guess you've read the scoped values JEP, which discusses
"Unconstrained mutability", "Unbounded lifetime" and "Expensive
inheritance" as disadvantages of thread-local variables. It goes on to
discuss the meaning of "scoped", and the way that, in effect, a scoped
value works as an invisible argument passed to all callees.

 > (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.

But thread-local variables are not going to go away. If that
unstructured style is preferred, then a thread-local variable is a
good choice.

 > 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.

That is by design. There is nothing in principle that scoped values
can do that can't be done by thread-local variables, albeit in a
somewhat more costly way, especially with regards to inheritance. The
_advantage_ that scoped values bring is that the structure is
enforced. This is true of all structured programming constructs: they
are less general purpose than if (x) goto label.

 > 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'.

Indeed. Scoped values have a property that thread-local variables
don't have, which is the invariant that once a binding scope exits,
the scoped value is no longer bound.

    var x = SV.get();
    callSomething();
    assert (! SV.isBound()) || SV.get() == x;  // Must succeed

Invariants make reasoning about and verifying Java programs easier.
See https://openjdk.org/jeps/8305968, "Integrity by Default," in
particular "Undermining integrity."

 > 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.

But it's not just you, the author of that library: scoped value
bindings are strictly ordered, so if we were to support the
try-with-resources style we'd have to add extra checking to make sure
that binding regions weren't closed out of order.

 > 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.

This is a somewhat philosophical point, but it has practical
consequences.

 From JEP draft: Integrity by Default

    "Abstraction enables us to create higher-level computing
    constructs; encapsulation enables us to imbue those constructs, and
    ultimately entire programs, with integrity.

    "Using encapsulation to imbue the class with integrity ensures that
    correctness cannot be undermined by code external to the class."

Scoped values offer something new that thread-local variables do not
have: a guarantee of integrity. If we were to offer support for Case
3, then scoped values would no longer have this property. As soon as
you support Case 3, scoped values have unbounded lifetimes.

-- 
Andrew Haley  (he/him)
Java Platform Lead Engineer
Red Hat UK Ltd. <https://www.redhat.com>
https://keybase.io/andrewhaley
EAC8 43EB D3EF DB98 CC77 2FAD A5CD 6035 332F A671



More information about the loom-dev mailing list