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: == (equality), $ (string), ? (isSet).

COMPARISON (pure, 1 arg, return Boolean or Integer)

  operator == as pure -> other as T <- rtn as Boolean
  operator <=> as pure -> other as T <- rtn as Integer

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 (pure, 1 arg, return Boolean or Integer) ===

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

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

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

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

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

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

      operator >= as pure
        -> other as Temperature
        <- rtn as Boolean: 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 equality checks 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 == other.celsius

Correct:

<- rtn as Boolean: celsius == other.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: immutable, expression, side-effect, type, practice, abstract, complete, implement, custom, temperature, operator, virtual, open, mutation, signature, override, class, example, default, pure