Question: Can while and do-while loops return values (loop expressions)?

How do I use a while loop as an expression in EK9?

Can a do-while loop return a value in EK9?

How do while and do-while expression forms work?

Answer:
Yes. In EK9, both while and do-while loops can return values by declaring a return variable inside the loop body. The pattern is the same as for-range expressions.

WHILE EXPRESSION
Accumulate a sum using a while expression with a guard variable:
  result <- while counter <- 0 with counter < limit
    <- rtn <- 0
    rtn: rtn + counter
    counter: counter + 1
The outer '<- result' captures the loop result. 'counter <- 0' is a guard variable initialised to 0. 'with counter < limit' is the condition. The inner '<- rtn <- 0' declares the return variable. After the loop completes, the final value of 'rtn' is assigned to 'result'.

DO-WHILE EXPRESSION
The do-while expression form guarantees the body runs at least once:
  result <- do counter <- 0
    <- rtn <- 0
    rtn: rtn + counter
    counter: counter + 1
  while counter < limit
The body executes first, then 'counter < limit' is checked. The inner '<- rtn <- 0' declares the return variable.

WHILE VS DO-WHILE
while expression: Condition checked BEFORE each iteration. If condition is immediately false, the return variable keeps its initial value.
do-while expression: Body executes ONCE before condition check. Return variable always reflects at least one iteration.

SIMPLE WHILE EXPRESSION
Without a guard variable (using variables from outer scope):
  n <- 1
  sum <- while n <= 10
    <- rtn <- 0
    rtn: rtn + n
    n: n + 1

WHEN TO USE LOOP EXPRESSIONS
Use loop expressions when the loop's purpose is to compute a single result: sums, products, string building, search results. Use the statement form (Q66, Q67) when the loop performs side effects like I/O.

See Q66 for while statement form. See Q67 for do-while statement form. See Q80 for for-range expression. See Q82 for for-in expression. See Q68 for switch expression.

defines module qa.flow.while.expression

  defines function

    supplyLimit() as pure
      -> initial as Integer
      <- rtn as Integer: initial

  defines program

    WhileExpressionDemo()
      stdout <- Stdout()

      // === WHILE EXPRESSION ===

      limit <- supplyLimit(5)
      sumResult <- while counter <- 0 with counter < limit
        <- rtn <- 0
        rtn: rtn + counter
        counter: counter + 1
      stdout.println(`While sum 0..4: ${sumResult}`)

      // === DO-WHILE EXPRESSION ===

      doResult <- do counter <- 0
        <- rtn <- 0
        rtn: rtn + counter
        counter: counter + 1
      while counter < limit
      stdout.println(`Do-while sum 0..4: ${doResult}`)

      // === DO-WHILE AT-LEAST-ONCE ===

      // Even with limit=0, do-while body runs once
      zeroLimit <- supplyLimit(0)
      atLeastOnce <- do counter <- 1
        <- rtn <- 0
        rtn: rtn + counter
        counter: counter + 1
      while counter < zeroLimit
      stdout.println(`At-least-once result: ${atLeastOnce}`)

      // === BUILDING A STRING ===

      expectedLength <- supplyLimit(4)
      builtString <- while n <- 1 with n <= expectedLength
        <- rtn <- ""
        if length rtn > 0
          rtn: rtn + "-"
        rtn: rtn + $n
        n: n + 1
      stdout.println(`Built: ${builtString}`)

Java: no while/do-while expression form, must declare accumulator outside loop. Python: no while expression, use list comprehension or itertools for functional accumulation. Rust: loop { break value } returns a value from loop, while and for do not directly return values. Go: no loop expression form, always use external accumulator. Kotlin: no while expression, use generateSequence().takeWhile().fold() for functional equivalent. C#: no while expression, use LINQ for functional accumulation. JavaScript: no while expression, use Array methods for functional patterns. Swift: no while expression, use sequence/reduce patterns. EK9: while and do-while expression with declared return variable, guard variables for loop initialisation, accumulator scoped to loop body.

accumulate branch compute condition control counter do expression flow guard isset null-safe safe value while

total <- 0
      counter <- 0
      while counter < limit
        total: total + counter
        counter: counter + 1
      return total

Explanation: EK9 has no return statement. While expressions use a declared return variable inside the loop body. The final value is returned implicitly. See ek9 -h E01072 for details.

total <- 0
      counter <- 0
      do
        total: total + counter
        counter: counter + 1
        if counter > 3
          break
      while counter < limit

Explanation: EK9 has no break statement. Do-while expressions always run until the condition is false. Structure the while condition to control termination. See ek9 -h E01070 for details.


Question: Can a for-in loop return a value (for-in as expression)?

How do I use a for-in loop as an expression in EK9?

How do I reduce a collection to a single value in EK9?

How does the for-in expression form work?

Answer:
Yes. A for-in loop can return a value by declaring a return variable inside the loop body. This is EK9's equivalent of fold/reduce operations in functional languages.

BASIC FOR-IN EXPRESSION
Concatenate all items in a list:
  joined <- for item in items
    <- rtn <- ""
    rtn: rtn + item
The outer '<- joined' captures the loop result. The inner '<- rtn <- ""' declares the return variable initialised to an empty string. Each iteration appends the current item to 'rtn'. After the loop completes, the final string is assigned to 'joined'.

SUM A LIST OF NUMBERS
Reduce a list to its sum:
  numbers <- [10, 20, 30, 40]
  total <- for n in numbers
    <- rtn <- 0
    rtn: rtn + n
Result: 100. This is the equivalent of numbers.stream().reduce(0, Integer::sum) in Java or numbers.iter().sum() in Rust.

FIND MAXIMUM
Find the largest value in a list:
  largest <- for n in numbers
    <- rtn <- Integer()
    if ~rtn? or n > rtn
      rtn: n
The return variable starts unset (Integer()). The first iteration sets it to the first element. Subsequent iterations update it only if the current element is larger.

BUILDING WITH SEPARATOR
Join strings with a comma separator:
  csv <- for name in names
    <- rtn <- ""
    if length rtn > 0
      rtn: rtn + ", "
    rtn: rtn + name
Result: 'Alice, Bob, Charlie'.

STATEMENT VS EXPRESSION
The statement form (see Q65) executes code for side effects like printing. The expression form computes a single result from the collection. Use expression form when the loop's purpose is to accumulate, reduce, or transform.

See Q65 for for-in statement form. See Q68 for switch expression. See Q80 for for-range expression. See Q81 for while/do-while expressions. See Q89 for stream pipeline basics. See Q122 for stream collect as aggregation. See Q237 for streams vs loops decision guide.

defines module qa.flow.forin.expression

  defines program

    ForInExpressionDemo()
      stdout <- Stdout()

      // === CONCATENATE STRINGS ===

      words <- ["Hello", " ", "World"]
      joined <- for word in words
        <- rtn <- ""
        rtn: rtn + word
      stdout.println(`Joined: ${joined}`)

      // === SUM NUMBERS ===

      numbers <- [10, 20, 30, 40]
      total <- for n in numbers
        <- rtn <- 0
        rtn: rtn + n
      stdout.println(`Sum: ${total}`)

      // === JOIN WITH SEPARATOR ===

      names <- ["Alice", "Bob", "Charlie"]
      csv <- for name in names
        <- rtn <- ""
        if length rtn > 0
          rtn: rtn + ", "
        rtn: rtn + name
      stdout.println(`Names: ${csv}`)

      // === COUNT MATCHING ITEMS ===

      scores <- [85, 42, 91, 67, 73, 95]
      passingGrade <- 70
      passing <- for s in scores
        <- rtn <- 0
        if s >= passingGrade
          rtn: rtn + 1
      stdout.println(`Passing scores: ${passing}`)

Java: stream().reduce(identity, accumulator) or stream().collect() for fold/reduce patterns. Python: functools.reduce(fn, iterable, initial) or list comprehension. Rust: iterator.fold(init, |acc, x| acc + x) or .sum()/.collect(). Go: no fold primitive, must use for-range with external accumulator. Kotlin: list.fold(initial) { acc, item -> acc + item } or list.reduce(). C#: list.Aggregate(seed, (acc, x) => acc + x) LINQ method. JavaScript: array.reduce((acc, x) => acc + x, initial). Swift: array.reduce(0, +) or array.reduce(into:). EK9: for-in expression with declared return variable, natural fold/reduce pattern, accumulator scoped to loop body, no external variable needed.

accumulate branch collection compute condition control expression flow fold for in migrate reduce value

total <- 0
      for n in numbers
        total: total + n
      return total

Explanation: EK9 has no return statement. For-in expressions use a declared return variable scoped to the loop body. The final value is returned implicitly. See ek9 -h E01072 for details.


Question: Which control flow constructs can be used as expressions in EK9?

How do expression forms work across different control flow constructs?

What is the expression vs statement distinction in EK9 control flow?

Answer:
EK9 allows six control flow constructs to be used as expressions that return values: switch, for-range, for-in, while, do-while, and try. All follow the same pattern: declare a return variable with '<-' inside the body.

THE UNIVERSAL PATTERN
Every expression form uses the same structure:
  result <- CONTROL_FLOW_CONSTRUCT
    <- rtn <- initialValue
    ... body updates rtn ...
The outer '<- result' captures the expression value. The inner '<- rtn <- initialValue' declares and initialises the return variable. The body modifies 'rtn' across iterations or branches. The final value of 'rtn' becomes the result.

SWITCH EXPRESSION (see Q68)
  label <- switch priority
    <- rtn as String?
    case 1
      rtn: "HIGH"
    default
      rtn: "NORMAL"

FOR-RANGE EXPRESSION (see Q80)
  total <- for i in 1 ... 10
    <- rtn <- 0
    rtn: rtn + i

FOR-IN EXPRESSION (see Q82)
  joined <- for item in items
    <- rtn <- ""
    rtn: rtn + item

WHILE EXPRESSION (see Q81)
  result <- while counter <- 0 with counter < limit
    <- rtn <- 0
    rtn: rtn + counter
    counter: counter + 1

DO-WHILE EXPRESSION (see Q81)
  result <- do counter <- 0
    <- rtn <- 0
    rtn: rtn + counter
    counter: counter + 1
  while counter < limit

STATEMENT VS EXPRESSION FORM
Statement form: performs side effects (I/O, mutation). No return variable.
  for i in 1 ... 10
    stdout.println($i)

Expression form: computes and returns a value. Has return variable.
  total <- for i in 1 ... 10
    <- rtn <- 0
    rtn: rtn + i

IF IS NOT AN EXPRESSION
Unlike the six constructs above, if/else in EK9 is always a statement. For a value chosen by a condition, use the TERNARY operator — 'grade <- score >= passMark <- "A" : "B"' (or 'else' in place of ':'), which is the if-expression equivalent (see Q1360) — or a switch expression for multi-way selection.

WHY EXPRESSIONS MATTER
Expression forms eliminate temporary accumulator variables that pollute the outer scope. The return variable is scoped to the construct, making intent clear and preventing accidental reuse.

See Q68 for switch expression. See Q80 for for-range expression. See Q81 for while/do-while expression. See Q82 for for-in expression.

See Q80 for for-range expressions. See Q81 for while expressions. See Q82 for for-in expressions.

defines module qa.flow.expression.overview

  defines function

    supplyPriority() as pure
      -> level as Integer
      <- rtn as Integer: level

    supplyLimit() as pure
      -> initial as Integer
      <- rtn as Integer: initial

  defines program

    ExpressionOverviewDemo()
      stdout <- Stdout()

      // === SWITCH EXPRESSION ===

      priority <- supplyPriority(2)
      label <- switch priority
        <- rtn as String?
        case 1
          rtn: "HIGH"
        case 2
          rtn: "MEDIUM"
        default
          rtn: "LOW"
      stdout.println(`Priority: ${label}`)

      // === FOR-RANGE EXPRESSION ===

      total <- for i in 1 ... 5
        <- rtn <- 0
        rtn: rtn + i
      stdout.println(`Sum 1..5: ${total}`)

      // === FOR-IN EXPRESSION ===

      words <- ["EK9", " ", "rocks"]
      joined <- for word in words
        <- rtn <- ""
        rtn: rtn + word
      stdout.println(`Joined: ${joined}`)

      // === WHILE EXPRESSION ===

      limit <- supplyLimit(4)
      whileResult <- while counter <- 0 with counter < limit
        <- rtn <- 0
        rtn: rtn + counter
        counter: counter + 1
      stdout.println(`While sum 0..3: ${whileResult}`)

      // === DO-WHILE EXPRESSION ===

      doResult <- do counter <- 0
        <- rtn <- 0
        rtn: rtn + counter
        counter: counter + 1
      while counter < limit
      stdout.println(`Do-while sum 0..3: ${doResult}`)

Java: switch expression (Java 14+), no loop expressions, Stream.reduce() for functional fold. Rust: all blocks are expressions, match/if/loop can all return values, for and while cannot. Go: no expression forms for any control flow. Kotlin: when is expression, no loop expressions, fold/reduce for functional patterns. Python: ternary expression and comprehensions, no loop expressions. C#: switch expression (C# 8+), no loop expressions, LINQ Aggregate for fold. JavaScript: ternary only, no loop or switch expressions. Swift: no loop expressions, switch not an expression. EK9: switch, for-range, for-in, while, and do-while all have expression forms with uniform return variable pattern.

branch condition constant control expression flow form loop overview pattern return statement switch value

switch priority
        case 1
          return "HIGH"
        case 2
          return "MEDIUM"
        default
          return "LOW"

Explanation: EK9 has no return statement. All five expression forms (switch, for-range, for-in, while, do-while) use declared return variables. The value is returned implicitly. See ek9 -h E01072 for details.

total <- 0
      for i in 1 ... 5
        total: total + i
        if total > 10
          break

Explanation: EK9 has no break statement. For-range expressions scope the accumulator inside the loop and always run to completion. See ek9 -h E01070 for details.


Question: What operations does Optional support in EK9?

What is the ternary guard pattern for Optional?

Answer:
Optional supports comparison, copy, merge, contains, and the ternary guard pattern. These operations follow EK9's tri-state semantics: two unset Optionals are equal, set and unset are not equal.

TERNARY GUARD
Extract a value or use a default in a single expression:
  value <- o? <- o.get() else String()
If 'o' is set, evaluates to o.get(); otherwise uses the default.

CONTAINS
Check if an Optional holds a specific value:
  item contains 42                    true if set AND value equals 42
An unset Optional never contains anything.

COMPARISON
  opt1 == opt2                        equal if both set with same value, or both unset
  opt1 <> opt2                        not equal

COPY AND MERGE
  copied :=: original                 deep copy
  target :~: source                   merge: sets target only if source is set
Merge is useful for layered defaults.

NO REASSIGNMENT IN SAFE BLOCKS
Once inside a guard, you cannot reassign the Optional:
  if o?
    o: Optional("other")             COMPILER ERROR
This prevents invalidating the safety guarantee.

STRING, JSON, AND HASHCODE
  $item                               string representation
  $$item                              JSON representation
  #? item                             hashcode

See Q47 for Optional basics (creation, guards, getOrDefault). See Q85 for Optional in stream pipelines. See Q29 for tri-state semantics. See Q98 for record copy, merge, and replace operators.

defines module qa.optional.operations

  defines program
    OptionalOperations()
      stdout <- Stdout()

      a <- Optional(10)
      b <- Optional(10)
      c <- Optional(99)
      none <- Optional() of Integer

      // === TERNARY GUARD ===

      ternarySet <- a? <- a.get() else 0
      stdout.println(`Ternary (set): ${ternarySet}`)

      ternaryNone <- none? <- none.get() else 0
      stdout.println(`Ternary (empty): ${ternaryNone}`)

      // === CONTAINS ===

      minQuantity <- 10
      maxQuantity <- 99
      stdout.println(`a contains 10: ${a contains minQuantity}`)
      stdout.println(`a contains 99: ${a contains maxQuantity}`)
      stdout.println(`none contains 10: ${none contains minQuantity}`)

      // === COMPARISON ===

      stdout.println(`a == b: ${a == b}`)
      stdout.println(`a <> c: ${a <> c}`)
      stdout.println(`a <> none: ${a <> none}`)

      // === COPY ===

      copied <- Optional() of Integer
      copied :=: a
      require copied == a
      stdout.println(`Copied: ${copied}`)

      // === MERGE ===

      target <- Optional() of Integer
      target :~: Optional(77)
      stdout.println(`Merged: ${target}`)

      // Merge with unset source — target unchanged
      target2 <- Optional(5)
      target2 :~: Optional() of Integer
      stdout.println(`Merge no-op: ${target2}`)

      // === STRING, JSON, HASHCODE ===

      require a?
      stdout.println(`String: ${a}`)
      stdout.println(`JSON: ${$ a}`)
      stdout.println(`Hash: ${#? a}`)

Java: Optional.equals() for comparison, no copy/merge, .map()/.flatMap() for transformations. Rust: Option implements PartialEq, Clone for copy, no merge concept. Kotlin: == on nullable types uses structural equality, no merge. Go: no Optional type, manual nil checks. EK9: full operator set (==, <>, :=:, :~:, contains, $, $$, #?) with consistent tri-state semantics and compile-time guard enforcement.

absent beginner comparison contains copy first guard hashcode intro isset json merge null-safe operations optional reassignment safe start string ternary

if a?
        a :=: Optional(5)
        stdout.println(`String: ${$a}`)

Explanation: Reassigning a variable inside a guard block invalidates the safety check. This triggers E08040 — reassignment/mutation within possible safe method access scope is not allowed. The guard guarantees a is set; reassigning could make it unset. See ek9 -h E08040 for details.

Explanation: Accessing .get() without a ? guard triggers E08030 — has not been checked before access. Always check with ? or use getOrDefault() to avoid needing a guard. See ek9 -h E08030 for details.


Question: How do I use Optional in stream pipelines?

How does flatten work with Optional in EK9?

How do I extract present values from a list of Optionals?

Answer:
Optional integrates with EK9's stream pipelines through the flatten operator. A List of Optional values can be flattened to extract only present values, discarding empty ones.

FLATTEN
Extract present values from a list of Optionals:
  optionals <- [Optional(5), Optional() of Integer, Optional(14)]
  cat optionals | flatten > stdout
The empty Optional contributes nothing. Only 5 and 14 pass through.

COLLECT WITH FLATTEN
Aggregate present values:
  sum <- cat optionals | flatten | collect as Integer
Sum equals 19. This is EK9's equivalent of Java's Stream.flatMap(Optional::stream) but more intuitive.

OPTIONAL AS ITERATOR
Optional supports the iterator protocol, yielding 0 or 1 elements:
  present <- Optional("Hello")
  while iter <- present.iterator() then iter.hasNext()
    stdout.println($iter.next())
An empty Optional's iterator yields nothing. This makes Optional compatible with any code that expects an iterable.

See Q47 for Optional basics (creation, guards, getOrDefault). See Q84 for Optional operations (comparison, copy, merge). See Q59 for function pipelines. See Q123 for flattening nested collections and lists of lists.

defines module qa.optional.streams

  defines program
    OptionalStreams()
      stdout <- Stdout()

      // === FLATTEN ===

      // Extract present values from list of Optionals
      optionals <- [Optional(5), Optional() of Integer, Optional(14)]
      cat optionals | flatten > stdout

      // === COLLECT WITH FLATTEN ===

      // Sum only the present values
      sum <- cat optionals | flatten | collect as Integer
      require sum == 19
      stdout.println(`Sum of present values: ${sum}`)

      // Inline form
      mixed <- [Optional(10), Optional() of Integer, Optional(20)]
      sum2 <- cat mixed | flatten | collect as Integer
      stdout.println(`Sum2: ${sum2}`)

      // === OPTIONAL AS ITERATOR ===

      // Present Optional yields one element
      present <- Optional("Hello")
      while iter <- present.iterator() then iter.hasNext()
        stdout.println(`Iterator: ${iter.next()}`)

      // Empty Optional yields nothing
      emptyOpt <- Optional() of String
      while iter <- emptyOpt.iterator() then iter.hasNext()
        stdout.println("Should not print")

      stdout.println("Done")

Java: Stream.flatMap(Optional::stream) to extract present values, verbose compared to EK9's flatten. Rust: .filter_map(|x| x) or .flatten() on Iterator<Option<T>>. Kotlin: .filterNotNull() on lists of nullable types. Python: list comprehension [x for x in items if x is not None]. EK9: cat optionals | flatten — single intuitive operator extracts present values from any list of Optionals.

absent aggregate beginner collect extract first flatten guard intro iterator migrate optional pipeline present safe start stream

Explanation: The 'cat' operation requires an iterable source (List, Dict, Range, Optional, etc.). Using a non-iterable Integer value triggers E07840 — cannot iterate over that type. See ek9 -h E07840 for details.

Explanation: EK9 Optional does not have a Java-style '.stream()' method. Use '.iterator()' to get an iterator that yields 0 or 1 elements. See ek9 -h E50060 for details.


Question: How do Result guard patterns and ternary access work?

How does the dual guard pattern work with Result isOk and isError?

How do I use ternary Result access to get ok or error values with defaults?

What are the safe access patterns for Result values in EK9?

Answer:
In EK9, Result guard patterns let you safely access ok and error values through compiler-enforced checks. The key insight is that isOk() and isError() are INDEPENDENT guards: isOk() unlocks .ok(), isError() unlocks .error(), and neither unlocks the other.

DUAL GUARD PATTERN
Check both sides of a Result independently using if/else if:
  if r.isOk()
    okVal <- r.ok()
  else if r.isError()
    errVal <- r.error()
The compiler tracks which guard is active. Inside the isOk() branch, only .ok() is permitted. Inside the isError() branch, only .error() is permitted. Using the wrong accessor is a compile error.

? OPERATOR GUARDS OK ONLY
The ? operator is shorthand for isOk(), so it only unlocks .ok():
  if r?
    okVal <- r.ok()
You cannot call .error() inside a ? guard block. For error access, you must use isError() explicitly.

TERNARY GUARD PATTERNS
Get a value or a default in a single expression:
  okVal <- r.isOk() <- r.ok() else String()
  errVal <- r.isError() <- r.error() else Integer()
The ternary reads as: declare okVal, check isOk(), if true assign r.ok(), otherwise assign the default. This is concise and safe. The compiler verifies the guard matches the accessor.

AND LOGIC WITH MULTIPLE RESULTS
Combine guards across multiple Results with 'and':
  if r1? and r2.isError()
    okVal <- r1.ok()
    errVal <- r2.error()
Each guard independently unlocks its own Result. The block only runs if ALL conditions pass.

NO REASSIGNMENT IN SAFE BLOCKS
Once a Result is guarded, the compiler prevents reassignment inside the safe block:
  if r?
    r: otherResult    COMPILER ERROR: NO_REASSIGNMENT_WITHIN_SAFE_ACCESS
This prevents invalidating the guard check by swapping the Result for one that might not be ok.

See Q48 for Result basics and creation patterns. See Q87 for Result operations (merge, copy, callbacks). See Q74 for guard patterns in if statements. See Q139 for when to use Result vs try/catch exception handling. See Q259 for how EK9's four-state Result differs from other languages.

defines module qa.result.guards

  defines function

    <?-
      Returns a Result with ok value set.
    -?>
    getOkResult()
      <- rtn <- Result("Success", Integer())

    <?-
      Returns a Result with error value set.
    -?>
    getErrorResult()
      <- rtn <- Result(String(), 42)

    <?-
      Returns a Result with both ok and error set.
    -?>
    getBothResult()
      <- rtn <- Result("Partial", -1)

  defines program

    ResultGuardDemo()
      stdout <- Stdout()

      // === DUAL GUARD PATTERN ===

      r1 <- getOkResult()
      if r1.isOk()
        stdout.println(`Dual ok: ${r1.ok()}`)
      else if r1.isError()
        stdout.println(`Dual error: ${r1.error()}`)

      r2 <- getErrorResult()
      if r2.isOk()
        stdout.println(`Should not print`)
      else if r2.isError()
        stdout.println(`Dual error side: ${r2.error()}`)

      // === ? OPERATOR GUARDS OK ONLY ===

      r3 <- getOkResult()
      if r3?
        stdout.println(`? guard ok: ${r3.ok()}`)

      r4 <- getErrorResult()
      if ~r4?
        stdout.println("Error result: ? is false as expected")

      // === isOk GUARDS ok, isError GUARDS error — INDEPENDENT ===

      r5 <- getBothResult()
      if r5.isOk()
        stdout.println(`Both has ok: ${r5.ok()}`)
      if r5.isError()
        stdout.println(`Both has error: ${r5.error()}`)

      // === TERNARY GUARD: ok with default ===

      r6 <- getOkResult()
      okVal <- r6.isOk() <- r6.ok() else String()
      stdout.println(`Ternary ok: ${okVal}`)

      // === TERNARY GUARD: error with default ===

      r7 <- getErrorResult()
      errVal <- r7.isError() <- r7.error() else Integer()
      stdout.println(`Ternary error: ${errVal}`)

      // Ternary on empty Result falls back to default
      r8 <- Result() of (String, Integer)
      fallback <- r8.isOk() <- r8.ok() else "default"
      stdout.println(`Ternary fallback: ${fallback}`)

      // === AND LOGIC WITH MULTIPLE RESULTS ===

      rOk <- getOkResult()
      rErr <- getErrorResult()
      if rOk? and rErr.isError()
        stdout.println(`Combined: ok=${rOk.ok()}, error=${rErr.error()}`)

      // Both-value Result: can guard both independently
      rBoth <- getBothResult()
      if rBoth.isOk() and rBoth.isError()
        stdout.println(`Both sides: ${rBoth.ok()} with ${rBoth.error()}`)

      // === SAFE BLOCK SCOPE ===
      // Inside a guard block, the guarded Result cannot be reassigned.
      // This prevents invalidating the safety guarantee.
      // Attempting: r: otherResult inside if r? would produce
      // COMPILER ERROR: NO_REASSIGNMENT_WITHIN_SAFE_ACCESS

      r9 <- getOkResult()
      if r9?
        safeOk <- r9.ok()
        stdout.println(`Safe access: ${safeOk}`)
        // r9 is protected here — no reassignment allowed

Java: try-catch blocks handle errors but have no compile-time enforcement that catch blocks are present for unchecked exceptions, no safe accessor pattern, can forget to handle the error path entirely. Rust: match on Result<T,E> with Ok(v)/Err(e) arms provides exhaustive checking, but .unwrap() is an escape hatch that panics at runtime, no independent ok+error access since Result is strictly either/or. Go: comma-ok idiom 'val, err := doThing(); if err != nil' is convention not enforcement, error can be silently ignored with _, no compiler-enforced safe access. EK9: isOk() and isError() are independent compiler-enforced guards, no escape hatches, ternary guard gives concise safe access with defaults, no reassignment rule prevents guard invalidation.

access beginner dual error first guard intro isError isOk isset null-safe ok pattern reassignment result safe start ternary

Explanation: Calling .ok() without an isOk() or ? guard triggers E08030 — has not been checked before access. The compiler enforces that isOk() must be checked before .ok(), and isError() before .error(). See ek9 -h E08030 for details.

if r9?
        r9 := getErrorResult()
        safeOk <- r9.ok()

Explanation: Reassigning a Result variable inside a guard block triggers E08040 — reassignment/mutation within safe access scope is not allowed. The guard verified r9 was ok; reassigning invalidates that check. See ek9 -h E08040 for details.


Question: What operations and callbacks does Result support?

How do whenOk and whenError callbacks work on Result?

How do I compare or copy Result values in EK9?

How does the merge operator work with Result?

Answer:
Result supports callbacks, factory methods, operators for comparison, copy, merge, and conversions beyond basic guard access.

WHEN OK / WHEN ERROR CALLBACKS
  r.whenOk(myConsumer)           called only if ok present
  r.whenError(errorConsumer)     called only if error present
Consumer is pure. Acceptor is non-pure (can mutate/do I/O). Callback never invoked if value absent.

CONTAINS
  r contains "Steve"             true if isOk AND ok equals argument

ITERATOR
Yields 0 or 1 elements over the ok value:
  while iter <- r.iterator() then iter.hasNext()
    item :=: iter.next()

FACTORY METHODS
  justOk <- r.asOk("Steve")     ok only
  justErr <- r.asError(-1)       error only
  cleared <- r.asEmpty()         neither set

MERGE (:~:)
Fills unset sides from another Result without overwriting:
  r1 :~: r2
Ideal for layering defaults.

COPY (:=:), COMPARISON (== <>)
Deep copy: target :=: source. Two empty Results are equal.

STRING ($), JSON ($$), HASHCODE (#?)
  $r, $$r, #? r

See Q48 for Result basics. See Q86 for Result guards. See Q54 for Consumer/Acceptor. See Q29 for tri-state. See Q139 for Result vs try/catch. See Q259 for four-state design.

defines module qa.result.operations

  defines function

    <?-
      Returns a Result with ok value set.
    -?>
    getOkResult()
      <- rtn <- Result("Steve", Integer())

    <?-
      Pure consumer for ok values.
    -?>
    okProcessor() as pure
      -> okContent as String
      require okContent?

    <?-
      Pure consumer for error values.
    -?>
    errorProcessor() as pure
      -> code as Integer
      require code?

  defines program
    ResultOperationsDemo()
      stdout <- Stdout()

      // === WHEN OK / WHEN ERROR CALLBACKS ===

      // Pure Consumer callbacks — read-only
      okOnly <- Result("Steve", Integer())
      okOnly.whenOk(okProcessor)
      stdout.println("whenOk with Consumer: called on ok Result")

      errOnly <- Result(String(), -1)
      errOnly.whenError(errorProcessor)
      stdout.println("whenError with Consumer: called on error Result")

      // Callbacks on Result with both values
      both <- Result("Default", -1)
      both.whenOk(okProcessor)
      both.whenError(errorProcessor)
      stdout.println("Both callbacks fired on dual-value Result")

      // Callback NOT fired when value absent
      emptyResult <- Result() of (String, Integer)
      emptyResult.whenOk(okProcessor)
      emptyResult.whenError(errorProcessor)
      stdout.println("Empty Result: neither callback fires")

      // === CONTAINS ===

      stdout.println(`contains Steve: ${okOnly contains "Steve"}`)
      stdout.println(`contains Other: ${okOnly contains "Other"}`)
      stdout.println(`error contains: ${errOnly contains "Steve"}`)

      // === ITERATOR ===

      extracted <- String()
      while iter <- okOnly.iterator() then iter.hasNext()
        extracted :=: iter.next()
      stdout.println(`Iterator ok: ${extracted}`)

      // Empty Result iterator yields nothing
      noItems <- String()
      while iter <- emptyResult.iterator() then iter.hasNext()
        noItems :=: iter.next()
      stdout.println(`Iterator empty: ${noItems?}`)

      // === FACTORY METHODS ===

      prototype <- Result("Proto", 99)

      justOk <- prototype.asOk("Fresh")
      require justOk.isOk() and not justOk.isError()
      stdout.println(`asOk: ${justOk}`)

      justErr <- prototype.asError(42)
      require not justErr.isOk() and justErr.isError()
      stdout.println(`asError: ${justErr}`)

      cleared <- prototype.asEmpty()
      require cleared is empty
      stdout.println(`asEmpty: ${cleared}`)

      // === MERGE (:~:) ===

      m1 <- Result("Steve", Integer())
      m2 <- Result(String(), -1)
      m1 :~: m2
      require m1.isOk() and m1.isError()
      stdout.println(`Merged: ${m1}`)

      // Merge does not overwrite existing ok
      m3 <- Result("Keep", Integer())
      m4 <- Result("Overwrite", -2)
      m3 :~: m4
      if m3.isOk()
        stdout.println(`Merge kept ok: ${m3.ok()}`)

      // === COPY (:=:) ===

      original <- Result("Alice", 7)
      copied <- Result() of (String, Integer)
      copied :=: original
      require copied == original
      stdout.println(`Copied: ${copied}`)

      // === COMPARISON (== and <>) ===

      c1 <- Result("A", 1)
      c2 <- Result("A", 1)
      c3 <- Result("B", 2)
      stdout.println(`c1 == c2: ${c1 == c2}`)
      stdout.println(`c1 <> c3: ${c1 <> c3}`)

      // Empty Results are equal
      e1 <- Result() of (String, Integer)
      e2 <- Result() of (String, Integer)
      stdout.println(`empty == empty: ${e1 == e2}`)

      // === STRING ($), JSON ($$), HASHCODE (#?) ===

      display <- Result("Steve", 42)
      stdout.println(`String: ${display}`)
      stdout.println(`JSON: ${$ display}`)
      stdout.println(`Hash: ${#? display}`)

Java: no Result, CompletableFuture callbacks no compile safety. Rust: map/and_then, no merge, either/or only. Go: (value, error) tuple, manual checks. EK9: whenOk/whenError callbacks, :~: merge, :=: copy, contains, iterator, factory methods, compile-time safety.

acceptor asEmpty asError asOk beginner callback comparison consumer contains copy error first guard hashcode intro iterator json merge ok operations result start string whenError whenOk

okProcessor() as pure
      -> okContent as String
      result <- getOkResult()
      require okContent?

Explanation: Calling a non-pure function like getOkResult() from within a pure function triggers E08130 — not marked pure but call is made in a pure scope. Pure functions cannot call non-pure functions. See ek9 -h E08130 for details.

Explanation: Result requires two different types for ok and error. Using the same type for both triggers E06190 — Result must be used with two different types. This ensures the compiler can distinguish ok from error. See ek9 -h E06190 for details.


Question: What operations does List support in EK9?

How do I check if an item is in a list in EK9?

How do I compare lists or convert them to JSON in EK9?

Answer:
EK9 Lists support a rich set of operations beyond basic add/remove: membership testing, reverse, comparison, copy/merge, and conversion to String/JSON.

MEMBERSHIP TESTING
Three ways to check if an item is in a list:
  numbers contains 3             operator syntax
  "Alice" is in names            natural language syntax
  "Zara" is not in names         negated form
All three return Boolean. The 'is in' and 'is not in' operators read like English.

REVERSE
  reversed <- numbers.reverse()   returns a new reversed list
The original list is unchanged.

COMPARISON
Lists compare element by element:
  [1, 2, 3] == [1, 2, 3]    true (same elements, same order)
  [1, 2, 3] <> [4, 5, 6]    true (different elements)

COPY AND MERGE
  copied :=: original       deep copy of original into copied
  target :~: "Eve"           merge a single item into target
  target :~: otherList       merge all items from otherList into target
Copy (:=:) replaces the contents entirely. Merge (:~:) appends to existing contents.

STRING AND JSON CONVERSION
  $numbers                   string representation: [1, 2, 3]
  $$numbers                  JSON array: [1, 2, 3]
The $$ operator converts any list to a JSON array.

HASHCODE
  hash <- #? numbers         integer hash of the list contents

EMPTY LIST IS SET (NOT UNSET)
Critical semantics: an empty list IS set. List() of String creates a valid, set, empty list. An empty list is meaningful, not unknown. Only explicit unSet() makes a list unset.

See Q45 for List basics (creation, add, remove, access). See Q89 for stream pipelines (filter, map, collect). See Q29 for the tri-state model (absent/unset/set). See Q130 for mutating vs non-mutating operator deep dive. See Q186 for merge collections. See Q187 for reverse list.

defines module qa.list.operations

  defines program
    ListOperationsDemo()
      stdout <- Stdout()

      // === MEMBERSHIP TESTING ===

      numbers <- [1, 2, 3, 4, 5]
      minTagLength <- 3
      hasThree <- numbers contains minTagLength
      stdout.println(`Contains 3: ${hasThree}`)

      names <- ["Alice", "Bob", "Charlie"]

      if "Alice" is in names
        stdout.println("Found Alice")

      if "Zara" is not in names
        stdout.println("Zara not found")

      // === REVERSE ===

      reversed <- numbers.reverse()
      stdout.println(`Reversed: ${reversed}`)
      stdout.println(`Original unchanged: ${numbers}`)

      // === COMPARISON ===

      l1 <- [1, 2, 3]
      l2 <- [1, 2, 3]
      l3 <- [4, 5, 6]

      stdout.println(`l1 == l2: ${l1 == l2}`)
      stdout.println(`l1 <> l3: ${l1 <> l3}`)

      // === COPY AND MERGE ===

      copied <- List() of Integer
      copied :=: numbers
      stdout.println(`Copied: ${copied}`)

      // Merge single item
      names :~: "Dave"
      stdout.println(`After merge item: ${names}`)

      // Merge list into list
      extras <- ["Eve", "Frank"]
      names :~: extras
      stdout.println(`After merge list: ${names}`)

      // === STRING AND JSON CONVERSION ===

      asString <- $numbers
      stdout.println(`As string: ${asString}`)

      stdout.println(`As JSON: ${$ numbers}`)

      // === HASHCODE ===

      hash <- #? numbers
      stdout.println(`Hash: ${hash}`)

      // === EMPTY LIST IS SET ===

      emptyList <- List() of String
      require emptyList?
      require emptyList is empty
      stdout.println(`Empty list isSet: ${emptyList?}`)
      stdout.println(`Empty list isEmpty: ${emptyList is empty}`)

Java: Collections.reverse() modifies in place, List.contains() for membership, no 'is in' syntax, equals() for comparison, no built-in JSON. Python: 'in' operator for membership, reversed() returns iterator, == for comparison, json.dumps() for JSON. JavaScript: includes() for membership, reverse() mutates, JSON.stringify() for JSON. Rust: contains() for membership, == via PartialEq, serde for JSON. Go: no contains (manual loop), no operator overloading. EK9: 'contains', 'is in', 'is not in' for membership, reverse() returns new list, == and <> for comparison, :=: copy, :~: merge, $ string, $$ JSON.

beginner
comparison
contains
copy
first
hashcode
intro
is in
is not in
json
list
membership
merge
operations
reverse
start
string

Explanation: List has no reversed() method. The correct EK9 method is reverse(). Use 'ek9 -h List' to see the full API. See ek9 -h E50060 for details.


Question: How do I use streams and stream pipelines in EK9?

What is the cat pipe collect pattern in EK9?

How do stream pipelines work with lists in EK9?

What replaces for loops with break in EK9?

What is the EK9 equivalent of Java Stream API?

Answer:
EK9 lists use cat | pipe | collect syntax for stream pipelines, replacing Java Streams, Python comprehensions, and JS array methods.

BASIC PATTERN: cat source | operations | terminal
  cat numbers | filter by isEven | collect as List of Integer
  cat numbers | map with doubleIt | collect as List of Integer
  cat numbers | collect as Integer

FILTER BY
  evens <- cat numbers | filter by isEven | collect as List of Integer
Predicate must be pure, returning Boolean.

MAP WITH
  doubled <- cat numbers | map with doubleIt | collect as List of Integer

COLLECT AS
  List of T collects elements. Integer reduces by summation.

DIRECT OUTPUT
  cat numbers | map with intToString > stdout

TEE FOR SIDE EFFECTS
  cat numbers | tee in sideEffect | collect as List of Integer

COMBINING: stages chain naturally. Streams replace loops with break/continue (EK9 has none).

See Q45 for List basics. See Q51 for abstract pipeline functions. See Q52 for dynamic functions. See Q53 for closures. See Q54 for Predicate/Comparator/UnaryOperator. See Q55 for delegates. See Q59 for pipelines. See Q64 for for-range. See Q65 for for-in. See Q88 for List operations. See Q120 for sorting. See Q122 for custom collect. See Q124 for group by. See Q125 for head/tail/skip. See Q133 for tee/uniq. See Q235 for stream reference. See Q236 for custom iterators. See Q237 for streams vs loops. See Q265 for fluent API. See Q275 for AI break/continue. See Q284 for first match.

defines module qa.list.streams

  defines function

    isEven() as pure
      -> num as Integer
      <- rtn as Boolean: num mod 2 == 0

    doubleIt() as pure
      -> num as Integer
      <- rtn as Integer: num * 2

    intToString() as pure
      -> num as Integer
      <- rtn as String: $num

  defines program
    ListStreamsDemo()
      stdout <- Stdout()

      numbers <- [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
      stdout.println(`Numbers: ${numbers}`)

      // === FILTER BY ===

      // Select even numbers
      evens <- cat numbers | filter by isEven | collect as List of Integer
      stdout.println(`Evens: ${evens}`)

      // === MAP WITH ===

      // Double each number
      doubled <- cat numbers | map with doubleIt | collect as List of Integer
      stdout.println(`Doubled: ${doubled}`)

      // Transform to strings
      asStrings <- cat numbers | map with intToString | collect as List of String
      stdout.println(`As strings: ${asStrings}`)

      // === COLLECT AS REDUCTION ===

      // Sum all numbers
      total <- cat numbers | collect as Integer
      stdout.println(`Sum: ${total}`)

      // === COMBINED PIPELINE ===

      // Filter then map
      doubledEvens <- cat numbers | filter by isEven | map with doubleIt | collect as List of Integer
      stdout.println(`Doubled evens: ${doubledEvens}`)

      // === DIRECT OUTPUT ===

      // Pipe directly to stdout
      cat numbers | map with intToString > stdout

Java: Stream API, verbose Collectors. Python: list comprehensions. JS: .filter().map().reduce(). Go: manual loops. EK9: cat | filter by | map with | collect as, unified pipe syntax.

async beginner cat chain collect concurrency filter first fluent functional intro lazy map migrate parallel pipe pipeline sequence start stdout stream tee transform

streams
stream pipeline
functional stream

Explanation: The 'filter by' pipeline stage requires a predicate function returning Boolean. A function returning any other type triggers E07520 — filter function must return Boolean. See ek9 -h E07520 for details.

map with intToString | collect as List of Integer
      stdout.println(`As strings: ${asStrings}`)

Explanation: Each pipeline stage must accept the type produced by the previous stage. A type mismatch between stages triggers E07830 — pipeline type mismatch. See ek9 -h E07830 for details.


Question: What operations does Dict support in EK9?

How do I get keys and values from an EK9 Dict?

How do I check if a Dict contains a key in EK9?

How do I merge, copy, or compare Dicts in EK9?

Answer:
Dict supports a rich set of operations beyond basic creation and lookup. This covers containment checks, key/value iterators, merging, copying, comparison, and conversions.

CONTAINS (KEY CHECK)
Three equivalent ways to check if a key exists:
  ages contains "Alice"           operator syntax
  "Bob" is in ages                natural language syntax
  "Zara" is not in ages           negated form
These check keys, not values.

KEYS AND VALUES ITERATORS
  keyIter <- ages.keys()          iterator over all keys
  valIter <- ages.values()        iterator over all values
These return iterators (not lists). Use in while loops:
  while keyIter?
    stdout.println(keyIter.next())

MERGING DICTS
+ creates a new merged dict (originals unchanged):
  combined <- dict1 + dict2
:~: merge operator (mutates the left-hand side):
  dict1 :~: dict2
When keys overlap, the right-hand side values win.

COPY AND REPLACE
:=: deep copy:
  copied :=: original
:^: replace contents entirely:
  target :^: source

COMPARISON
  dict1 == dict2                  equal if same key-value pairs
  dict1 <> dict2                  not equal

STRING AND JSON CONVERSION
  $ages                           string representation
  $$ages                          JSON object representation
The $$ operator converts Dict to JSON with keys as JSON keys and values as JSON values.

HASHCODE
  hash <- #? ages                 integer hashcode

EMPTY DICT IS SET (NOT UNSET)
CRITICAL: An empty dict IS set. Creating Dict() of (K, V) gives you a valid, set, empty dict. This follows EK9's collection semantics: collections are always meaningful when created, even if empty. An empty dict is not the same as a missing dict.
  emptyDict <- Dict() of (String, Integer)
  emptyDict?                      true (set)
  emptyDict is empty              true (no entries)

See Q46 (How does the Dict type work?) for creation, DictEntry, adding/removing, getOrDefault, and basic iteration. See Q45 (How does the List type work?) for the sister collection type. See Q29 (How do unset variables work?) for tri-state semantics. See Q129 for safe access patterns with missing keys. See Q260 for Dict key type requirements and custom keys.

Use 'ek9 -h Dict' to see the full API.

defines module qa.dict.operations

  defines program
    DictOperationsDemo()
      stdout <- Stdout()

      ages <- {"Alice": 30, "Bob": 25, "Charlie": 35}

      // === CONTAINS (KEY CHECK) ===

      hasAlice <- ages contains "Alice"
      stdout.println(`Contains Alice: ${hasAlice}`)

      if "Bob" is in ages
        stdout.println("Found Bob")

      if "Zara" is not in ages
        stdout.println("Zara not found")

      // === KEYS AND VALUES ITERATORS ===

      keyIter <- ages.keys()
      while keyIter?
        stdout.println(`Key: ${keyIter.next()}`)

      valIter <- ages.values()
      while valIter?
        stdout.println(`Value: ${valIter.next()}`)

      // === MERGING DICTS ===

      d1 <- {"x": 1, "y": 2}
      d2 <- {"y": 99, "z": 3}

      // + creates a new merged dict
      merged <- d1 + d2
      stdout.println(`Merged: ${merged}`)

      // :~: merge operator mutates left-hand side
      d1 :~: d2
      stdout.println(`After merge: ${d1}`)

      // === COPY ===

      copied <- Dict() of (String, Integer)
      copied :=: ages
      stdout.println(`Copied: ${copied}`)

      // === COMPARISON ===

      c1 <- {"a": 1, "b": 2}
      c2 <- {"a": 1, "b": 2}
      c3 <- {"x": 9}

      require c1 == c2
      require c1 <> c3
      stdout.println(`c1 == c2: ${c1 == c2}`)
      stdout.println(`c1 <> c3: ${c1 <> c3}`)

      // === STRING AND JSON CONVERSION ===

      asString <- $ages
      stdout.println(`As string: ${asString}`)
      stdout.println(`As JSON: ${$ ages}`)

      // === HASHCODE ===

      hash <- #? ages
      stdout.println(`Hash: ${hash}`)

      // === EMPTY DICT IS SET ===

      emptyDict <- Dict() of (String, Integer)
      require emptyDict?
      require emptyDict is empty
      stdout.println(`Empty dict isSet: ${emptyDict?}`)
      stdout.println(`Empty dict isEmpty: ${emptyDict is empty}`)

Java: HashMap .containsKey(), .keySet(), .values(), .putAll() for merge, no operator overloading. Python: 'in' operator, .keys(), .values(), dict union | (3.9+), == comparison. JavaScript: Object.keys(), Object.values(), spread for merge, no == comparison. Rust: .contains_key(), .keys(), .values(), .extend() for merge. Go: comma-ok idiom, no built-in merge or comparison. Kotlin: 'in' operator, .keys, .values, + for merge. EK9: 'contains'/'is in'/'is not in', .keys()/.values() iterators, + for new merged dict, :~: for in-place merge, :=: deep copy, == comparison, $$ for JSON.

beginner compare contains copy dict empty first hashcode intro iterator json keys merge operations set start values

dict operations
dictionary methods
dict API

Explanation: Dict has no get() method in EK9. Use the contains operator for key existence checks, or getOrDefault() for value retrieval with a fallback. See ek9 -h E50060 for details.


Question: What advanced features do Integer and Float support?

How do mod, rem, and bitwise operators work on Integer in EK9?

How do I format numbers with locale-specific separators in EK9?

What happens when I mix Float and Integer in an expression?

Answer:
Beyond basic arithmetic (see Q39), EK9 Integer and Float support mod/rem, factorial, bitwise logic, increment/decrement, mixed-type operations, unset propagation, locale formatting, and stream pipelines.

MOD VS REM
Both give the remainder after division, but differ for negative numbers. mod always returns a non-negative result (mathematical modulo). rem preserves the sign of the dividend (like Java %).
  17 mod 5 = 2,    17 rem 5 = 2      (identical for positives)
  -17 mod 5 = 3,   -17 rem 5 = -2    (differ for negatives)
mod is useful for wrapping (clock arithmetic, array indexing). rem matches C/Java % behaviour.

FACTORIAL
The ! operator computes factorial:
  five <- 5
  five! returns 120 (5 * 4 * 3 * 2 * 1)

BITWISE: AND, OR, XOR
Integer supports bitwise logic operators:
  0b11001100 and 0b10101010 = 0b10001000
  0b11001100 or 0b10101010 = 0b11101110
  0b11001100 xor 0b10101010 = 0b01100110
IMPORTANT: Integer does NOT have shift operators. Use the Bits type for shifting, rotation, and variable-width bit sequences. Convert with Bits(integerValue).

INCREMENT AND DECREMENT
Both Integer and Float support ++ and --:
  counter <- 10
  counter++ gives 11,  counter-- gives 10
  fVal <- 1.5
  fVal++ gives 2.5,  fVal-- gives 1.5

MIXED FLOAT AND INTEGER OPERATIONS
Float operators accept both Float and Integer arguments:
  3.5 * 4 gives Float 14.0,  3.5 + 4 gives Float 7.5
Integer operators only accept Integer. To convert Integer to Float, use #^ promote (see Q25).

UNSET PROPAGATION
Operations involving unset values return unset:
  unsetInt <- Integer()
  unsetInt + 1 is unset
  unsetFloat <- Float()
  unsetFloat + 1.0 is unset
This follows the tri-state principle (see Q29): operations on unknown values produce unknown results.

LOCALE FORMATTING
Both types format through the Locale type:
  Locale("en_GB").format(1234567) gives "1,234,567"
  Locale("de_DE").format(1234567) gives "1.234.567"
Float adds decimal place control:
  Locale("en_GB").format(3.14159, 2) gives "3.14"
  Locale("de_DE").format(3.14159, 4) gives "3,1416"
See Q44 for the full Locale API.

FOR-RANGE WITH STEP AND STREAM PIPELINE
Integer integrates with for-range and stream pipelines:
  theValues <- List() of Integer
  intSum <- for i in 1 ... 11 by 2 | tee in theValues | collect as Integer
Generates 1, 3, 5, 7, 9, 11 (step by 2), tees each into theValues, collects the sum (36).

See Q39 for Integer and Float basics (literals, arithmetic, division by zero, promote). See Q40 for the Bits type. See Q44 for full Locale formatting. See Q240 for arithmetic and mathematical operators (mod, rem, abs, sqrt, ^).

defines module qa.numeric.advanced

  defines program
    NumericAdvancedDemo()
      stdout <- Stdout()

      // === MOD VS REM ===

      stdout.println(`17 mod 5 = ${17 mod 5}`)
      stdout.println(`17 rem 5 = ${17 rem 5}`)
      stdout.println(`-17 mod 5 = ${-17 mod 5}`)
      stdout.println(`-17 rem 5 = ${-17 rem 5}`)

      // === FACTORIAL ===

      five <- 5
      stdout.println(`5! = ${five!}`)

      // === BITWISE LOGIC ===

      x <- 0b11001100
      y <- 0b10101010

      stdout.println(`x and y: ${x and y}`)
      stdout.println(`x or y: ${x or y}`)
      stdout.println(`x xor y: ${x xor y}`)

      // For shift operations, use the Bits type
      bitValue <- Bits(x)
      stdout.println(`Bits of x: ${bitValue}`)

      // === INCREMENT AND DECREMENT ===

      counter <- 10
      counter++
      stdout.println(`After ++: ${counter}`)
      counter--
      stdout.println(`After --: ${counter}`)

      fVal <- 1.5
      fVal++
      stdout.println(`Float after ++: ${fVal}`)
      fVal--
      stdout.println(`Float after --: ${fVal}`)

      // === MIXED FLOAT AND INTEGER ===

      product <- 3.5 * 4
      sum <- 3.5 + 4
      stdout.println(`3.5 * 4 = ${product}`)
      stdout.println(`3.5 + 4 = ${sum}`)

      // === UNSET PROPAGATION ===

      unsetInt <- Integer()
      propagatedInt <- unsetInt + 1
      stdout.println(`Unset int + 1 isSet: ${propagatedInt?}`)

      unsetFloat <- Float()
      propagatedFloat <- unsetFloat + 1.0
      stdout.println(`Unset float + 1.0 isSet: ${propagatedFloat?}`)

      // === LOCALE FORMATTING ===

      enGB <- Locale("en_GB")
      deutsch <- Locale("de_DE")
      largeNum <- 1234567

      stdout.println(`GB: ${enGB.format(largeNum)}`)
      stdout.println(`DE: ${deutsch.format(largeNum)}`)

      pi <- 3.141592653589793
      stdout.println(`GB 2 decimals: ${enGB.format(pi, 2)}`)
      stdout.println(`DE 4 decimals: ${deutsch.format(pi, 4)}`)

      // === FOR-RANGE WITH STEP AND STREAM PIPELINE ===

      theValues <- List() of Integer

      intSum <- for i in 1 ... 11 by 2 | tee in theValues | collect as Integer
      stdout.println(`Values: ${theValues}`)
      stdout.println(`Sum: ${intSum}`)

Java: % operator (rem semantics only), no factorial operator, bitwise &/|/^/<</>>, Integer.parseInt for locale-agnostic formatting, NumberFormat for locale display, IntStream for range pipelines. Python: % (mod semantics), no factorial operator (use math.factorial), bitwise &/|/^/<</>>, locale module. JavaScript: % (rem semantics), no factorial, bitwise &/|/^/<</>>, Intl.NumberFormat for locale. Rust: % (rem), no factorial, bitwise &/|/^/<</>>, no built-in locale formatting. Go: % (rem), no factorial, bitwise &/|/^/<</>>, no built-in locale formatting. EK9: both mod and rem operators, ! factorial, and/or/xor bitwise (shift on Bits type only), Locale.format() built-in, for-range with stream pipeline integration.

advanced and beginner bitwise decrement factorial first format increment intro locale migrate mixed mod or pipeline propagation rem remainder start stream unset xor

Explanation: Integer does NOT have shift operators in EK9. Use the Bits type for shifting, rotation, and variable-width bit sequences. Convert with Bits(integerValue). See ek9 -h E50001 for details.


Question: How does DateTime work with timezones in EK9?

How do I use DateTime literals with UTC and offsets?

How does EK9 handle timezone conversion?

What is the difference between Date and DateTime in EK9?

Answer:
DateTime combines date, time, and timezone into a single built-in type. It is the preferred type for timestamps, events, logging, and any data that needs timezone awareness.

LITERAL SYNTAX
UTC: 2024-06-15T10:30:00Z (the Z means UTC)
With offset: 2020-10-04T12:15:00-05:00 (five hours behind UTC)

CONSTRUCTOR FORMS
DateTime(2024, 06, 15, 10, 30) creates a DateTime with year, month, day, hour, minute. Additional constructors accept 3 args (date only), 4 args (with hour), or 6 args (with seconds).
DateTime() creates an unset DateTime (not now). DateTime().today() returns the current date and time. DateTime().now() also returns the current date and time.

ACCESSORS
All Date accessors: year(), month(), day(), dayOfMonth(), dayOfWeek(), dayOfYear()
Time accessors: hour(), minute(), second()
Timezone: zone() returns the timezone string (e.g. 'Z' or '-05:00'). offSetFromUTC() returns the offset as a Duration (e.g. PT-5H for the -05:00 timezone).
Extract parts: date() returns the Date portion, time() returns the Time portion.

ARITHMETIC
DateTime + Duration gives DateTime. DateTime - Duration gives DateTime. DateTime - DateTime gives Duration. Compound assignment with += and -= also works.

DATE PROMOTES TO DATETIME
The #^ promote operator converts Date to DateTime automatically. You can assign a Date to a DateTime variable directly.

Stream collection: cat durations | collect as DateTime (adds durations from epoch 1970-01-01T00:00:00Z).

See Q31 for Date and Time basics. See Q32 for Duration details. See Q44 for locale formatting with shortFormat(), longFormat(), and other locale-aware display methods.

Use 'ek9 -h DateTime' to see the full API.

See Q31 for date and time. See Q32 for duration. See Q541-Q553 for deep-dive timezone coverage including withSameInstant vs withZone, UTC storage, and cross-timezone comparison.

defines module qa.datetime

  defines program
    DateTimeDemo()
      stdout <- Stdout()

      // DateTime literals with timezone
      meetingUTC <- 2024-06-15T10:30:00Z
      meetingEST <- 2020-10-04T12:15:00-05:00
      stdout.println(`UTC meeting: ${meetingUTC}`)
      stdout.println(`EST meeting: ${meetingEST}`)

      // Constructor form
      constructed <- DateTime(year: 2024, month: 06, dayOfMonth: 15, hour: 10, minute: 30)
      stdout.println(`Constructed matches UTC: ${constructed == meetingUTC}`)

      // DateTime() is unset, DateTime().today() gets current
      unsetDT <- DateTime()
      stdout.println(`Unset isSet: ${unsetDT?}`)
      currentDT <- DateTime().today()
      stdout.println(`Current: ${currentDT}`)

      // All accessors
      stdout.println(`Year: ${meetingUTC.year()}, Month: ${meetingUTC.month()}, Day: ${meetingUTC.day()}`)
      stdout.println(`Hour: ${meetingUTC.hour()}, Minute: ${meetingUTC.minute()}, Second: ${meetingUTC.second()}`)
      stdout.println(`Day of week: ${meetingUTC.dayOfWeek()}, Day of year: ${meetingUTC.dayOfYear()}`)

      // Timezone accessors
      tz <- meetingEST.zone()
      offset <- meetingEST.offSetFromUTC()
      stdout.println(`Timezone: ${tz}, Offset: ${offset}`)

      // Extract date and time parts
      datePart <- meetingUTC.date()
      timePart <- meetingUTC.time()
      stdout.println(`Date part: ${datePart}, Time part: ${timePart}`)

      // DateTime + Duration arithmetic
      meetingEnd <- meetingUTC + PT1H30M
      prepTime <- meetingUTC - PT30M
      stdout.println(`Meeting ends: ${meetingEnd}`)
      stdout.println(`Prep starts: ${prepTime}`)

      // DateTime - DateTime gives Duration
      laterMeeting <- 2024-06-15T14:00:00Z
      meetingGap <- laterMeeting - meetingUTC
      stdout.println(`Gap between meetings: ${meetingGap}`)

      // Compound assignment
      scheduled <- 2024-06-15T09:00:00Z
      scheduled += P1D
      stdout.println(`Rescheduled: ${scheduled}`)

      // Date promotes to DateTime
      birthday <- 1971-02-01
      birthdayDT as DateTime: birthday
      stdout.println(`Promoted: ${birthdayDT}`)

      // Stream collection from durations
      durations as List of Duration := [P1Y1M4D, PT2H30M]
      collectedDT <- cat durations | collect as DateTime
      stdout.println(`Collected: ${collectedDT}`)

      // Locale formatting
      enGB <- Locale("en_GB")
      stdout.println(`Short: ${enGB.shortFormat(meetingUTC)}`)
      stdout.println(`Long: ${enGB.longFormat(meetingUTC)}`)

Java: java.time.ZonedDateTime/OffsetDateTime (Java 8), verbose factory methods, no literals. Python: datetime.datetime with pytz/zoneinfo for timezones, no literals. Rust: no built-in, chrono crate with DateTime<Tz>. Go: time.Time with time.Location, no literals, bizarre reference format. JavaScript: Date has no timezone support beyond local/UTC, Temporal.ZonedDateTime still not finalised. C#: DateTimeOffset reasonable but no literals. EK9: DateTime built-in with literal syntax including timezone offset, zone()/offSetFromUTC() accessors, Duration arithmetic, Date promotion, no imports needed.

beginner combined date datetime first intro offset promote start time timestamp timezone utc zone

Explanation: EK9 DateTime uses short method names: zone(), date(), time() not getZone(), getDate(), getTime(). Triggers E50060 — method not resolved. See ek9 -h DateTime for the full API.

Explanation: EK9 DateTime uses short method names: zone(), date(), time() — not Java-style getZone(), getDate(), getTime(). Using a Java-style name triggers E50060 — method not resolved. Use 'ek9 -h DateTime' to see the full API.


Question: How do I define a class in EK9?

What is the syntax for creating a class in EK9?

Answer:
Classes in EK9 are defined under a 'defines class' section. They use indentation-based syntax with no braces, no semicolons, and no explicit visibility keywords like public or private. Properties are private by default, and methods use '->' for parameters and '<-' for return values.

BASIC CLASS
Define a class with properties and methods:
  defines class
    Person
      name <- String()
      age <- Integer()
Properties use the declaration operator '<-' for type-inferred fields or explicit types.

PROPERTIES
Properties can be declared with type inference or explicit types:
  name <- "Steve"                type inferred as String
  age as Integer: 25             explicit type with initial value
  email <- String()              type inferred, starts unset

METHODS
Methods use '->' for incoming parameters and '<-' for the return:
  greet()
    -> greeting as String
    <- rtn as String: `${greeting} ${name}`
Methods are public by default. Use 'private' or 'protected' to restrict access.

CONSTRUCTOR
Constructors share the class name:
  Person()
    -> name as String
    this.name: name

DEFAULT OPERATOR
The 'default operator' keyword generates standard operators (==, <>, <=>, $, #?, ?) based on fields:
  default operator

DYNAMIC CLASSES
EK9 also supports dynamic classes defined inline. Two forms exist.

Unnamed dynamic class implements a trait inline with variable capture:
  handler <- (msg) with trait of Greeter as class
    override greetPerson()
      <- rtn as String: msg
    default operator ?
Captures variables from enclosing scope. Must override all abstract methods and include 'default operator ?' when capturing.

Named dynamic class creates a reusable type elevated to module scope:
  record <- PersonRecord(name: n, year: y) as class
    describe() as pure
      <- rtn as String: combine name and year
    default operator ?
Named dynamic classes can be used as types in function signatures and collections.

See Q22 for variable declarations. See Q49 for functions. See Q50 for function returns. See Q94 for constructors. See Q95 for field visibility. See Q96 for operators. See Q97 for records vs classes. See Q238 for the fixed operator set and enforcement rules. See Q245 for a complete example of implementing all operators on a custom type. See Q115 for dynamic classes in detail. See Q52 for dynamic functions. See Q596 for function vs method distinction.

defines module qa.oop.defineclass

  defines trait

    Greeter
      greetPerson() as abstract
        <- rtn as String?

  defines class

    Person
      name <- String()
      age <- Integer()

      Person()
        ->
          name as String
          age as Integer
        this.name: name
        this.age: age

      greet()
        -> greeting as String
        <- rtn as String: `${greeting} ${name}`

      default operator

  defines program

    DefineClassDemo()
      stdout <- Stdout()

      // === BASIC CLASS ===

      person <- Person("Steve", 30)
      stdout.println(`Person: ${person}`)

      // === METHODS ===

      message <- person.greet("Hello")
      stdout.println(message)

      // === DEFAULT OPERATOR ===

      same <- Person("Steve", 30)
      stdout.println(`Equal: ${person == same}`)
      stdout.println(`Compare: ${person <=> same}`)

      // === CONSTRUCTOR ===

      another <- Person("Jane", 25)
      stdout.println(`Different: ${person <> another}`)

      // === UNNAMED DYNAMIC CLASS ===

      msg <- "Hi there"
      handler <- (msg) with trait of Greeter as class
        override greetPerson()
          <- rtn as String: msg
        default operator ?

      stdout.println(handler.greetPerson())

      // === NAMED DYNAMIC CLASS ===

      firstName <- "Carol"
      birthYear <- 1990
      entry <- PersonRecord(firstName, birthYear) as class
        describe() as pure
          <- rtn as String: `${firstName} born ${birthYear}`
        operator $ as pure
          <- rtn as String: describe()
        default operator ?

      stdout.println($entry)

Java: class MyClass { private String name; public MyClass(String name) { this.name = name; } } with braces, semicolons, explicit visibility. Python: class MyClass: with __init__(self, name) and self.name, dynamic typing. Rust: struct + impl blocks, no inheritance, traits for behavior. Go: struct types with methods via receiver functions, no classes. Kotlin: class MyClass(val name: String) with concise primary constructor. Swift: class MyClass with init() initializer, properties with let/var, deinit for cleanup, final by default. EK9: indentation-based, no braces or semicolons, properties private by default, constructors named after class, default operator generates standard operators.

basic class constructor create default define indentation method new object object-oriented oop operator property swift syntax

class
define class
create class
object oriented

Explanation: Class properties are always private in EK9. You cannot access 'name' directly from outside the class; define an accessor method instead. See ek9 -h E06180 for details.

Explanation: Class properties must be initialized inline or marked for injection with '!'. A bare 'name as String' with no initializer triggers E08180. Use '<-' with a default value, explicit type with ':' initializer, or 'as String?' for an unset field. See ek9 -h E08180 for details.

handler <- (msg) with trait of Greeter as class
        default operator ?

Explanation: A dynamic class implementing a trait must override ALL abstract methods. Omitting greetPerson() means the abstract method is unimplemented. See ek9 -h E07140 for details.

Explanation: Dynamic classes with captured variables MUST include 'default operator ?' because captures become private fields. Without it the compiler cannot determine the set/unset state. See ek9 -h E07236 for details.

Oracle can generate a class outline with fields, constructor, methods, and default operator declarations.


Question: How do I implement a constructor in EK9?

What is the syntax for EK9 class constructors?

How do constructors work in EK9 classes?

Can I have multiple constructors in an EK9 class?

Answer:
Constructors in EK9 share the name of the class. You can define multiple constructors with different parameter lists. Properties are initialised using the colon assignment operator.

BASIC CONSTRUCTOR
A constructor takes parameters with '->' and assigns to properties:
  Account()
    -> holder as String
    this.holder: holder
The 'this.' prefix disambiguates property from parameter when names match.

PROPERTY INITIALIZATION
Properties can have default values or be set in the constructor:
  balance <- 0.0              default value at declaration
  holder <- String()          starts unset, must be set in constructor

MULTIPLE CONSTRUCTORS
Overload constructors with different parameter lists:
  Account()
    -> holder as String
    this(holder, 0.0)
  Account()
    ->
      holder as String
      initialBalance as Float
    this.holder: holder
    this.balance: initialBalance
Multiple parameters use multi-line '->' blocks (comma-separated on one line is not valid). Use 'this(...)' to chain to another constructor.

COPY CONSTRUCTOR
The copy operator ':=:' copies all fields from another instance:
  operator :=:
    -> from as Account
    holder :=: from.holder
    balance :=: from.balance

DEFAULT CONSTRUCTOR
Use 'default' keyword to hide or control the no-arg constructor:
  default private Account()
This prevents external creation without parameters.

PURE CONSTRUCTORS
Mark a constructor 'as pure' to enforce immutability rules. Pure constructors use ':=?' (guarded assign) instead of ':' for field assignment. Critical rule: if ANY constructor is pure, ALL constructors must be pure (E05190).

ACCESS MODIFIERS
Constructors are public by default. Do NOT write 'public Account()' — the compiler rejects redundant 'public' (E07280). Use 'private' or 'protected' only when restricting access.

DYNAMIC CLASS CONSTRUCTORS
Named dynamic classes can define explicit constructors for reusable types:
  entry <- PersonRecord(name: n, age: a) as class
    PersonRecord()
      ->
        name as String
        age as Integer
      this.name: name
      this.age: age
    default operator ?
The captured variables become fields, and custom constructors control the interface.

See Q93 for class basics. See Q95 for field visibility. See Q104 for uninitialised properties. See Q98 for record operators. See Q118 for builder pattern. See Q580 for constructor delegation patterns. See Q563 for pure constructor assignment rules. See Q115 for dynamic classes. See Q598 for why EK9 has no default parameters and uses constructor overloading instead.

defines module qa.oop.constructors

  defines class

    Account
      holder <- String()
      balance <- 0.0

      default private Account()

      Account()
        -> holder as String
        this(holder, 0.0)

      Account()
        ->
          holder as String
          initialBalance as Float
        this.holder: holder
        this.balance: initialBalance

      holder() as pure
        <- rtn as String: holder

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

      deposit()
        -> amount as Float
        balance += amount

      operator :=:
        -> from as Account
        holder :=: from.holder
        balance :=: from.balance

      default operator ?

  defines program

    ConstructorDemo()
      stdout <- Stdout()

      // === BASIC CONSTRUCTOR ===

      account1 <- Account("Alice")
      stdout.println(`Holder: ${account1.holder()}, Balance: ${account1.balance()}`)

      // === MULTIPLE CONSTRUCTORS ===

      account2 <- Account("Bob", 100.0)
      stdout.println(`Holder: ${account2.holder()}, Balance: ${account2.balance()}`)

      // === CONSTRUCTOR CHAINING ===

      account1.deposit(50.0)
      stdout.println(`After deposit: ${account1.balance()}`)

      // === COPY OPERATOR ===

      account3 <- Account("Temp")
      account3 :=: account2
      stdout.println(`Copied: ${account3.holder()}, Balance: ${account3.balance()}`)

Java: constructors match class name, this() chaining, no named parameters, copy via clone() or copy constructor. Python: __init__(self) only, no overloading (use defaults), copy via copy.deepcopy(). Rust: no constructors, use associated functions like new(), From trait for conversion. Go: no constructors, convention is NewType() factory functions. Kotlin: primary constructor in class header, secondary with constructor keyword, copy() on data classes. EK9: constructors match class name, multiple overloads, this() chaining, ':=:' copy operator, default private constructors.

chain constant constructor copy default initialise object-oriented overload parameter private property this

Explanation: A bare 'holder as String' with no initializer and no injection marker '!' triggers E08180. Use '<-' with a default value, explicit type with ':' initializer, or 'as String?' for an unset field. See ek9 -h E08180 for details.

public Account()
        -> holder as String

Explanation: Constructors and methods are public by default in EK9. Adding an explicit 'public' modifier is redundant and triggers E07280. Simply omit it. Use 'private' or 'protected' only when restricting access. See ek9 -h E07280 for details.

Account() as pure
        -> holder as String
        this(holder, 0.0)

Explanation: If ANY constructor is marked 'as pure', ALL constructors must be pure. Mixing pure and non-pure constructors triggers E05190. Also note pure constructors use ':=?' not ':' for field assignment. See ek9 -h E05190 for details.

Oracle can generate a class with constructor patterns including delegation and field initialisation.


Question: How do I control field visibility in EK9?

Are class properties public or private by default in EK9?

What is the difference between class, record, and component field visibility?

Answer:
EK9 fields have fixed visibility rules that vary by construct type. There are no access modifiers on fields; the construct type determines visibility.

CLASSES: ALWAYS PRIVATE
Class properties are always private. Access them through explicit accessor methods:
  defines class
    Person
      name <- String()
      name() as pure
        <- rtn as String: name
You write accessor methods to expose properties. There is no getter/setter annotation.

RECORDS: ALWAYS PUBLIC
Record properties are always public. Access them directly:
  defines record
    Point
      x <- 0.0
      y <- 0.0
Callers use 'point.x' directly. Records are data carriers by design.

COMPONENTS: ALWAYS PRIVATE
Component properties are always private, just like classes. Components are large-scale containers for dependency injection:
  defines component
    Processor as abstract
      process()
        -> input as String
        <- rtn as String
Concrete components access their own fields internally.

TRAITS: NO FIELDS
Traits cannot have properties at all. They define method contracts with no retained state:
  defines trait
    Printable
      show() as pure abstract
        <- rtn as String

NO ACCESS MODIFIERS ON FIELDS
Unlike methods (which can be public, protected, or private), fields have no access modifier in the grammar. Writing 'private name <- String()' or 'public x <- 0.0' causes a parse error. The construct type alone determines field visibility.

FIELDS MUST BE INITIALISED
All fields must be initialised at declaration, set in a constructor, or marked for injection with '!' on components. Uninitialised fields cause E08180.

See Q93 for class basics. See Q94 for constructors. See Q97 for records vs classes. See Q106 for traits. See Q111 for components. See Q573 for method access modifier rules.

defines module qa.oop.visibility

  defines trait

    Printable
      show() as pure abstract
        <- rtn as String?

  defines class

    Person with trait of Printable
      name <- String()
      age <- Integer()

      Person()
        ->
          name as String
          age as Integer
        this.name: name
        this.age: age

      name() as pure
        <- rtn as String: name

      age() as pure
        <- rtn as Integer: age

      override show() as pure
        <- rtn as String: `Person: ${name}, age ${age}`

      default operator

  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

    Processor as abstract

      process() abstract
        -> input as String
        <- rtn as String?

      default operator ?

  defines component

    UpperCaseProcessor extends Processor
      prefix <- "PROCESSED"

      override process()
        -> input as String
        <- rtn as String: `${prefix}: ${input.upperCase()}`

      default operator ?

  defines program

    VisibilityDemo()
      stdout <- Stdout()

      // === CLASS FIELDS: always private, use accessor methods ===

      person <- Person("Alice", 30)
      stdout.println(`Name via accessor: ${person.name()}`)
      stdout.println(`Age via accessor: ${person.age()}`)
      stdout.println(person.show())

      // === RECORD FIELDS: always public, access directly ===

      point <- Point(3.0, 4.0)
      stdout.println(`Direct access x: ${point.x}, y: ${point.y}`)

      // === COMPONENT FIELDS: always private, used internally ===

      processor <- UpperCaseProcessor()
      stdout.println(processor.process("hello world"))

      // === TRAIT: no fields, just method contracts ===

      printable as Printable: person
      stdout.println(`Via trait: ${printable.show()}`)

      // === COMPARISON ===

      point2 <- Point(3.0, 4.0)
      stdout.println(`Points equal: ${point == point2}`)

      person2 <- Person("Alice", 30)
      stdout.println(`Persons equal: ${person == person2}`)

Java: fields private by convention with explicit getters/setters, no enforcement unless you add private. Python: no true private, underscore convention, properties via @property decorator. Rust: struct fields private by default outside module, pub for public. Go: uppercase for exported, lowercase for unexported. Kotlin: val/var properties with automatic getters/setters, backing field access. C#: fields private by default, properties with get/set accessors. EK9: class and component properties always private (no modifier allowed), record properties always public, traits have no fields. The construct type determines visibility with no override possible.

E08180 accessor class component encapsulation field getter object-oriented private property public record setter trait visibility

field visibility
access modifier
public private
encapsulation

Explanation: Class fields are always private in EK9. There is no 'private' keyword because it is the only option. Access fields through accessor methods like 'name()'. See ek9 -h E06180 for details.

Explanation: EK9 does not use Java-style 'getX()' accessors. The accessor method matches the field name: 'name()' not 'getName()'. See ek9 -h E50060 for details.

Explanation: Component fields are always private. Even though the field 'prefix' exists on UpperCaseProcessor, it cannot be accessed from outside the component. Use the public methods instead. See ek9 -h E06180 for details.


Question: How do operators work in EK9?

How do I define custom operators on an EK9 type?

What constructs support operators in EK9?

How does operator overloading work in EK9?

Which EK9 constructs can have operators?

Answer:
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.

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?}`)

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

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

operator
operator overloading
custom operator

Explanation: 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.

Explanation: 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.

Explanation: 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.

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

Explanation: 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.


Question: What is the difference between classes and records in EK9?

When should I use a record instead of a class in EK9?

How do records differ from classes in EK9?

Answer:
Classes and records serve different purposes in EK9. Classes encapsulate behaviour with private properties and methods. Records expose data with public properties and are ideal for eliminating data clumps.

KEY DIFFERENCES
Classes: properties always private, behaviour-focused, methods provide controlled access, can have methods.
Records: properties always public, data-focused, direct field access, constructors and operators only (no methods).
Both support constructors, operators, and inheritance.

INHERITANCE — SAME OPT-IN RULE FOR BOTH
Records, like classes, are closed-by-default. Mark a base record (or class) 'as open' to allow extension. A child can override operators and call 'super.<op>(...)' to chain into the parent. See Q1297 for the full record-to-record inheritance pattern. See Q101 for closed-by-default rationale, Q102 for 'as open' details.

RECORDS ELIMINATE DATA CLUMPS
When multiple parameters appear together across several functions and methods, that is a data clump. Introducing a record groups them into a named type:
  defines record
    Coordinate
      x <- 0.0
      y <- 0.0
Instead of passing x and y separately to every function, pass a single Coordinate. This reduces parameter counts, improves readability, and gives the group a meaningful name.

MUTABILITY VIA PURE, NOT IMMUTABLE TYPES
Unlike Java records (immutable) or Kotlin data classes (val vs var), EK9 records are mutable. Mutability is controlled at the function and method level via 'as pure', not at the data structure level. A pure function cannot modify any of its parameters, so the same record is safely mutable in normal code and protected in pure contexts. This preserves the Liskov Substitution Principle: a mutable subtype can always be used where the parent is expected. Languages that make records immutable break LSP because an immutable subtype cannot substitute for a mutable parent.

WHEN TO USE WHICH
Use a record when: you have a data clump, need a data transfer object, or want transparent data structures that functions operate on.
Use a class when: you need to hide implementation details, enforce invariants through methods, or have complex behaviour tied to state.

See Q93 for class basics. See Q95 for visibility details. See Q96 for operators across constructs. See Q98 for record operators. See Q116 for default operator. See Q119 for constructs overview. See Q241 for mutation operators.

defines module qa.oop.classvsrecord

  defines record

    <?-
      Groups x and y into a named type,
      eliminating the data clump of passing
      two separate Float parameters everywhere.
    -?>
    Coordinate
      x <- 0.0
      y <- 0.0

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

      default operator

  defines class

    Position
      x <- 0.0
      y <- 0.0

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

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

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

      default operator

  defines function

    <?-
      Pure function: Coordinate is safely passed in
      without risk of mutation. No immutable type needed.
    -?>
    distanceFromOrigin() as pure
      -> point as Coordinate
      <- rtn as Float: sqrt(point.x * point.x + point.y * point.y)

    <?-
      Normal function: can modify the record freely.
      Same type works in both pure and non-pure contexts.
    -?>
    translate()
      ->
        point as Coordinate
        dx as Float
        dy as Float
      point.x += dx
      point.y += dy

  defines program

    ClassVsRecordDemo()
      stdout <- Stdout()

      // === RECORD: public fields, direct access ===

      coord <- Coordinate(3.0, 4.0)
      stdout.println(`Record x: ${coord.x}, y: ${coord.y}`)

      // === RECORDS ELIMINATE DATA CLUMPS ===
      // Instead of passing x and y separately, pass Coordinate

      dist <- distanceFromOrigin(coord)
      stdout.println(`Distance from origin: ${dist}`)

      // === PURE CONTROLS MUTABILITY, NOT THE TYPE ===
      // Same Coordinate is mutable here, protected in pure functions

      translate(coord, 1.0, 2.0)
      stdout.println(`After translate: ${coord.x}, ${coord.y}`)

      // === CLASS: private fields, accessor methods ===

      pos <- Position(3.0, 4.0)
      stdout.println(`Class x: ${pos.x()}, y: ${pos.y()}`)

      // === BOTH support operators ===

      coord2 <- Coordinate(3.0, 4.0)
      stdout.println(`Records equal: ${coord == coord2}`)

      pos2 <- Position(3.0, 4.0)
      stdout.println(`Classes equal: ${pos == pos2}`)

      // === Records ideal for data transfer ===

      stdout.println(`Coordinate: ${coord}`)
      stdout.println(`Position: ${pos}`)

Java: records (Java 16+) are immutable with auto-generated accessors, breaking LSP for mutable hierarchies. Python: dataclasses are mutable by default, frozen=True makes them immutable. Rust: structs are mutable by default, 'mut' controls mutability at the binding level. Go: only structs, all mutable, no immutability mechanism. Kotlin: data classes use val (immutable) or var (mutable) per field. Swift: structs are value types (copied on assignment), can have methods and computed properties unlike EK9 records, recommended over classes for most data types. EK9: records are mutable, mutability is controlled by 'as pure' on functions and methods rather than at the data structure level. This preserves LSP and separates the concern of data shape from data protection.

Liskov
behaviour
class
data
data clump
difference
encapsulation
immutable
mutability
object-oriented
private
public
pure
record
struct
swift
transfer
visibility

class vs record
record
struct
data class

Explanation: Class properties are private by default. Accessing 'x' directly on a Position class instance fails; use the accessor method 'x()' instead. Records like Coordinate allow 'coord.x' directly. See ek9 -h E06180 for details.

Explanation: There is no 'Point' type in this module. EK9 type names are case-sensitive and must match exactly. The class is called 'Position', not 'Point'. See ek9 -h E50001 for details.

getName()
        <- rtn as String: name

Explanation: Records can only have constructors and operators, not methods. Adding a method such as 'getName()' triggers E07290. Use a standalone function or switch to a class if you need methods. See ek9 -h E07290 for details.

Explanation: A class cannot extend a record because they are different construct types (genus). Classes extend classes and records extend records. Use composition instead of cross-genus inheritance. See ek9 -h E50020 for details.

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

stdout.println(`Class x: ${pos.getX()}, y: ${pos.y()}`)

Explanation: EK9 does not use Java-style 'getX()' accessors. Class accessor methods match the property name: 'pos.x()' not 'pos.getX()'. See ek9 -h E50060 for details.


Question: How do record operators work 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?

Answer:
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.

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

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.

JSON
collection
comparison
copy
data clump
default
field
hashcode
merge
mutation
operator
pipeline
record
replace
serialisation
sort
stream
string

      getDisplay() as pure
        <- rtn as String: host

      // === COMPARISON: enables sorting and deduplication ===

Explanation: 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.

Explanation: 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.

Explanation: 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.