How do I write a generic type that can be parameterised with an abstract type?

← Generics · Ref: Q2020

Give the generic a PRIVATE no-argument constructor that leaves the T property unset (it must not call T()), and make every public construction go through the inferred-type constructor that takes a value:

  Holder of type T
    item as T?
    default private Holder()
    Holder()
      -> initial as T
      item: initial

Then 'Holder(square)' with 'square as Shape' (an abstract Shape) gives a 'Holder of Shape' that compiles and runs.

WHY T() FAILS FOR AN ABSTRACT TYPE

A generic over 'T?' needs a no-argument constructor (E06040), and the usual way to fill one is 'item: T()' - an unset T. But T() is a constructor call on whatever type parameterises the generic, and an abstract type has no constructor that may be called directly. So as soon as the generic is parameterised with Shape, every T() in it becomes 'Shape()' and is rejected with E10030 - even in a PRIVATE constructor that nothing calls, because the whole parameterised type is checked, not just the parts that are used.

WHY THE PRIVATE NO-ARG IS SAFE

The private constructor leaves 'item' without a value, so it may only be reached through this() from a constructor that then sets 'item'. Inside the generic, a construction call that reaches it directly - 'Holder() of T' - is E08264; outside it, 'Holder() of Shape' is already E06180 (not accessible). So no code can ever obtain a Holder whose item was never set, and nothing ever needs T().

The same Holder still works with concrete types: 'Holder("Steve")' is a Holder of String.

See Q1347 for when a generic must use 'default private'. See Q841 for abstract generics such as Iterator. See Q1404 for E08264.

Example

defines module qa.genericsdeep.abstractparameterisation

  defines class

    Shape as abstract
      area() as abstract
        <- rtn as Float?

      default operator ?

    Square extends Shape
      side as Float?

      default private Square()

      Square()
        -> sideLength as Float
        side: sideLength

      override area()
        <- rtn as Float: side * side

      default operator ?

    <?-
      Usable with abstract and concrete types alike: the no-arg constructor is private and does
      not call T(), so nothing here needs T to be constructable.
    -?>
    Holder of type T
      item as T?

      default private Holder()

      Holder()
        -> initial as T
        item: initial

      get()
        <- rtn as T: item

      duplicate()
        <- rtn as Holder of T: Holder(item)

      default operator ?

  defines program

    AbstractParameterisation()
      stdout <- Stdout()

      square as Shape: Square(3.0)
      shapes <- Holder(square)
      stdout.println(`area: ${shapes.get().area()}`)

      copied <- shapes.duplicate()
      stdout.println(`copy area: ${copied.get().area()}`)

      names <- Holder("Steve")
      stdout.println(`name: ${names.get()}`)

Common mistakes

E10030 — The no-arg constructor calls T(), and the generic is parameterised with the abstract Shape, so T() becomes a constructor call on an abstract type - rejected even though the constructor is private and never called. Leave 'item' unset in the private no-arg instead. See ek9 -h E10030 for details.

Incorrect:

private Holder()
        item: T()

Correct:

default private Holder()

E08264 — 'Holder() of T' reaches the private no-arg constructor, which leaves 'item' without a value; it may only be reached through this() from a constructor that sets 'item'. Construct through the value constructor. See ek9 -h E08264 for details.

Incorrect:

<- rtn as Holder of T: Holder() of T

Correct:

<- rtn as Holder of T: Holder(item)
Other ways to ask this
  • Holder(square) fails with E10030 when square is declared as an abstract Shape
  • My generic's no-arg constructor calls T() and now I cannot use it with an abstract type
  • Why is a private constructor that is never called still E10030 for an abstract type parameter?
  • Can a user generic hold a value whose declared type is an abstract class or a trait?

Coming from another language?

Java: a generic field of an abstract type is simply left null by a no-arg constructor (type erasure means T() does not exist at all). Kotlin/Swift: generic type parameters have no constructor either; you pass a value or a factory. EK9: T() exists for types that can be constructed, and is checked per parameterisation, so a generic meant for abstract types must not call it - the private no-arg plus the value constructor is the idiom.

Keywords: E06040, constructor, parameterise, E10030, default, E08264, generic, abstract, private, inferred, T(), no-arg