Why must all constructors be pure if any one is pure?

← Purity Contracts · Ref: Q564

EK9 enforces an all-or-nothing rule for constructor purity. If any constructor in a class is marked 'as pure', then every constructor must be marked 'as pure'. The compiler reports E05190 for violations.

WHY ALL OR NOTHING

Factory methods and delegation chains might call any constructor. If purity is inconsistent, the caller cannot know whether construction had side effects. Consistent purity makes object creation predictable.

THE ERROR: E05190

If one constructor is pure and another is not, the compiler reports E05190: mix of pure and not pure constructors.

CORRECT PATTERN: ALL PURE

Make every constructor pure if you want pure construction:

  MyClass() as pure
    -> a as String
    ...
  MyClass() as pure
    -> a as String, b as Integer
    ...

CORRECT PATTERN: NONE PURE

Make no constructors pure if construction needs side effects:

  MyClass()
    -> a as String
    ...
  MyClass()
    -> a as String, b as Integer
    ...

See Q563 for pure constructor field assignment. See Q565 for multiple pure constructors. See Q586 for pure constructor delegation.

Example

defines module qa.purity.allornothing

  defines class

    //Correct: ALL constructors are pure
    Coordinate
      xPos as Float: Float()
      yPos as Float: Float()

      Coordinate() as pure
        ->
          xPos as Float
          yPos as Float
        this.xPos :=? xPos
        this.yPos :=? yPos

      //Second pure constructor: also uses :=?
      Coordinate() as pure
        -> xPos as Float
        this.xPos :=? xPos

      xPos() as pure
        <- rtn as Float: xPos

      yPos() as pure
        <- rtn as Float: yPos

      default operator ?

    //Correct: NO constructors are pure
    LoggedEntity
      entityName <- String()

      LoggedEntity()
        -> entityName as String
        this.entityName: entityName

      LoggedEntity()
        ->
          entityName as String
          prefix as String
        this.entityName: prefix + entityName

      entityName() as pure
        <- rtn as String: entityName

      default operator ?

  defines program

    AllOrNothingDemo()
      stdout <- Stdout()

      //All-pure construction
      coord1 <- Coordinate(3.0, 4.0)
      coord2 <- Coordinate(5.0)
      stdout.println(`Coord1: (${coord1.xPos()}, ${coord1.yPos()})`)
      stdout.println(`Coord2: (${coord2.xPos()}, ${coord2.yPos()})`)

      //No-pure construction
      entity1 <- LoggedEntity("server")
      entity2 <- LoggedEntity("server", "prod-")
      stdout.println(`Entity1: ${entity1.entityName()}`)
      stdout.println(`Entity2: ${entity2.entityName()}`)

Common mistakes

E05190 — Since the two-parameter Coordinate constructor is pure, the one-parameter constructor must also be pure. Mixing pure and non-pure constructors triggers E05190. See ek9 -h E05190 for details.

Incorrect:

Coordinate()
        -> xPos as Float

Correct:

Coordinate() as pure
        -> xPos as Float

E50001 — Renaming the variable means later references to 'coord1' become unresolved, triggering E50001. See ek9 -h E50001 for details.

Incorrect:

coord1XYZ <- Coordinate(3.0, 4.0)

Correct:

coord1 <- Coordinate(3.0, 4.0)
Other ways to ask this
  • What is E05190 mix of pure and not pure constructors?
  • Can I have some pure and some non-pure constructors?
  • What is the all-or-nothing constructor purity rule?

Coming from another language?

Java: no constructor purity concept. Python: no purity. Rust: constructors are always effectively pure. Kotlin: no constructor purity. EK9: all-or-nothing constructor purity enforced by E05190.

Keywords: immutable, purity, E05190, mix, constructor, pure, all-or-nothing, consistent, contract, factory, side-effect, constant