Why is 'animals as List of Animal: [ Dog() ]' a List of Animal when '[ Dog() ]' on its own is a List of Dog?

← Collections and Data Structures · Ref: Q1395

A List or Dict literal gets its type in one of two ways.

INFERRED - NO DECLARED TYPE

'pets <- [ Dog(), Cat() ]' works out the most specific type every element shares: with Dog and Cat both extending Animal it is a List of Animal. '[ Dog() ]' alone is a List of Dog - that is all the compiler has to go on.

DECLARED - YOU SAID WHICH TYPE

'animals as List of Animal: [ Dog() ]' is ALWAYS a List of Animal, whatever the elements happen to be, because you were explicit. Each element must then be assignable to Animal exactly as a single assignment would be; one that is not - a Rock - is E50030, reported at that element. The same holds for a plain assignment ('animals := [ Cat() ]'), a returning declaration ('<- rtn as List of Animal: [ Dog() ]'), a nested literal ('List of List of Animal: [ [ Dog() ] ]') and a Dict literal, whose keys and values are checked against the declared key and value types.

AN EXISTING LIST STAYS WHAT IT IS

'dogs <- [ Dog() ]' followed by 'animals as List of Animal: dogs' is E50030. That is not a literal but a second name for the same mutable list: through 'animals' you could add a Cat to something 'dogs' still promises holds only Dogs. Generic types are invariant for exactly this reason.

Example

defines module qa.collections.declaredliteraltype

  defines class

    Animal as open
      name <- "animal"

      describe()
        <- rtn as String: "some animal"

      default operator ?

    Dog extends Animal
      override describe()
        <- rtn as String: "dog"

      default operator ?

    Cat extends Animal
      override describe()
        <- rtn as String: "cat"

      default operator ?

    Rock
      weight <- 1

      default operator ?

  defines program

    DeclaredLiteralTypeDemo()
      stdout <- Stdout()

      //Declared: this IS a List of Animal, so a Cat can be added to it.
      animals as List of Animal: [ Dog() ]
      animals += Cat()
      for animal in animals
        stdout.println(animal.describe())

      //Inferred from both elements: also a List of Animal.
      pets <- [ Dog(), Cat() ]
      for pet in pets
        stdout.println(pet.describe())

Common mistakes

E50030 — Every element of a declared List of Animal must be an Animal; Rock is not, so the error is reported at Rock(). See ek9 -h E50030.

Incorrect:

      animals as List of Animal: [ Dog(), Rock() ]

Correct:

      animals as List of Animal: [ Dog() ]

E50030 — 'dogs' is an existing List of Dog; naming it as a List of Animal would let a Cat be added to a list that promises only Dogs. Generic types are invariant - only a literal takes the declared type. See ek9 -h E50030.

Incorrect:

      dogs <- [ Dog(), Dog() ]
      pets as List of Animal: dogs

Correct:

      pets <- [ Dog(), Cat() ]
Other ways to ask this
  • How is the type of a list literal decided in EK9?
  • Can a List of Animal be initialised with only Dog elements?
  • Why can't I assign my List of Dog variable to a List of Animal?
  • Does a Dict literal take its key and value types from the declaration?

Coming from another language?

Java infers 'List<Animal> a = List.of(new Dog())' from the target type but rejects assigning a List<Dog> to a List<Animal> (generics are invariant; Java arrays are covariant and fail at run time with ArrayStoreException). Kotlin and Swift also take a literal's element type from the declaration. EK9 does the same: a literal takes the declared type, an existing list does not change type.

Keywords: inferred type, common super type, collection, declared type, dict literal, variance, generics, invariant, E50030, list literal