ScopedValue: Capturing current bindings

Attila Kelemen attila.kelemen85 at gmail.com
Tue May 23 14:07:36 UTC 2023


I was contemplating on a dependency injection framework built on
scoped values. For the sake of simplicity, I will make it trivial and
unoptimized here. Consider the following as our mini limited DI
framework:

```
ScopedValue<Map<Class<?>, Supplier<?>>> FACTORIES
    = ScopedValue.newInstance();

<T> void withBinding(
    Class<T> type,
    Supplier<T> supplier,
    Runnable task
) {
  var prevValue = FACTORIES.orElse(Collections.emptyMap());
  var newValue = new HashMap<>(prevValue);
  newValue.put(type, supplier);

  ScopedValue.where(FACTORIES, newValue, task);
}

<T> T getByType(Class<T> type) {
  Supplier<?> provider = FACTORIES
      .orElse(Collections.emptyMap())
      .get(type);
  if (provider == null) {
    throw new IllegalStateException();
  }
  return type.cast(provider.get());
}
```

A trivial use of such framework would look like this:

```
withBinding(String.class, () -> "Hello!", () -> {
  System.out.println(getByType(String.class));
});
```

This would print "Hello!" as expected. However, consider the following case:

```
ScopedValue<String> testValue = ScopedValue.newInstance();

ScopedValue.where(testValue, "OuterValue", () -> {
  withBinding(String.class, testValue::get, () -> {
    ScopedValue.where(testValue, "InnerValue", () -> {
      System.out.println(getByType(String.class));
    });
  });
});
```

While we would normally expect the injection to be uninfluenced by the
calling context (especially, since you could even use this for lazy
singletons). However, the above would obviously print "InnerValue",
even though that was not the case when calling `withBinding`.

This is because we can't capture the bindings (at least I don't know a
way). Not even StructuredTaskScope can be (ab)used for this, because
it would (rightfully) refuse the `fork`, if the current scoped value
context changed.

What I wish for is a way to capture the current scoped values, and
later execute code with the captured scoped values. For example, we
could have a `ScopedValues.withCurrentContext(Consumer<CapturedScopedValueContext>)`
(or similar , variants), where `CapturedScopedValueContext` could look
like this (simplified for clarity):

```
interface CapturedScopedValueContext {
  <T> T inContext(Callable<? extends T> task);
}
```

in which case, now we could create our safe `withBinding` variant
relying on the current unsafe version:

```
<T> void withBindingSafe(
    Class<T> type,
    Supplier<T> supplier,
    Runnable task
) {
  ScopedValues.withCurrentContext(context -> {
    withBinding(type, () -> context.inContext(supplier::get), task);
  });
}
```

Replacing `withBinding` with `withBindingSafe` in our "complicated"
use-case, we would receive "OuterValue" as intended.

I hope we can have something like this or similar feature (obviously
not in JDK 21) enabling this use-case.

Attila


More information about the loom-dev mailing list