What are the complete constructor rules for generic types?

← Generics · Ref: Q683

Generic types have specific constructor requirements to support type inference, default instantiation, and external usage.

TWO CONSTRUCTORS REQUIRED (E06040)

Every generic type must have a no-argument constructor and an inferred (typed) constructor. A conceptual-T property declared 'item as T?' must NOT be left null by the no-arg constructor - give it an unset 'T()' so it is present-but-unset, never null:

  Container of type T
    item as T?
    Container() as pure
      item :=? T()

PARAMETER TYPE MATCHING (E06050)

Constructor parameters must use the declared type parameters:

  Box of type T
    Box(item as T)        // Correct: parameter is T
    Box(item as String)   // Wrong: must use T

ACCESS (E06060)

The inferred (parameterised) constructor must be public for type inference. The no-arg DEFAULT constructor may be 'default private' as an escape hatch when the field cannot be given an unset 'T()' (T abstract, holds a function, or has no public no-arg constructor); external code then cannot create an empty instance. Protected is never allowed.

PURE CONSTRUCTOR CONSISTENCY

If the default constructor is pure, all other constructors must also be pure (E05190). In a pure constructor ':=' is forbidden, so use the guarded ':=?' to first-initialise the field.

COMBINED PATTERN

  Container of type T
    item as T?
    Container() as pure
      item :=? T()                   // optional field, never null
    Container(item as T) as pure     // inferred, public
      this.item :=? item

See Q642 for constructor inference. See Q643 for two-constructor requirement. See Q645 for the private no-arg escape hatch.

Example

defines module qa.genericsdeep.constructorrules

  defines class

    <?-
      Correct generic type with all constructor rules satisfied.
      1. Default constructor present (public, pure)
      2. Typed constructor uses type parameter T
      3. All constructors are pure (consistency)
    -?>
    Container of type T
      element as T?

      //Rule 1: default no-arg constructor required
      Container() as pure
        element :=? T()

      //Rule 2: typed constructor uses T (not a concrete type)
      Container() as pure
        -> item as T
        element :=? item

      //Public methods for access
      getElement()
        <- rtn as T: T()
        rtn :=? T(element)

      isEmpty() as pure
        <- rtn as Boolean: not element?

      default operator ?

    <?-
      Multi-parameter generic with correct constructors.
      Both type parameters used in typed constructor.
    -?>
    Association of type (K, V)
      theKey as K?
      theValue as V?

      //Default no-arg constructor
      Association() as pure
        theKey :=? K()
        theValue :=? V()

      //Typed constructor uses both K and V
      Association() as pure
        ->
          theKey as K
          theValue as V
        this.theKey :=? theKey
        this.theValue :=? theValue

      key()
        <- rtn as K: K()
        rtn :=? K(theKey)

      getTheValue()
        <- rtn as V: V()
        rtn :=? V(theValue)

      default operator ?

  defines program

    GenericConstructorRulesDemo()
      stdout <- Stdout()

      //Type inference from argument
      intContainer <- Container(42)
      stdout.println(`Container set: ${intContainer?}`)

      //Explicit type parameter
      emptyContainer <- Container() of String
      stdout.println(`Empty container: ${emptyContainer.isEmpty()}`)

      //Multi-parameter inference
      assoc <- Association("key", 99)
      stdout.println(`Association set: ${assoc?}`)

Common mistakes

E06050 — Constructor parameters in generic types must use the declared type parameter T, not a concrete type like String. Using a concrete type triggers E06050. See ek9 -h E06050 for details.

Incorrect:

Container() as pure
        -> item as String
        element :=? item

Correct:

Container() as pure
        -> item as T
        element :=? item

E06040 — Every generic type needs two constructors, a default no-argument one and an inferred typed one; omitting the no-arg constructor triggers this. See ek9 -h E06040 for details.

Incorrect:

      Container() as pure
        -> item as T

Correct:

      Container() as pure
        element :=? T()

      //Rule 2: typed constructor uses T (not a concrete type)
      Container() as pure
        -> item as T

E50060 — Container has no toString() method. Use the $ prefix operator or ? for boolean check. See ek9 -h E50060 for details.

Incorrect:

stdout.println(intContainer.toString())

Correct:

stdout.println(`Container set: ${intContainer?}`)
Other ways to ask this
  • What is E06040 generic constructor argument type mismatch?
  • What is E06050 generic default constructor required?
  • What is E06060 generic constructor must be public?
  • What constructors does a generic type need?

Coming from another language?

Java: generic classes need no special constructor rules. Rust: no constructors, uses associated functions. Go: no constructors, uses factory functions. Kotlin: generic classes follow standard constructor rules. EK9: generic types require default public constructor, typed constructor params must match type parameters.

Keywords: type, side-effect, generic, E06060, constant, type-parameter, parameter, E06050, immutable, constructor, default, pure, E06040