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