How do operators work in EK9?

← Classes and OOP · Ref: Q96

EK9 supports operators on records, classes, components, traits, and dynamic classes with uniform syntax. 'default operator' auto-generates standard operators from fields.

KEY CATEGORIES

Comparison: ==, <>, <=>, <, >, <=, >=. Conversion: $ (String), $$ (JSON), #? (hashcode), #^ (promote). IsSet: operator ? (MUST define on aggregates with fields, E07235). Mutation: :=: (copy), :~: (merge), :^: (replace).

'default operator' generates all standard operators from fields. Cannot be used on traits (E07030). Child classes call parent via $super, super?, super :=: source.

See Q93 for class basics. See Q97 for records. See Q106 for traits. See Q238 for the complete operator set. See Q241 for mutation operators.

Example

defines module qa.oop.operators

  defines trait

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

      operator <=> as pure
        -> other as Measurable
        <- rtn as Integer: measure() <=> other.measure()

      operator == as pure
        -> other as Measurable
        <- rtn as Boolean: measure() == other.measure()

      operator <> as pure
        -> other as Measurable
        <- rtn as Boolean: not (this == other)

      override operator ? as pure
        <- rtn as Boolean: measure()?

  defines class

    Temperature with trait of Measurable as open
      degrees <- Float()
      scale <- String()

      default private Temperature() as pure

      Temperature() as pure
        ->
          degrees as Float
          scale as String
        this.degrees :=? degrees
        this.scale :=? scale

      override measure() as pure
        <- rtn as Float: degrees

      operator + as pure
        -> other as Temperature
        <- rtn as Temperature: Temperature(degrees + other.degrees, scale)

      operator - as pure
        -> other as Temperature
        <- rtn as Temperature: Temperature(degrees - other.degrees, scale)

      operator :=:
        -> source as Temperature
        degrees :=: source.degrees
        scale :=: source.scale

      operator $ as pure
        <- rtn as String: `${$degrees}${scale}`

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

      override operator ? as pure
        <- rtn as Boolean: degrees? and scale?

    PreciseTemperature extends Temperature
      precision as Integer: Integer()

      PreciseTemperature() as pure
        ->
          degrees as Float
          scale as String
          precision as Integer
        super(degrees, scale)
        this.precision :=? precision

      override operator :=:
        -> source as PreciseTemperature
        super :=: source
        precision :=: source.precision

      override operator $ as pure
        <- rtn as String: `${$super} (precision: ${$precision})`

      override operator ? as pure
        <- rtn as Boolean: super? and precision?

  defines record

    Point
      x <- 0.0
      y <- 0.0

      Point()
        ->
          x as Float
          y as Float
        this.x: x
        this.y: y

      default operator

  defines component

    Gauge as abstract

      reading() as pure abstract
        <- rtn as Float?

      operator $ as pure
        <- rtn as String: `Gauge: ${$reading()}`

      override operator ? as pure
        <- rtn as Boolean: reading()?

  defines component

    TemperatureGauge extends Gauge
      sensor as Temperature: Temperature(0.0, "")

      TemperatureGauge()
        -> sensor as Temperature
        this.sensor: sensor

      override reading() as pure
        <- rtn as Float: sensor.measure()

      default operator ?

  defines program

    OperatorsDemo()
      stdout <- Stdout()

      // === CLASS OPERATORS: custom implementations ===

      t1 <- Temperature(20.0, "C")
      t2 <- Temperature(5.0, "C")

      sum <- t1 + t2
      stdout.println(`Sum: ${sum}`)

      diff <- t1 - t2
      stdout.println(`Diff: ${diff}`)

      stdout.println(`Equal: ${t1 == t2}`)
      stdout.println(`Compare: ${t1 <=> t2}`)
      stdout.println(`String: ${t1}`)
      stdout.println(`IsSet: ${t1?}`)

      // === SUPER OPERATORS: child calls parent ===

      pt <- PreciseTemperature(20.0, "C", 3)
      stdout.println(`Precise: ${pt}`)

      copied <- PreciseTemperature(0.0, "", 0)
      copied :=: pt
      stdout.println(`Copied: ${copied}`)

      // === RECORD OPERATORS: default operator auto-generates ===

      p1 <- Point(3.0, 4.0)
      p2 <- Point(3.0, 4.0)
      p3 <- Point(1.0, 2.0)

      stdout.println(`Points equal: ${p1 == p2}`)
      stdout.println(`Points differ: ${p1 <> p3}`)
      stdout.println(`Point string: ${p1}`)

      // === TRAIT OPERATORS: concrete operators on trait ===

      measurable as Measurable: t1
      stdout.println(`Via trait ==: ${measurable == t2}`)
      stdout.println(`Via trait isSet: ${measurable?}`)

      // === COMPONENT OPERATORS: custom $ and ? ===

      gauge <- TemperatureGauge(Temperature(22.5, "C"))
      stdout.println(`${gauge}`)
      stdout.println(`Gauge set: ${gauge?}`)

Common mistakes

E05120 — The '?' operator is inherited from the base type. When providing a custom implementation, you must use the 'override' keyword. Omitting 'override' triggers E05120. See ek9 -h E05120 for details.

Incorrect:

operator ? as pure

Correct:

override operator ? as pure

E07500 — Comparison and query operators must be marked 'as pure'. Equality checks cannot have side effects. Omitting 'as pure' triggers E07500. See ek9 -h E07500 for details.

Incorrect:

operator ==

Correct:

operator == as pure

E07235 — Any aggregate with fields must define operator ? for EK9's tri-state semantics. Without it, guard expressions, coalescing, and safe access cannot inspect field state. Use 'default operator ?', 'default operator', or implement manually. See ek9 -h E07235 for details.

Incorrect:

//no operator ? defined

Correct:

default operator ?

E50060 — EK9 does not have a 'toString()' method. Use the $ operator for string conversion: '$sum' or string interpolation '`${sum}`'. See ek9 -h E50060 for details.

Incorrect:

stdout.println(sum.toString())

Correct:

stdout.println(`Sum: ${sum}`)

E50001 — Renaming the field to 'decimalPlaces' breaks all references to 'precision' in the constructor and operators. Field names must be consistent throughout the class. See ek9 -h E50001 for details.

Incorrect:

decimalPlaces <- 2

Correct:

precision <- 2
Other ways to ask this
  • How do I define custom operators on an EK9 type?
  • What constructs support operators in EK9?
  • How does operator overloading work in EK9?
  • How do I compare objects in EK9?
  • Which EK9 constructs can have operators?

Coming from another language?

Java: equals/hashCode/toString methods. Python: dunder methods. Rust: derive macros. EK9: operator keyword syntax, 'default operator' auto-generates from fields.

Keywords: component, overload, copy, E07235, pure, abstract, default, class, record, equality, custom, override, trait, string, compare, dynamic, tri-state, object-oriented, operator, mutation, isset, hashcode, isSet, comparison, merge