Why can't I reassign an injected component variable?

← DI Validation · Ref: Q669

An injected variable ('cache as CacheService!') is never assigned - not with ':=', and not with ':=?' either (E08170). The application declares which concrete component satisfies each abstract injection point; if code could reassign the variable, the wiring would become unpredictable.

AN INJECTED COMPONENT IS ALWAYS SET

The compiler proves that every injection a program can reach is registered in its application (E08210, E08220), so the value always arrives. That is also why ':=?' (assign only if unset) would never apply, and why checking it - 'if cache?', 'require cache?', 'cache ?: other', 'x <- cache? <- cache else other' - is itself an error (E08266): the check is always true.

THE FALLBACK IDIOM: A NEW VARIABLE

When something else may supply a component and the injected one is the default, the fallback runs the other way round - assign to a NEW variable, then fall back to the injected one:

  chosen <- supplied
  chosen :=? cache    // only if 'supplied' was unset

The example below passes a set and an unset component to such a function: the set one is used, the unset one falls back to the injected cache.

See Q667 for abstract injection. See Q668 for injectable contexts. See Q670 for field initialization.

Example

defines module qa.divalidation.reassignment

  defines component

    <?-
      Abstract cache contract.
    -?>
    CacheService as abstract

      lookup() as abstract
        -> cacheKey as String
        <- cachedValue as String?

      default operator ?

    <?-
      Simple in-memory cache implementation - unset when it has no prefix.
    -?>
    InMemoryCache extends CacheService
      prefix as String?

      InMemoryCache()
        prefix: String()

      InMemoryCache()
        -> keyPrefix as String
        prefix: keyPrefix

      override lookup()
        -> cacheKey as String
        <- cachedValue as String: `${prefix}${cacheKey}`

      default operator ?

  defines function

    <?-
      Use the supplied cache if it is set, else fall back to the injected one.
      The injected variable is never assigned: the fallback goes in a NEW variable.
    -?>
    chooseCache()
      -> supplied as CacheService
      <- chosen as CacheService?
      cache as CacheService!
      chosen: supplied
      chosen :=? cache

  defines application

    CacheApp
      register InMemoryCache("cached:") as CacheService

  defines program

    InjectionReassignmentDemo() with application of CacheApp
      stdout <- Stdout()

      cache as CacheService!
      directLookup <- cache.lookup("settings")
      stdout.println(`Lookup: ${directLookup}`)

      preferred <- chooseCache(InMemoryCache("preferred:"))
      stdout.println(`Preferred: ${preferred.lookup("theme")}`)

      fallback <- chooseCache(InMemoryCache())
      stdout.println(`Fallback: ${fallback.lookup("theme")}`)

Common mistakes

E08170 — Injected component variables are set by the application and are never assigned, with ':=' or with ':=?'. The application wiring defines which implementation satisfies each injection point, and manual reassignment would break that contract. See ek9 -h E08170 for details.

Incorrect:

      cache as CacheService!
      cache := InMemoryCache()
      directLookup <- cache.lookup("settings")

Correct:

      cache as CacheService!
      directLookup <- cache.lookup("settings")

E08266 — An injected component is always set, so 'cache?' is always true and the 'else' never runs. For a fallback, assign to a NEW variable and fall back to the injected one with ':=?'. See ek9 -h E08266 for details.

Incorrect:

      cache as CacheService!
      chosen: cache? <- cache else supplied

Correct:

      cache as CacheService!
      chosen: supplied
      chosen :=? cache
Other ways to ask this
  • What is E08170 reassignment of injected variable?
  • Can I change an injected dependency at runtime?
  • How do I handle optional injection?

Coming from another language?

Java: Spring injection is final by convention (not enforced), and optional injection (@Autowired(required = false)) can leave a null to check. Python: no enforcement. Go: manual wiring can be changed. Rust: ownership prevents aliasing. EK9: the compiler prevents any assignment to an injected variable, proves the injection always arrives, and rejects set-checks on it; a fallback goes in a new variable.

Keywords: fallback, null-safe, reassignment, E08170, injection, E08266, registration, isset, safe, component, immutable, inject, validate, DI