Do built-in generic types and my own generic types behave the same when I pass a callback over a collection?
← Generics · Ref: Q1373
Yes. From the point of view of the code that USES them, a built-in generic (List, Optional, Result - implemented in the runtime) and a generic you define yourself behave identically, including when the element is itself a parameterised type such as List of String. You call them the same way and the callback receives the same typed value. The backends implement them differently, but that difference never leaks to you.
YOUR OWN GENERIC (needs two constructors - see Q643)
Holder of type T held as T? Holder() as pure held :=? T() Holder() as pure -> initial as T held :=? initial whenPresent() -> consumer as Consumer of T if held? consumer(held) default operator ?
USING BOTH THE SAME WAY
words <- List() of String words += "widget"
h <- Holder(words) // user generic hc <- (stdout) is Consumer of List of String as pure function stdout.println(`user-generic: ${$t}`) h.whenPresent(hc)
opt <- Optional(words) // built-in generic oc <- (stdout) is Consumer of List of String as pure function stdout.println(`built-in: ${$t}`) opt.whenPresent(oc)
Both print the same list. The callback is 'Consumer of List of String' in both cases; both hand you the typed List. This surface parity is the guarantee: pick a built-in generic or write your own, and the calling code, the callback shapes, and the tri-state behaviour are the same.
See Q643 for the two-constructor rule. See Q198 for built-in generics. See Q1372 for callbacks over a container's collection.
Example
defines module qa.genericsdeep.callbackparity defines class Holder of type T held as T? Holder() as pure held :=? T() Holder() as pure -> initial as T held :=? initial whenPresent() -> consumer as Consumer of T if held? consumer(held) default operator ? defines program CallbackParityDemo() stdout <- Stdout() words <- List() of String words += "widget" // User-defined generic (Scenario 3 - monomorphised directly) h <- Holder(words) hc <- (stdout) is Consumer of List of String as pure function stdout.println(`user-generic: ${$t}`) h.whenPresent(hc) // Built-in generic (Scenario 2 - delegating wrapper) - same call, same result opt <- Optional(words) oc <- (stdout) is Consumer of List of String as pure function stdout.println(`built-in: ${$t}`) opt.whenPresent(oc)
Common mistakes
E06040 — A generic class needs both a public no-arg constructor and the value constructor (E06040 / E06060). Provide both so the type can be default-constructed and value-constructed. 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
- Is Optional of List of String the same to use as my own Holder of List of String?
- Scenario-2 built-in generic versus Scenario-3 user generic surface parity
- Will my generic class behave like the built-in ones for whenPresent-style callbacks?
- Do user-defined generics and library generics feel the same?
Coming from another language?
Java: a library Optional<T> and a user-defined generic Box<T> both use the same call syntax; type erasure means neither preserves T at runtime. EK9: user and built-in generics share the same call syntax AND the compiler preserves the parameterised element type through callbacks for both - the built-in (delegating) and user (monomorphised) implementations are indistinguishable to the caller.
Keywords: Consumer, whenPresent, generic, parity, Holder, callback, Optional, user-defined, scenario2, surface, scenario3, built-in, monomorphization