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

#!ek9
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: DI, E08170, E08266, component, fallback, immutable, inject, injection, isset, null-safe, reassignment, registration, safe, validate