Scope Locals

John Rose john.r.rose at oracle.com
Wed Mar 31 00:00:29 UTC 2021


On Mar 30, 2021, at 6:33 AM, Andrew Haley <aph at redhat.com> wrote:
> 
> In Project Loom, we've been looking at scope-local variables.
> 
> The idea is to support something like a "special variable" in
> Common Lisp. This is a dynamically-scoped variable which acquires a
> value on entry to a lexical scope, and when that scope terminates, its
> previous value (or none) is restored. We also intend to support thread
> inheritance for scope locals, so that parallel constructs can easily
> set a value in the outer scope before threads start.

This is very good.  I support this goal as a way to escape from
the corner we’ve painted ourselves into with ThreadLocal.

And I agree Common Lisp gives a good mental model, at
least as a starting point.  There are aspects of the CL model
I’d like to question carefully before importing.  The two
main ones are (a) surface syntax for variable access:
“naked” variable names are the access syntax for a CL
special-variable, but that’s not appropriate for Java,
because it impinges on the JLS which is managed very
conservatively.  Also (b) do we need special variables to
be side-effectible?  That imports costs; it would be better
if they were “effectively final”, with the usual provisions
for escaping that limitation explicitly (add an indirection
to a non-final variable).

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

This isolation is true of both assignments and bindings.  I’d
rather outlaw assignments completely (see above).

> We'd like to implement this without any changes to the Language or
> Virtual Machine specifications, so a language-centric design like
> this isn't going to happen:
> 
>    scopeLocal MyType x;
> 
>    {
>        x = value;
>        ... code that eventually calls foo()
> 
>    }
> 
>    void foo() {
>        ... code that uses x ...
>    }
> 
> 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 ...)
>  }

Perhaps you meant to say “code that uses x.get() and y.get()”?

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

This raises the question of separation of concerns:
What linkage *does* a scoping link (term here??) have to
threads, apart from the “in” call and the “get” call?

More fundamentally, what is the type returned by
ScopeLocal.set(…)?  Is it a ScopeLocal, or is it a scoping
link (type TBD), or are those one and the same?

Can I say:

var slink = ScopeLocal.set(x, xinitval);

and then call a non-static set on it:

slink = slink.set(y, yinitval);

Can I store that “slink” in data structure for a week
before accessing it in another thread, like this:

slink.set(z, zinitval).in(() -> …code that calls get on x,y,z…)


> We'll still
> enforce the rule that bindings cannot be *used* except inside the in()
> method.

OK, that means you can’t say this:

var slink = ScopeLocal.set(x, xinitval).set(y, yinitval);
var xval = slink.get(x);
var yval = slink.get(y);

But you could say this:

var xval = slink.in(x::get);

So is there any gain from disallowing the previous API
usages?

> There are some disadvantages to this approach, in particular that this
> method chaining can result in the creation of a bunch of temporary
> objects. Insisting on the use of a Lambda (a Runnable or a Callable)
> is, however, necessary in order strictly to enforce the property that
> a binding acquires a value on entry to a lexical scope, and when that
> scope terminates, its previous value (or none) is restored.

Yes; it’s more reliable than using a TWR construct, and
that means the JVM can optimize more “intensely”.

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

For bikeshed colors: I think the functional version of “setX”
can often be usefully called “withX”.  So it’s get/set for
mutable objects, but get/with for immutable ones.
Your scope-link object seems to be immutable in its
basic structure, although a “set” method could be
applied to side-effect leaves that are already present
due to a let/bind/with call if you disagree with my
point (b) above.  (As in Common Lisp, there are two
kinds of potential changes:  Making a new binding,
and changing the value of an existing binding.
That latter, if it is allowed, deserves a “set” name
rather than a “with” name.)

> Thank you for reading this far; comments and questions welcome.
> 
> -- 
> 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