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