What are the two safe ways to write a generic class that has an optional 'T?' field?

← Generics · Ref: Q1348

A generic 'T?' field is uninitialised at declaration, so EK9 forces you into one of exactly two safe shapes - both eliminate the null at compile time.

SHAPE 1: PRESENT-BUT-UNSET (public no-arg, give the field a T())
Use when T is guaranteed to have a public no-argument constructor (String, Integer, your own value types). Empty construction is allowed; the field is an unset T, never null:

  Box of type T
    content as T?
    Box() as pure
      content :=? T()
    Box() as pure
      -> initial as T
      content :=? initial
  empty <- Box() of String     // allowed; content is unset, '?' is false

SHAPE 2: NO EMPTY CONSTRUCTION (default private)
Use when T() is not available (T may be abstract, hold a function, or lack a public no-arg constructor). The no-arg default is private, so no empty instance can be created; callers must pass a value:

  Box of type T
    content as T?
    default private Box() as pure
    Box() as pure
      -> initial as T
      content :=? initial
  filled <- Box(someValue)     // only way to construct - always has a value

WHAT IS NOT ALLOWED

A public 'default Box()' with a 'T?' field is E07175 - it would leave the field null. Pick shape 1 or shape 2.

The difference is a deliberate design choice: shape 1 says 'an empty one is meaningful (and unset)', shape 2 says 'there is no meaningful empty one'.

See Q1346 for the E07175 error. See Q1347 for the private escape hatch. See Q643 for the two-constructor requirement.

Example

defines module qa.genericsdeep.twosafeshapes

  defines class

    //Shape 1: present-but-unset - an empty one is meaningful
    Box of type T
      content as T?

      Box() as pure
        content :=? T()

      Box() as pure
        -> initial as T
        content :=? initial

      default operator ?

    //Shape 2: no empty construction - there is no meaningful empty one
    Required of type T
      content as T?

      default private Required() as pure

      Required() as pure
        -> initial as T
        content :=? initial

      default operator ?

  defines program

    TwoSafeShapesDemo()
      stdout <- Stdout()

      emptyBox <- Box() of String
      stdout.println(`empty box set: ${emptyBox?}`)

      filledBox <- Box("value")
      stdout.println(`filled box set: ${filledBox?}`)

      onlyWithValue <- Required("must have")
      stdout.println(`required set: ${onlyWithValue?}`)

Common mistakes

E07175 — A public default constructor with a 'T?' field leaves it null (E07175). Choose shape 1 (content :=? T()) or shape 2 (default private). See ek9 -h E07175 for details.

Incorrect:

default Box() as pure

Correct:

Box() as pure
        content :=? T()
Other ways to ask this
  • How should a generic optional field be initialised in EK9?
  • Default-value generic field versus no-empty-construction generic field
  • Generic class with a nullable field done safely

Coming from another language?

Java/Kotlin: nothing forces a choice - the generic field is null until set and the risk is silent. Rust: Option<T> is always explicit. EK9: the compiler forces shape 1 (present-but-unset via T()) or shape 2 (no empty construction via default private), so a generic optional field is never a hidden null.

Keywords: default, T?, field, two, optional, null-safety, shapes, private, design, generic, present-but-unset