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