Why does my for-range over a super type give super-type values when the start and end are subtype objects?

← Control Flow · Ref: Q2245

A range works ENTIRELY in its DECLARED type T. It makes its own values with T's copy constructor: the counter is T(start), the end is T(end) - made once - and each loop variable is T(counter). So with 'first as Step: TenStep(1)' and 'last as Step: TenStep(3)' the loop values are Steps, whatever the start and end objects really are; range over the subtype itself to get TenSteps.

This is also why the range runs at all: per-type sameness is the type's own, so a Step and a TenStep are never comparable ('<=>' is unset). Comparing the counter (a Step) with the developer's TenStep end would have run no iterations; comparing it with the range's own Step end works.

A range needs '++' and '--' (it may run either way), '<=>' and the copy constructor. The start, end and step you wrote are only read, never changed - the range advances its own counter in place, so the copy constructor must COPY each field (':=:'), never share it: 'this.position: other.place()' would hand the range the start's own Integer, and the loop would move the start. A range over an ABSTRACT declared type is E08272: it cannot make its values.

Example

#!ek9
defines module qa.controlflow.range.declared.super

  defines class

    Step as open
      position as Integer: 0

      Step()
        -> start as Integer
        this.position: start

      //The copy constructor: a range makes its own values with it. It COPIES the position (':=:'), so the
      //range's '++' on its own values never reaches the object it was copied from.
      Step()
        -> other as Step
        position :=: other.place()

      place() as pure
        <- rtn as Integer: position

      kind() as pure
        <- rtn as String: "Step"

      operator <=> as pure
        -> other as Step
        <- rtn as Integer: position <=> other.place()

      operator #? as pure
        <- rtn as Integer: #? position

      operator ++
        position++

      operator --
        position--

      operator $ as pure
        <- rtn as String: `${kind()}(${position})`

      default operator ?

    TenStep extends Step
      TenStep()
        -> start as Integer
        super(start)

      TenStep()
        -> other as TenStep
        super(other)

      override kind() as pure
        <- rtn as String: "TenStep"

  defines function

    describe()
      -> step as Step
      <- rtn as String: `${step} is a ${step.kind()}`

  defines program

    RangeOverDeclaredSuper()
      stdout <- Stdout()
      first as Step: TenStep(1)
      last as Step: TenStep(3)

      //Declared as Step: the values are Steps.
      for step in first ... last
        stdout.println(describe(step))

      //Declared as the subtype: the values are TenSteps.
      for tenStep in TenStep(1) ... TenStep(2)
        stdout.println(describe(tenStep))

      stdout.println(`first is still ${first}`)
Other ways to ask this
  • What type are the loop values of 'for step in first ... last' when first and last hold subclass objects?
  • Does an EK9 range iterate the run-time type or the declared type of its bounds?
  • Why does a range over a class with subclass bounds still run with D24 sameness?
  • Does a for-range modify my start or end object?

Coming from another language?

Java/Kotlin: a range is over primitives or Comparable values and iterates the objects it is given (Kotlin's rangeTo keeps the bounds). Python: range() is integers only. EK9: any type with a copy constructor, '<=>' and '++' can be ranged over, and the range makes its own values of the declared type.

Keywords: E08272, copy constructor, declared type, end, for range, loop variable, range, start, subtype