When must a generic class use 'default private' for its no-argument constructor?

← Generics · Ref: Q1347

The idiomatic way to keep a generic's 'T?' field non-null is to give it an unset T() in the no-arg constructor ('item :=? T()'). But T() is not always available - it requires the concrete type used to parameterise the generic to have a public no-argument constructor. T() is NOT available when:
1. T may be ABSTRACT - an abstract type cannot be constructed (E10030).
2. T may hold a FUNCTION - a function cannot be constructed with T() (E06110).
3. T may have NO public no-arg constructor - for example a type whose own default is 'default private' (E10031).
4. T's no-arg constructor may be NON-pure while the generic calls 'T()' in a PURE constructor - a pure 'T()' demands a pure no-arg constructor on the parameterising type (E10032).

These constructor requirements (existence, public access, purity) are properties of the CONCRETE type, so they are only checked at parameterisation - not when the generic is defined.

In these cases, declare the no-arg default PRIVATE:

  Holder of type T
    item as T?
    default private Holder() as pure          // escape hatch - no T() needed
    Holder() as pure
      -> initial as T
      item :=? initial

WHAT 'default private' GIVES YOU
External code CANNOT create an empty Holder (the no-arg constructor is private), so it can never observe a null field. Callers must use the inferred constructor with a real value. The inferred (parameterised) constructor stays public so type inference still works.

DECISION RULE

- T always has a public no-arg constructor (String, Integer, your own value types): use 'item :=? T()' so empty construction is allowed and the field is present-but-unset.
- T might not (abstract bound, function, or no-arg-private types): use 'default private' and construct only with a value.

See Q1346 for the E07175 error. See Q1348 for the two safe shapes. See Q645 for the constructor access rule.

Example

defines module qa.genericsdeep.defaultprivateescapehatch

  defines class

    Holder of type T
      item as T?

      default private Holder() as pure

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

      get() as pure
        <- rtn as T: item

      default operator ?

  defines program

    EscapeHatchDemo()
      stdout <- Stdout()

      //Cannot do 'Holder() of String' here - the no-arg default is private.
      filled <- Holder("present")
      stdout.println(`filled set: ${filled?}`)

Common mistakes

E07175 — A generic with an uninitialised 'T?' field must make its no-arg default constructor 'private' (E07175); a public 'default' no-arg constructor would let external code create an empty instance whose field is never set. Keep it 'default private' so callers must supply a value via the inferred constructor. See ek9 -h E07175 for details.

Incorrect:

default Holder() as pure

Correct:

default private Holder() as pure
Other ways to ask this
  • I cannot call T() in my generic - now what?
  • My generic holds an abstract type or a function and item :=? T() fails
  • How do I write a generic with an optional field that cannot be defaulted?

Coming from another language?

Java: a generic with an abstract or functional T simply leaves the field null and you hope no one reads it. EK9: you must choose - either guarantee an unset value with T(), or forbid empty construction with 'default private'. Either way the null is designed out at compile time.

Keywords: E10031, E10030, function, constructor, abstract, E07175, T, no-arg, pure, generic, private, escape-hatch, E06110, default, E10032