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:

  circles <- List() of Circle    // one type
  shapes <- List() of Shape      // completely different type
  numbers <- 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:

  Processor
    process()
      -> circles as List of Circle
      <- rtn as Integer: length circles
    process()
      -> shapes as List of Shape
      <- rtn as Integer: length shapes

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

    Hue as abstract
      colourName as String: String()

      default Hue() as pure

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

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

      default operator ?

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

    Blue is Hue
      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: parameterised, covariance, erasure, overload, migrate, independence, list, generic, type, separate, type-parameter