Why is List of Circle not compatible with List of Shape?

← Generics · Ref: Q653

EK9 creates truly independent types for each generic parameterisation. 'List of Circle' and 'List of Shape' are completely separate types with no relationship, even if Circle extends Shape.

TYPE INDEPENDENCE

Each parameterisation is a distinct type:

  List of Circle   // one type
  List of Shape    // completely different type
  List of Integer  // yet another different type

NO ERASURE

Unlike Java where 'List<Circle>' and 'List<Shape>' become the same 'List' at runtime (erasure), EK9 maintains full type identity. They are separate types that happen to have similar structures.

NO COVARIANCE

You cannot pass 'List of Circle' where 'List of Shape' is expected. This prevents the type safety violation where adding a Square to a 'List of Shape' that is actually a 'List of Circle'.

METHOD OVERLOADING

Because types are independent, you can overload methods:

  process(circles as List of Circle)
  process(shapes as List of Shape)

Java cannot do this due to erasure.

See Q198 for built-in generics. See Q195 for constrain by. See Q196 for multi-parameter generics. See Q655 for method overloading with generics. See Q642 for constructor inference.

Example

defines module qa.genericsdeep.typeindependence

  defines class

    Colour as abstract
      colourName as String: String()

      default Colour() as pure

      Colour() as pure
        -> n as String
        colourName :=? String(n)

      name() as pure
        <- rtn as String: String(colourName)

      default operator ?

    Red is Colour
      default Red() as pure
      Red() as pure
        -> n as String
        super(n)

    Blue is Colour
      default Blue() as pure
      Blue() as pure
        -> n as String
        super(n)

  defines program

    TypeIndependenceDemo()
      stdout <- Stdout()

      // === SEPARATE LISTS OF DIFFERENT TYPES ===

      reds <- List() of Red
      reds += Red("Crimson")
      reds += Red("Scarlet")

      blues <- List() of Blue
      blues += Blue("Navy")

      // === EACH LIST IS AN INDEPENDENT TYPE ===

      redCount <- length reds
      stdout.println(`Red count: ${redCount}`)

      blueCount <- length blues
      stdout.println(`Blue count: ${blueCount}`)

      // === CANNOT MIX: List of Red != List of Blue ===
      // reds += Blue("Teal")  // ERROR: Blue is not Red

      // === INDEPENDENT CONTAINERS ===

      redOpt <- Optional(Red("Vermillion"))
      blueOpt <- Optional(Blue("Azure"))
      stdout.println(`Red optional set: ${redOpt?}`)
      stdout.println(`Blue optional set: ${blueOpt?}`)

      // Optional of Red and Optional of Blue are different types

      stdout.println("Each parameterised type is a separate independent type")

Common mistakes

E50060 — Each generic parameterisation creates a fully independent type. List of Red and List of Blue are completely separate types with no relationship, so adding a Blue to a List of Red is a type error. See ek9 -h E50060 for details.

Incorrect:

reds <- List() of Red
      reds += Blue("Navy")

Correct:

reds <- List() of Red
Other ways to ask this
  • How does type independence work for parameterised types in EK9?
  • Does EK9 use type erasure like Java?
  • Can I pass List of Circle where List of Shape is expected?

Coming from another language?

Java: type erasure makes List<Circle> and List<Shape> the same at runtime, cannot overload. C++: templates create independent types (full monomorphisation). Rust: generics create independent types. Go: generics create independent types (Go 1.18+). Kotlin: same erasure as Java. EK9: fully independent types, no erasure, safe method overloading.

Keywords: covariance, erasure, overload, migrate, independence, list, generic, type, separate, type-parameter, parameterised