How do pure constructors work and why use ':=?' for field assignment?

← Purity Contracts · Ref: Q563

Pure constructors use ':=?' (guarded assignment) for field assignment, not ':' (direct assignment). The ':=?' operator only assigns if the variable is currently unset, preventing mutation.

WHY ':=?' IN PURE CONSTRUCTORS

In a pure constructor, mutation operators are forbidden (E08120). The ':' operator on a field that already has a default value counts as reassignment. The ':=?' operator is safe because it only assigns when the field is unset.

PATTERN FOR PURE CONSTRUCTORS

Declare fields with default values, then use ':=?' in the constructor:

  Config
    host as String: String()
    port as Integer: Integer()
    Config() as pure
      -> host as String, port as Integer
      this.host :=? host
      this.port :=? port

WHY THIS WORKS

The fields must start UNSET - 'String()', 'Integer()' - and NOT a value such as 0 or "". A field given a value is already SET, so ':=?' can never fire and the constructor argument is silently discarded; the compiler rejects that as E08095. The ':=?' checks if the field is unset and assigns only then. After the first assignment, the field is set and ':=?' becomes a no-op. This is idempotent and non-mutating.

ALL OR NOTHING RULE

If ANY constructor is pure, ALL constructors must be pure (see Q564). This prevents inconsistent construction semantics.

See Q560 for pure method basics. See Q564 for all-or-nothing constructor purity. See Q565 for multiple pure constructors. See Q79 for guarded assignment operator.

Example

defines module qa.purity.constructorassign

  defines class

    Config
      host as String: String()
      port as Integer: Integer()

      //Pure constructor using :=? for field assignment
      Config() as pure
        ->
          host as String
          port as Integer
        this.host :=? host
        this.port :=? port

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

      port() as pure
        <- rtn as Integer: port

      default operator ?

    //Another example: immutable value object
    Point
      xCoord as Float: Float()
      yCoord as Float: Float()

      Point() as pure
        ->
          xCoord as Float
          yCoord as Float
        this.xCoord :=? xCoord
        this.yCoord :=? yCoord

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

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

      distanceFromOrigin() as pure
        <- rtn as Float: sqrt(xCoord * xCoord + yCoord * yCoord)

      default operator ?

  defines program

    PureConstructorAssignDemo()
      stdout <- Stdout()

      config <- Config("localhost", 8080)
      stdout.println(`Host: ${config.host()}, Port: ${config.port()}`)

      point <- Point(3.0, 4.0)
      stdout.println(`Point: (${point.xCoord()}, ${point.yCoord()})`)
      stdout.println(`Distance: ${point.distanceFromOrigin()}`)

Common mistakes

E08095 — Giving the field a value at declaration makes it SET, so the ':=?' in the constructor can never apply and the 'host' argument is silently discarded - the object keeps "localhost" whatever you construct it with. Fields assigned by a pure constructor must be declared UNSET. See ek9 -h E08095 for details.

Incorrect:

host as String: "localhost"

Correct:

host as String: String()

E08100 — In a pure constructor, direct assignment with : counts as reassignment of a field. The guarded assignment :=? must be used instead because it only assigns when the field is unset. See ek9 -h E08100 for details.

Incorrect:

this.host: host

Correct:

this.host :=? host

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

Incorrect:

configXYZ <- Config("localhost", 8080)

Correct:

config <- Config("localhost", 8080)
Other ways to ask this
  • What is E08120 in a pure constructor?
  • Why does my pure constructor reject ':' for field assignment?
  • How do I assign fields in a pure constructor?

Coming from another language?

Java: no pure constructors, no guarded assignment. Python: no purity concept. Rust: constructors are always pure (no mutation after move). Kotlin: no pure constructors. EK9: pure constructors with ':=?' for idempotent field initialization, E08120 prevents mutation in pure context.

Keywords: assign, safe, mutation, side-effect, guarded, constant, E08120, immutable, purity, field, idempotent, constructor, pure, isset, null-safe, contract