Can I store my own generic type inside a built-in container like List, Optional, Result or Dict?

← Generics · Ref: Q1374

Yes. A user-defined generic (your own {@code Holder of T}, {@code Pair of (A, B)}, ...) can be the element, value, key, ok or error type of ANY built-in container - List, Set, Optional, Result, Dict, PriorityQueue, MutexLock - and it behaves exactly like a built-in element. You construct, store and retrieve it the same way; the compiler carries the parameterised element type through for you. The built-in container (delegating wrapper) and your user generic (monomorphised) are implemented differently, but that never leaks to your code.

USER GENERIC AS A LIST ELEMENT

  holders <- List() of (Holder of String)
  holders += Holder("alpha")
  for h in holders
    stdout.println(h.get())          // alpha

AS AN OPTIONAL VALUE

  maybe <- Optional(Holder(42))
  if maybe?
    stdout.println(`held: ${maybe.get().get()}`)   // held: 42

AS A DICT VALUE

  scores <- Dict() of (String, Holder of Integer)
  scores += DictEntry("a", Holder(1))

AS A RESULT OK TYPE

  outcome <- Result(Holder("ok"), 0)
  outcome.whenOk(consumer)           // consumer receives a Holder of String

NESTING BOTH WAYS ALSO WORKS

  Optional of (Holder of (List of String))   // user generic wrapping a built-in generic
  Optional of (Holder of (Holder of String)) // user generic wrapping a user generic

WHAT YOUR GENERIC NEEDS

Two constructors (Q643): a public no-arg and a value constructor. If you use it as a Dict KEY, a Set element, or a PriorityQueue element, default the operators those containers need - {@code default operator ==}, {@code default operator #?} (for keys/sets) and {@code default operator <=>} (for ordering).

See Q198 for built-in generics. See Q643 for the two-constructor rule. See Q1373 for built-in vs user generic surface parity.

Example

defines module qa.genericsdeep.usergenincontainer

  defines class

    Holder of type T
      held as T?

      Holder() as pure
        held :=? T()

      Holder() as pure
        -> initial as T
        held :=? initial

      get() as pure
        <- rtn as T?
        rtn: held

      default operator ?

  defines program

    UserGenericInContainerDemo()
      stdout <- Stdout()

      // User generic as a List element
      holders <- List() of (Holder of String)
      holders += Holder("alpha")
      holders += Holder("beta")
      for h in holders
        stdout.println(`list: ${h.get()}`)

      // User generic as an Optional value
      maybe <- Optional(Holder(42))
      if maybe?
        stdout.println(`optional: ${maybe.get().get()}`)

      // User generic as a Dict value
      scores <- Dict() of (String, Holder of Integer)
      scores += DictEntry("a", Holder(1))
      for entry in scores
        stdout.println(`dict: ${entry.value().get()}`)

      // User generic as a Result ok type
      outcome <- Result(Holder("ok"), 0)
      reporter <- (stdout) is Consumer of (Holder of String) as pure function
        stdout.println(`result: ${t.get()}`)
      outcome.whenOk(reporter)

Common mistakes

E06040 — A generic class used inside a container still needs both a public no-arg and a value constructor (E06040 / E06060). Provide both. See ek9 -h E06040 for details.

Incorrect:

Holder() as pure
        -> initial as T
        held :=? initial

Correct:

Holder() as pure
        held :=? T()

      Holder() as pure
        -> initial as T
        held :=? initial
Other ways to ask this
  • Put a user-defined generic as the element of a List or Optional
  • Use my own Holder of T as a Dict value or Result ok type
  • Nest a user generic inside a built-in generic
  • Optional of (my generic type)

Coming from another language?

Java/Kotlin: List<MyGeneric<String>> is routine; type erasure means the element type is not preserved at runtime. EK9: a built-in container over a user generic is equally routine AND the parameterised element type is preserved through storage and retrieval - your Holder of String comes back as a Holder of String, not an erased Object.

Keywords: user, Holder, value, built-in, Dict, generic, nest, Pair, element, Result, List, monomorphization, Optional, container, key, scenario3