How do traits interact with pure method contracts?

← Purity Contracts · Ref: Q569

Traits can declare methods as pure, and any class implementing the trait must maintain that purity contract. This is the same E05150 rule that applies to class inheritance.

PURE METHODS IN TRAITS

A trait can declare abstract pure methods:

  defines trait
    Measurable
      measure() as pure abstract
        <- rtn as Float?

Any class implementing Measurable must provide a pure override.

PURE DEFAULT METHODS

Traits can have default method implementations that are pure:

  Describable
    label() as pure
      <- rtn as String: "unknown"

Classes can override the default but must keep it pure.

IMPLEMENTATION RULE

When a class uses 'with trait of' and overrides a pure trait method, the override must be pure (E05150):

  MyClass with trait of Measurable
    override measure() as pure
      <- rtn as Float: 42.0

MULTIPLE TRAITS

If a class implements multiple traits, each pure method contract is independently enforced. A method that satisfies a pure trait must be pure.

See Q106 for trait basics. See Q107 for multiple traits. See Q561 for purity override rules. See Q567 for purity chains.

Example

defines module qa.purity.traitpure

  defines trait

    //Trait requiring pure methods
    Measurable
      measure() as pure abstract
        <- rtn as Float?

    //Trait with pure default method
    Labelable
      label() as pure
        <- rtn as String: "unlabeled"

  defines class

    //Must keep measure() pure because trait requires it
    Box with trait of Measurable, Labelable
      width <- 0.0
      height <- 0.0

      Box()
        ->
          width as Float
          height as Float
        this.width: width
        this.height: height

      //Pure override satisfies Measurable trait
      override measure() as pure
        <- rtn as Float: width * height

      //Pure override satisfies Labelable trait
      override label() as pure
        <- rtn as String: `Box ${width}x${height}`

      default operator ?

    //Another implementation of same traits
    Sphere with trait of Measurable, Labelable
      radius <- 0.0

      Sphere()
        -> radius as Float
        this.radius: radius

      override measure() as pure
        <- rtn as Float: 4.0 * 3.14159 * radius * radius

      override label() as pure
        <- rtn as String: `Sphere r=${radius}`

      default operator ?

  defines program

    TraitPureMethodsDemo()
      stdout <- Stdout()

      box <- Box(3.0, 4.0)
      stdout.println(`${box.label()}: ${box.measure()}`)

      sphere <- Sphere(5.0)
      stdout.println(`${sphere.label()}: ${sphere.measure()}`)

      //Polymorphic use through trait
      items <- List() of Measurable
      items += Box(2.0, 3.0)
      items += Sphere(1.0)

      for item in items
        stdout.println(`Measurement: ${item.measure()}`)

Common mistakes

E05150 — The Measurable trait declares measure as pure abstract. The implementing class must maintain purity. Overriding without as pure violates the trait contract. See ek9 -h E05150 for details.

Incorrect:

override measure()

Correct:

override measure() as pure

E05150 — The Labelable trait provides a pure default for label. Overriding it without as pure would violate the purity contract inherited from the trait. See ek9 -h E05150 for details.

Incorrect:

override label()

Correct:

override label() as pure
Other ways to ask this
  • Can a trait require pure methods?
  • Must a class implementing a trait maintain purity?
  • How does purity work with trait default methods?

Coming from another language?

Java: interfaces can have default methods but no purity concept. Kotlin: interfaces with defaults but no purity. Rust: traits with default implementations but no purity markers. EK9: traits can declare pure methods, implementations must maintain purity via E05150.

Keywords: method, pure, default, trait, contract, side-effect, implement, abstract, E05150, purity, immutable