How do I implement all the operators my custom type needs?

← Operators and Expressions · Ref: Q245

Choose operators based on how your type will be used. Use 'default operator' for field-by-field behaviour, override for custom logic.

MINIMUM USEFUL SET

Most types need: a sameness (<=> or ==) with its #?, $ (string), ? (isSet).

COMPARISON - the sameness, written ONCE (pure, 1 arg)

  operator <=> as pure -> other as T <- rtn as Integer   (ordered type)
  operator == as pure -> other as T <- rtn as Boolean    (equatable-only type, instead of <=>)

The compiler generates the rest: ==, <>, <, <=, >, >= from <=>; <> from ==. Writing them by hand is E07181. A hand-written sameness needs a hand-written #? hashing the same fields (E07184).

ARITHMETIC (pure, 1 arg, return new value)

  operator + as pure -> other as T <- rtn as T
  operator - as pure -> other as T <- rtn as T

UNARY (pure, 0 args)

  operator - as pure <- rtn as T (negate)
  operator #^ as pure <- rtn as OtherType (promote)

MUTATION (NOT pure, no return)

  operator += -> other as T
  operator :=: -> source as T (copy)

CONVERSION (pure, 0 args, enforced return types)

  operator $ as pure <- rtn as String
  operator #? as pure <- rtn as Integer
  operator ? as pure <- rtn as Boolean

ERROR SCENARIOS

  E07500 - must be pure. E07510 - cannot be pure. E06280/E06290 - wrong param count. E07520/E07550/E07570/E07580 - wrong return type.

See Q96 for class operator syntax. See Q98 for record copy/merge/replace. See Q116 for default operator. See Q238 for complete operator set. See Q239 for comparison. See Q240 for arithmetic. See Q241 for mutation. See Q242 for conversion.

Example

defines module qa.operators.customtype

  defines class

    Temperature
      celsius <- Float()

      default private Temperature() as pure

      Temperature() as pure
        -> celsius as Float
        this.celsius :=: celsius

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

      // === COMPARISON: the sameness, written ONCE (pure, 1 arg, returns Integer) ===
      // ==, <>, <, <=, >, >= with another Temperature are generated from it

      operator <=> as pure
        -> other as Temperature
        <- rtn as Integer: celsius <=> other.celsius

      // === ARITHMETIC (pure, 1 arg, return new value) ===

      operator + as pure
        -> other as Temperature
        <- rtn as Temperature: Temperature(celsius + other.celsius)

      operator - as pure
        -> other as Temperature
        <- rtn as Temperature: Temperature(celsius - other.celsius)

      // === UNARY (pure, 0 args, return value) ===

      operator - as pure
        <- rtn as Temperature: Temperature(-celsius)

      operator #^ as pure
        <- rtn as Float: Float(celsius)

      // === CONVERSION (pure, 0 args, strictly typed returns) ===

      operator $ as pure
        <- rtn as String: `${celsius}C`

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

      override operator ? as pure
        <- rtn as Boolean: celsius?

      // === MUTATION (NOT pure, no return) ===

      operator +=
        -> other as Temperature
        celsius += other.celsius

      operator :=:
        -> source as Temperature
        celsius :=: source.celsius

  defines program

    CustomTypeDemo()
      stdout <- Stdout()

      freezing <- Temperature(0.0)
      boiling <- Temperature(100.0)
      body <- Temperature(37.0)

      // === COMPARISON ===

      stdout.println(`Equal: ${freezing == boiling}`)
      stdout.println(`Less: ${freezing < boiling}`)
      stdout.println(`Compare: ${freezing <=> boiling}`)

      // === ARITHMETIC (creates new values) ===

      sum <- freezing + body
      stdout.println(`Sum: ${sum}`)
      diff <- boiling - body
      stdout.println(`Diff: ${diff}`)

      // === UNARY ===

      negated <- -body
      stdout.println(`Negated: ${negated}`)

      // === PROMOTE ===

      floatVal as Float: body
      stdout.println(`Promoted: ${floatVal}`)

      // === CONVERSION ===

      stdout.println(`String: ${body}`)
      stdout.println(`IsSet: ${body?}`)
      stdout.println(`Hash: ${#?body}`)

      // === MUTATION ===

      room <- Temperature(20.0)
      room += Temperature(5.0)
      stdout.println(`After +=: ${room}`)

      target <- Temperature(0.0)
      target :=: boiling
      stdout.println(`After copy: ${target}`)

Common mistakes

E07500 — Comparison operators must be marked 'as pure' because comparisons cannot have side effects. Omitting 'as pure' triggers E07500. See ek9 -h E07500 for details.

Incorrect:

operator <=>

Correct:

operator <=> as pure

E07550 — The <=> (spaceship) operator must return Integer (negative, zero, or positive for ordering). Returning Boolean instead triggers E07550. See ek9 -h E07550 for details.

Incorrect:

<- rtn as Boolean: celsius <=> other.celsius

Correct:

<- rtn as Integer: celsius <=> other.celsius

E07520 — The ? operator must return Boolean. Returning Integer instead of Boolean triggers E07520. See ek9 -h E07520 for details.

Incorrect:

<- rtn as Integer: celsius?

Correct:

<- rtn as Boolean: celsius?

E50030 — A String literal cannot initialise a Temperature variable. EK9 is strongly typed and requires compatible types. Use the Temperature constructor with a Float argument. See ek9 -h E50030 for details.

Incorrect:

freezing as Temperature: "cold"

Correct:

freezing <- Temperature(0.0)
Other ways to ask this
  • What operators should I implement for a custom class in EK9?
  • How do I write a complete custom type with all operators?
  • What is the best practice for implementing operators on EK9 classes?

Coming from another language?

Java: Comparable, manual equals/hashCode/toString. Python: __eq__/__lt__/__add__ etc. Rust: derive traits. Go: no operator overloading. EK9: operators with enforced signatures, 'default operator' for field-by-field, override for custom.

Keywords: temperature, custom, implement, complete, abstract, practice, type, side-effect, expression, immutable, pure, default, example, class, override, signature, mutation, open, virtual, operator