How do record operators work in EK9?
← Classes and OOP · Ref: Q98
EK9 records combine public fields with constructors AND operators, making them first-class in collections, serialisation, and comparison without external utility functions.
COMPARISON OPERATORS
<=> for ordering (sorting), == and <> for equality, <, >, <=, >= for relational.
Enables stream sort, PriorityQueue, and generic ordered code.
CONVERSION OPERATORS
$ to String (display), $$ to JSON (serialisation), #? hashcode (Dict keys).
MUTATION OPERATORS
:=: deep copies all fields. :~: merges only SET fields (partial updates). :^: replaces entire content.
DEFAULT OPERATOR
default operator
Auto-generates all standard operators from fields. One line for full collection, serialisation, and comparison support.
NO METHODS ON RECORDS
Records have only constructors and operators. Use functions or classes for methods.
See Q96 for operators across constructs. See Q97 for records vs classes. See Q116 for default operator. See Q120 for sorting. See Q238 for the fixed operator set. See Q241 for mutation rules.
Example
defines module qa.oop.recordops defines record <?- A record with the full operator suite. These operators make it work naturally in collections, streams, and serialisation. -?> Config host <- String() port <- Integer() Config() -> host as String port as Integer this.host: host this.port: port // === COMPARISON: enables sorting and deduplication === operator <=> as pure -> other as Config <- rtn as Integer: host <=> other.host operator == as pure -> other as Config <- rtn as Boolean: host == other.host and port == other.port operator <> as pure -> other as Config <- rtn as Boolean: not (this == other) // === CONVERSION: enables display, logging, JSON, Dict keys === operator $ as pure <- rtn as String: `${host}:${port}` operator #? as pure <- rtn as Integer: #?host + #?port // === MUTATION: enables structured data transfer === operator :=: -> from as Config host :=: from.host port :=: from.port operator :~: -> from as Config if from.host? host :=: from.host if from.port? port :=: from.port operator :^: -> from as Config host :=: from.host port :=: from.port override operator ? as pure <- rtn as Boolean: host? and port? defines record <?- A record using 'default operator' to get the full suite auto-generated from fields. -?> Endpoint name <- String() url <- String() priority <- Integer() Endpoint() -> name as String url as String priority as Integer this.name: name this.url: url this.priority: priority default operator defines program RecordOpsDemo() stdout <- Stdout() // === COMPARISON: records sort naturally in collections === configs <- [ Config("zeta-host", 9090), Config("alpha-host", 8080), Config("mu-host", 3000) ] sorted <- cat configs | sort | collect as List of Config stdout.println("Sorted configs:") cat sorted > stdout // === EQUALITY: deduplication and lookup === c1 <- Config("localhost", 8080) c2 <- Config("localhost", 8080) c3 <- Config("production", 443) stdout.println(`Equal: ${c1 == c2}`) stdout.println(`Different: ${c1 <> c3}`) // === STRING AND HASHCODE: display, logging, Dict keys === stdout.println(`Config: ${c1}`) stdout.println(`Hash: ${#?c1}`) // === COPY: full deep copy === backup <- Config() backup :=: c1 stdout.println(`Backup: ${backup}`) // === MERGE: partial update (only set fields) === partial <- Config() partial.host: "override-host" target <- Config("original", 3000) target :~: partial stdout.println(`After merge: ${target}`) // === REPLACE: full replacement === replacement <- Config("new-host", 9090) target :^: replacement stdout.println(`After replace: ${target}`) // === DEFAULT OPERATOR: auto-generated full suite === endpoints <- [ Endpoint("api", "https://api.example.com", 1), Endpoint("web", "https://www.example.com", 2), Endpoint("api", "https://api.example.com", 1) ] first <- endpoints.getOrDefault(0, Endpoint()) third <- endpoints.getOrDefault(2, Endpoint()) stdout.println(`First equals third: ${first == third}`) sortedEndpoints <- cat endpoints | sort | collect as List of Endpoint stdout.println("Sorted endpoints:") cat sortedEndpoints > stdout
Common mistakes
E07290 — Records can only have constructors and operators, not methods. Adding a method like getDisplay() triggers E07290. Use operators for conversion ($, $$) and access fields directly (config.host). If you need methods, use a class instead. See ek9 -h E07290 for details.
Incorrect:
getDisplay() as pure <- rtn as String: host // === COMPARISON: enables sorting and deduplication ===
Correct:
// === COMPARISON: enables sorting and deduplication ===
E07235 — Records with fields must define operator ? for tri-state semantics. Without it, the record cannot participate in guard expressions or coalescing. Use 'default operator', 'default operator ?', or implement manually. See ek9 -h E07235 for details.
Incorrect:
defines record
Correct:
override operator ? as pure <- rtn as Boolean: host? and port? defines record
E07500 — Comparison operators must be marked 'as pure' because they should not have side effects. Omitting 'as pure' on ==, <>, <=>, <, >, <=, >= triggers E07500. See ek9 -h E07500 for details.
Incorrect:
operator <=>
Correct:
operator <=> as pure
Other ways to ask this
- What operators can records have in EK9?
- How do :=: and :~: and :^: operators work on records?
- Why are record operators important for collections?
- How do records support sorting, comparison, and serialisation?
Coming from another language?
Java: records (16+) immutable, no copy/merge. Python: dataclasses, no merge. Rust: derive macros, no merge. Go: structs have no operators. EK9: records carry comparison, conversion, mutation operators. 'default operator' generates full suite from fields.
Keywords: data clump, merge, collection, comparison, operator, copy, serialisation, field, sort, mutation, hashcode, stream, replace, JSON, record, pipeline, string, default