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