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

#!ek9
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: abstract, class, complete, custom, default, example, expression, immutable, implement, mutation, open, operator, override, practice, pure, side-effect, signature, temperature, type, virtual