Scope Locals

Brian Goetz brian.goetz at oracle.com
Sat Apr 3 14:27:46 UTC 2021


Thanks for writing this up, Andrew.  This is looking very nice.

> One useful way to think of scope locals is as invisible parameters
> which are passed to every method. However, we will only allow values to
> propagate from caller to called methods: assignments to a scope local
> variable in a called method will not be observed in the calling method,
> because when a scope terminates, scope locals set in that scope will
> have their previous values restored.

Indeed, "implicit parameter" is the model we're going for.  In addition 
to the other issues with ThreadLocal you mention, this model is stricter 
than that of ThreadLocal, because ThreadLocal is a mutable cell and 
callees can mutate that cell such that mutations are visible after the 
callee returns.  Of course, we can try to enforce some discipline on use 
of TLs to enforce scoping, but this is usually something we only succeed 
to the 99% level, with much debugging fun ensuing.

You allude to this obliquely, but let's say it outright -- the 
caller-callee edges need not be restricted to plain vanilla invoke* 
operations, but could also be the creation/starting of a virtual 
thread.  (Presumably this is controlled with some thread-factory 
parameter as to whether scope locals are inherited or not.)  Virtual 
threads are cheap enough that we can consider starting a VT to be an 
"asynchronous call" and it is reasonable, under some cases, to inherit 
some context from the "caller" in this case.  (Legacy TLs and thread 
pools have always been a bad mix, but because we're not going to be 
reusing VTs like we did platform threads, this won't be an issue.)

> I've been kicking around various ideas and implementations of scope
> locals for some time, and I've now got something to share with the
> wider world, somewhat inspired by Haskell syntax, which looks
> something like.
>
>      let x = expr1
>          y = expr2
>      in
>          ... expression that uses x, y ...
>
> Translated into something a bit more Java-esque, that looks like
>
>    // Declare to scope locals x and y
>    static final ScopeLocal<MyType> x = ... ;
>    static final ScopeLocal<MyType> y = ... ;
>
>    {
>      ScopeLocal.set(x, expr1)
>                .set(y, expr2)
>                .in(() -> ... code that uses x and y ...)
>    }

This looks like a good API direction for this.  It scales to multiple 
SLs, and makes it exceedingly clear what their scope is.  I assume that 
if you're only setting one, you might also have a simpler form for

     ScopeLocal.with(x, expr, () -> code)

that doesn't produce an intermediate carrier.

> I quite like the way this looks, and it allows for some interesting
> and useful variants. In particular, the result of the "binding
> expression" (a list of key/value pairs):
>
>      ScopeLocal.set(x, expr1)
>                .set(y, expr2)
>
> can be passed to threads, saved for later use, and so on. We'll still
> enforce the rule that bindings cannot be *used* except inside the in()
> method.

Yes, this offers some flexibility.  Some obvious questions, which 
probably have obvious answers:

  - Presumably the set() method doesn't actually set anything, but 
instead sets up a pair which will be bound before calling the in()
  - Presumably the result of SL.set() is a "carrier" that encapsulates a 
chain of bindings
  - Presumably the carrier can be reused:

     ScopeLocalCarrier sc = ScopeLocal.set(x, e1).set(y, e2);
     sc.in(x);
     sc.in(y);

> I'm not entirely sure about the naming here. set() is given a
> different meaning from usual, and I'm not sure anyone would let me get
> away with using let(). in() is slightly odd too, and perhaps something
> more verbose would be better.
>
> Other possibilities are e.g.
>
>      ScopeLocal.bind(x, expr1)
>                .bind(y, expr2)
>                .exec(() -> ... code that uses x and y ...)

This naming is a definite improvement over the version above, for the 
reasons you cite.  The let ... in might have been the mental model that 
kicked off this line of thinking, but the terminology may not come along 
for the ride.  Perhaps `withBinding(sl, expr)`, which evokes a little 
bit of "save this for later" or "use this in that context."

This model will surely fit nicely into whatever structured concurrency 
APIs we eventually layer atop VTs.




More information about the loom-dev mailing list