Question: Create me a constrained EmailAddress type as String validated by a regex, then write a registerUser function that uses a guard expression to accept only valid emails.

Define an EmailAddress constrained type and use it with a guard.

Show me a validated email type that is unset on bad input.

Implement a constrained String type with regex matching for emails.

Write a registration function that rejects malformed email addresses.

Answer:
EK9 constrained types let you build domain types from existing primitive types with a validation rule. There are two construction paths. A BARE constructor — EmailAddress(value) — ASSERTS the value is valid: a set value that violates the regex PANICS at runtime (and an invalid literal constant is the compile error E08260, so it never reaches runtime). For UNTRUSTED/boundary input use the fallible factory EmailAddress().of(value), which returns an UNSET instance on failure — not an exception — and composes cleanly with guard expressions to filter bad data at the boundary.

DEFINING THE CONSTRAINED TYPE
  defines type
    EmailAddress as String constrain as
      matches /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/

The type is declared in a 'defines type' section. The fallible factory (e.g. EmailAddress().of("alice@example.com")) attempts to validate the regex; if it fails, the result is unset.

FUNCTION USING THE TYPE
  registerUser() as pure
    ->
      userName as String
      email as EmailAddress
    <- rtn as String: `Registered ${userName} with ${email}`

The function takes EmailAddress (not String). The type system makes it impossible to call this function with an unvalidated value.

GUARD AT THE CALL SITE
Use the fallible factory in a guard expression to attempt construction and only call the function on success:
  if validEmail <- EmailAddress().of("alice@example.com")
    msg <- registerUser("Alice", validEmail)
    stdout.println(msg)
  else
    stdout.println("Invalid email format")

  if invalidEmail <- EmailAddress().of("not-an-email")
    msg <- registerUser("Bob", invalidEmail)
    stdout.println(msg)
  else
    stdout.println("Invalid email format")

The '.of(value)' factory returns an unset EmailAddress when validation fails (a bare EmailAddress(value) would PANIC on a set invalid value instead). The block under 'if' only runs when construction succeeded. The 'else' handles the rejected case explicitly.

KEY ADVANTAGES
1. Validation lives in the TYPE, not scattered through call sites.
2. Functions taking EmailAddress are guaranteed to receive valid values.
3. No exceptions for predictable input failures — unset is the natural representation.
4. The compiler enforces that you check 'isSet' before use.

See Q269 for input validation patterns. See Q257 for constrained type basics. See Q74 for guard expressions.

defines module qa.security.emailtype

  defines type

    EmailAddress as String constrain as
      matches /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/

  defines function

    registerUser() as pure
      ->
        userName as String
        email as EmailAddress
      <- rtn as String: `Registered ${userName} with ${email}`

  defines program

    EmailValidationDemo()
      stdout <- Stdout()

      if validEmail <- EmailAddress().of("alice@example.com")
        msg <- registerUser("Alice", validEmail)
        stdout.println(msg)
      else
        stdout.println("Invalid email format")

      if invalidEmail <- EmailAddress().of("not-an-email")
        msg <- registerUser("Bob", invalidEmail)
        stdout.println(msg)
      else
        stdout.println("Invalid email format")

Java: validate-on-setter or Bean Validation @Email annotation. Kotlin: Result<Email> or sealed Either type. Rust: newtype struct Email(String) with try_from validation. Scala: Refined types or smart constructors. Python: pydantic EmailStr or manual regex check. EK9: 'defines type X as String constrain as matches /regex/' then X().of(value) produces unset on validation failure, composing with guard expressions for type-safe validation at the boundary.

constrained type
domain type
email
guard
matches
newtype
regex
validation

constrained type with regex
email validation
guard with constructor

EmailAddress as UserRecord constrain as
      matches /^.+@.+$/

Explanation: Constrained types can only be built from primitive types like String, Integer or Float. User-defined records and classes cannot be constrained because they lack the comparison and pattern-matching semantics required. See ek9 -h E50010 for details.


Question: Create me a generic Validator class of type T with two abstract methods (check and describe), and a dynamic class implementation that validates a Date by checking it falls within a known year range.

Write a generic abstract Validator with two abstract methods, instantiated for Date.

Implement a generic Validator that has both a check and a describe method.

Show me how to extend a multi-method generic abstract for the Date type.

Build a parameterised Validator with check and describe overrides for Date values.

Answer:
When a generic abstract class declares more than one abstract method, the dynamic class extending it must override EVERY abstract method. The compiler refuses partial implementations.

GENERIC ABSTRACT WITH MULTIPLE ABSTRACTS
  Validator of type T as abstract
    check() as abstract
      -> arg0 as T
      <- rtn as Boolean?
    describe() as abstract
      -> arg0 as T
      <- rtn as String?
    override operator ? as pure
      <- rtn as Boolean: true

DYNAMIC CLASS WITH ALL OVERRIDES
Every abstract must be supplied an override. Here we instantiate the generic for the Date type and check whether a date falls between two known years:
  appointmentValidator <- () extends Validator of Date as class
    override check()
      -> arg0 as Date
      <- rtn as Boolean: arg0 >= EARLIEST_VALID_DATE and arg0 <= LATEST_VALID_DATE
    override describe()
      -> arg0 as Date
      <- rtn as String: "Date " + $arg0

Each overridden method independently uses the type-substituted parameter (arg0 as Date). Methods can refer to date-specific operators like '>=' that the abstract knows nothing about.

USAGE
  scheduledDate <- 2025-06-15
  stdout.println($appointmentValidator.check(scheduledDate))
  stdout.println(appointmentValidator.describe(scheduledDate))

KEY POINT
The SAME generic class works for ANY type T where the override body can produce sensible behaviour. Use String for label processing, Date for temporal validation, Money for currency checking, Dict for lookup containers — the generic carries no assumptions beyond what its abstract methods require.

See Q1234 for single-method generic extension. See Q197 for extending generics. See Q1235 for capture combined with generic extension.

defines module qa.genericsdeep.validatordatemultimethod

  defines constant

    EARLIEST_VALID_DATE <- 2024-01-01

    LATEST_VALID_DATE <- 2030-12-31

  defines class

    Validator of type T as abstract

      check() as abstract
        -> arg0 as T
        <- rtn as Boolean?

      describe() as abstract
        -> arg0 as T
        <- rtn as String?

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

  defines program

    ValidatorDateDemo()
      stdout <- Stdout()

      appointmentValidator <- () extends Validator of Date as class
        override check()
          -> arg0 as Date
          <- rtn as Boolean: arg0 >= EARLIEST_VALID_DATE and arg0 <= LATEST_VALID_DATE
        override describe()
          -> arg0 as Date
          <- rtn as String: "Date " + $arg0

      scheduledDate <- 2025-06-15
      stdout.println($appointmentValidator.check(scheduledDate))
      stdout.println(appointmentValidator.describe(scheduledDate))

Java: anonymous inner class extending Validator<Date> { @Override check, @Override describe }. Kotlin: object : Validator<LocalDate>() { override fun check, override fun describe }. Scala: new Validator[LocalDate] { def check; def describe }. Rust: impl Validator<NaiveDate> for ValidatorImpl { fn check; fn describe }. EK9: dynamic class form with all abstract methods overridden in one expression, parameterised for any type.

Date
Validator
abstract
dynamic class
generic
multiple methods
of type T
override

multi-method generic
Date type parameter
abstract method completeness

Explanation: Both abstract methods (check and describe) must be overridden in the dynamic class. Leaving describe unimplemented means the method cannot be resolved at the call site. See ek9 -h E07140 for details.


Question: Write me a generic Transformer class with two type parameters K and V, and a dynamic class implementation that maps String keys to Integer values via a transform method.

Create a generic abstract class with two type parameters K and V.

Show me how to declare a generic with multiple type parameters in EK9.

Implement a Transformer of (K, V) and use it for String to Integer mapping.

Build a parameterised type with key and value type parameters.

Answer:
Generic classes can declare multiple type parameters using the syntax 'of type (K, V)' with parameters in parentheses. Both parameters can appear in the abstract method signatures and are independently substituted at the extension site.

GENERIC WITH TWO TYPE PARAMETERS
  Transformer of type (K, V) as abstract
    transform() as abstract
      -> key as K
      <- rtn as V?
    override operator ? as pure
      <- rtn as Boolean: true

The parameter names K and V are conventional but you can use any identifier. The 'of type (K, V)' form is required when there is more than one type parameter.

DYNAMIC CLASS WITH BOTH SUBSTITUTIONS
When extending, supply both concrete types in the same parenthesised form:
  transformer <- () extends Transformer of (String, Integer) as class
    override transform()
      -> key as String
      <- rtn as Integer: length of key

Both parameters are substituted simultaneously: K becomes String and V becomes Integer. The override signature uses the concrete types throughout.

USAGE
  result <- transformer.transform("EK9")
  stdout.println($result)

KEY POINTS
1. Use 'of type (K, V)' with parentheses for multi-parameter generics.
2. Use 'of (Type1, Type2)' at the extension site to supply both.
3. Each type parameter can appear in any position in any method signature.

See Q196 for multi-parameter generics. See Q1234 for single-parameter generic basics. See Q1242 for multi-method generics.

defines module qa.genericsdeep.transformerkv

  defines class

    Transformer of type (K, V) as abstract

      transform() as abstract
        -> key as K
        <- rtn as V?

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

  defines program

    TransformerKVDemo()
      stdout <- Stdout()

      transformer <- () extends Transformer of (String, Integer) as class
        override transform()
          -> key as String
          <- rtn as Integer: length of key

      result <- transformer.transform("EK9")
      stdout.println($result)

Java: class Transformer<K, V> { abstract V transform(K key); }. Kotlin: abstract class Transformer<K, V> { abstract fun transform(key: K): V }. Scala: trait Transformer[K, V] { def transform(key: K): V }. Rust: trait Transformer<K, V> { fn transform(&self, key: K) -> V }. EK9: 'of type (K, V)' parenthesised form for multi-parameter generics.

K
Transformer
V
generic
key value
multi parameter
of type
two type parameters

multi-parameter generic
of type (K, V)
two type substitutions

Explanation: When declaring multiple type parameters, parentheses are required: 'of type (K, V)'. Without parentheses the parser cannot tell where the parameter list ends. See ek9 -h E50001 for details.


Question: Create me a dispatcher for a deep class hierarchy (Shape -> Polygon -> Quadrilateral -> Square) where the Polygon handler catches Quadrilateral but a more specific Square handler exists.

Show me how cost-based dispatch picks the closest handler in a deep inheritance tree.

Write a dispatcher that demonstrates exact-match wins over superclass-match.

Build a renderer that dispatches across a four-level hierarchy.

Implement a dispatcher with handlers for some intermediate types but not all.

Answer:
Dispatchers select the handler whose parameter type most closely matches the runtime type of the argument, walking up the inheritance chain until a handler is found. This is COST-BASED dispatch — exact match has cost 0, parent has cost 1, grandparent cost 2, and so on.

DEEP INHERITANCE
  Shape as abstract
    default operator ?
  Polygon is Shape as open
    default operator ?
  Quadrilateral is Polygon as open
    default operator ?
  Square is Quadrilateral
    default operator ?

Note 'as open' on intermediate classes — without it, EK9 closes the class and you cannot extend it further. Square is closed (default) because it is a leaf.

DISPATCHER WITH PARTIAL HANDLERS
Three handlers cover four types in the hierarchy:
  GeometryRenderer
    render() as dispatcher
      -> shape as Shape
      <- rtn as String: "Shape handler"
    render()
      -> shape as Polygon
      <- rtn as String: "Polygon handler"
    render()
      -> shape as Square
      <- rtn as String: "Square handler"

DISPATCH BEHAVIOUR
  renderer.render(Polygon())        // "Polygon handler" (exact match)
  renderer.render(Quadrilateral())  // "Polygon handler" (Quadrilateral has no handler, walks up to Polygon)
  renderer.render(Square())         // "Square handler" (exact match wins over Polygon)

KEY INSIGHT
For Quadrilateral, the dispatcher walks up the hierarchy: no Quadrilateral handler -> try Polygon (found, use it). For Square, the exact handler exists so it wins even though Polygon is also a valid match (lower cost wins).

This avoids the visitor pattern boilerplate found in Java/C#: you only need handlers for the types you actually want to differentiate.

See Q620 for hierarchy dispatch rules. See Q616 for ambiguity detection. See Q1236 for sealed exhaustive dispatch.

defines module qa.dispatcher.deephierarchy

  defines class

    Shape as abstract
      default operator ?

    Polygon is Shape as open
      default operator ?

    Quadrilateral is Polygon as open
      default operator ?

    Square is Quadrilateral
      default operator ?

    GeometryRenderer

      render() as dispatcher
        -> shape as Shape
        <- rtn as String: "Shape handler"

      render()
        -> shape as Polygon
        <- rtn as String: "Polygon handler"

      render()
        -> shape as Square
        <- rtn as String: "Square handler"

  defines program

    DeepHierarchyDispatchDemo()
      stdout <- Stdout()
      renderer <- GeometryRenderer()

      stdout.println(renderer.render(Polygon()))
      stdout.println(renderer.render(Quadrilateral()))
      stdout.println(renderer.render(Square()))

Java: visitor pattern with accept(visitor) on each subclass — must implement EVERY visit method. Scala: pattern matching on case classes — order matters, exhaustiveness optional. Kotlin: when expression with class checks — must have 'else' or sealed classes. Rust: match on enum variants — exhaustive by default. EK9: dispatcher walks the hierarchy by cost, with explicit fallback in the entry method's body.

cost based
deep hierarchy
dispatcher
exact match
fallback
inheritance
open
walk up

cost-based dispatch
deep hierarchy dispatcher
open class hierarchy

Explanation: EK9 classes are CLOSED by default. To allow further extension (Quadrilateral extending Polygon), declare the parent 'as open'. Leaf classes can stay closed. See ek9 -h E05030 for details.


Question: Create me a two-parameter dispatcher Combiner with handlers for (Circle, Circle) and (Circle, Rectangle), demonstrating that parameter order matters and unmatched combinations fall back to the entry method.

Show me a dispatcher that resolves on TWO parameter types simultaneously.

Implement a combine method that dispatches differently for different shape pairs.

Build a multi-method-style dispatcher with two parameters in EK9.

Write a dispatcher demonstrating asymmetric handler coverage on parameter pairs.

Answer:
EK9 dispatchers can resolve on multiple parameter types at once. With two parameters, the dispatcher matches on the runtime type of EACH parameter independently, so handler selection considers the full combination, not just the first parameter.

SHAPE HIERARCHY
  Shape as abstract
    default operator ?
  Circle is Shape
    default operator ?
  Rectangle is Shape
    default operator ?

TWO-PARAMETER DISPATCHER
  Combiner
    combine() as dispatcher
      ->
        s1 as Shape
        s2 as Shape
      <- rtn as String: "Generic"
    combine()
      ->
        s1 as Circle
        s2 as Circle
      <- rtn as String: "Circle-Circle"
    combine()
      ->
        s1 as Circle
        s2 as Rectangle
      <- rtn as String: "Circle-Rectangle"

The entry method takes both parameters as the parent type Shape. Each handler narrows BOTH parameters to specific subtypes.

DISPATCH BEHAVIOUR
  combiner.combine(circle, circle)        // "Circle-Circle" — exact match
  combiner.combine(circle, rectangle)     // "Circle-Rectangle" — exact match
  combiner.combine(rectangle, circle)     // "Generic" — no (Rectangle, Circle) handler
  combiner.combine(rectangle, rectangle)  // "Generic" — no handler for this pair

KEY INSIGHT
Parameter ORDER matters: (Circle, Rectangle) and (Rectangle, Circle) are DIFFERENT dispatch keys. If you want them to behave the same, you must declare both handlers. Unhandled combinations fall through to the entry method's body, which acts as the default.

This is true multiple dispatch — found in CLOS, Julia, Common Lisp — but rare in mainstream languages. Java/C# require visitor or instanceof chains; EK9 makes it a one-liner per case.

See Q619 for two-parameter dispatch basics. See Q620 for hierarchy rules. See Q1244 for deep hierarchy single-parameter dispatch.

defines module qa.dispatcher.twoparam

  defines class

    Shape as abstract
      default operator ?

    Circle is Shape
      default operator ?

    Rectangle is Shape
      default operator ?

    Combiner

      combine() as dispatcher
        ->
          s1 as Shape
          s2 as Shape
        <- rtn as String: "Generic"

      combine()
        ->
          s1 as Circle
          s2 as Circle
        <- rtn as String: "Circle-Circle"

      combine()
        ->
          s1 as Circle
          s2 as Rectangle
        <- rtn as String: "Circle-Rectangle"

  defines program

    TwoParamDispatchDemo()
      stdout <- Stdout()
      combiner <- Combiner()

      circle <- Circle()
      rectangle <- Rectangle()

      stdout.println(combiner.combine(circle, circle))
      stdout.println(combiner.combine(circle, rectangle))
      stdout.println(combiner.combine(rectangle, circle))
      stdout.println(combiner.combine(rectangle, rectangle))

Java: nested instanceof or double-dispatch via visitor. Scala: pattern match on tuple (s1, s2) match { case (Circle, Circle) => ... }. Common Lisp / CLOS: defmethod with two specialised parameters — the inspiration. Julia: multimethod with type ascriptions on each parameter. EK9: native two-parameter dispatcher with cost-based selection on both arguments.

asymmetric
combine
dispatcher
fallback
multiple dispatch
parameter order
two parameter

two-parameter dispatch
multiple dispatch
asymmetric handler coverage

combine() as dispatcher
        -> s1 as Shape
        -> s2 as Shape

Explanation: Multiple parameters are declared with a single '->' followed by an indented parameter block. Repeating '->' for each parameter is a parser-level syntax error. See ek9 -h E01082 for details.


Question: Create me an AuditLogger class whose constructor takes a sanitized String log message, where construction with malicious input (file paths, terminal escape sequences, traceback fragments) leaves the logger in a 'rejected' state.

Show me how the sanitized keyword works on a constructor parameter for an audit logger.

Write a logging class that rejects malicious log messages at construction time.

Implement an AuditLogger that validates its incoming message via the sanitized modifier.

Build an object that records whether its construction message was clean or rejected.

Answer:
The 'sanitized' modifier works on constructor parameters identically to function and method parameters. When a class is constructed with malicious input, the constructor sees an UNSET parameter, allowing the object to set itself into a safe default state rather than throwing an exception.

CLASS WITH SANITIZED CONSTRUCTOR PARAMETER
  AuditLogger
    message as String: "<no message>"
    state as String: "empty"

    AuditLogger()
      -> incomingMessage as sanitized String

      if incomingMessage?
        message: incomingMessage
        state: "clean"
      else
        state: "rejected"

    getMessage()
      <- rtn as String: message

    getState()
      <- rtn as String: state

    default operator ?

The field defaults ensure the object is always well-formed. The constructor body branches on 'incomingMessage?' to record whether the input was accepted.

DEMONSTRATING ACCEPTED AND REJECTED INPUTS
This example varies the malicious payload types — file path traversal, ANSI escape codes, traceback fragments, log injection — rather than the usual SQL/XSS strings, to teach the model that 'sanitized' applies to ANY untrusted input, not just web payloads:
  // Clean log message
  log1 <- AuditLogger("User alice logged in successfully")
  // "clean"

  // Path traversal attempt
  log2 <- AuditLogger("file=../../../etc/shadow")
  // "rejected"

  // ANSI escape attempt
  log3 <- AuditLogger("Login\u001b[31m FAILED \u001b[0m")
  // "rejected"

  // Log injection (newline forging)
  log4 <- AuditLogger("action=lookup
2025-01-01 ADMIN_OVERRIDE")
  // "rejected"

KEY ADVANTAGE
The object remains usable even when the input was malicious — it just records 'rejected' instead of 'clean'. The caller can inspect getState() to know the outcome. No exceptions, no try/catch, no nullable result wrapping.

See Q1237 for sanitized function parameters. See Q1247 for sanitized method parameters. See Q272 for defense in depth.

defines module qa.security.auditloggerconstructor

  defines class

    AuditLogger
      message as String: "<no message>"
      state as String: "empty"

      AuditLogger()
        -> incomingMessage as sanitized String

        if incomingMessage?
          message: incomingMessage
          state: "clean"
        else
          state: "rejected"

      getMessage()
        <- rtn as String: message

      getState()
        <- rtn as String: state

      default operator ?

  defines program

    SanitizedAuditLoggerDemo()
      stdout <- Stdout()

      stdout.println("=== AuditLogger Sanitization Demo ===")

      cleanMessage <- "User alice logged in successfully"
      log1 <- AuditLogger(cleanMessage)
      stdout.println("Clean: " + log1.getState())

      pathTraversal <- "file=../../../etc/shadow"
      log2 <- AuditLogger(pathTraversal)
      stdout.println("Path: " + log2.getState())

      ansiEscape <- "Login\u001b[31m FAILED \u001b[0m"
      log3 <- AuditLogger(ansiEscape)
      stdout.println("ANSI: " + log3.getState())

      logInjection <- "action=lookup\n2025-01-01 ADMIN_OVERRIDE"
      log4 <- AuditLogger(logInjection)
      stdout.println("Inject: " + log4.getState())

      stdout.println("=== Complete ===")

Java: validation in setters with throw on invalid input, or Optional<AuditLogger> wrapper. Kotlin: data class with init { require(...) } that throws. Rust: TryFrom<String> for AuditLogger with Result return type. Python: __init__ raises ValueError. EK9: 'sanitized' modifier on the constructor parameter — input is unset if rejected, object always constructs successfully with a state field indicating which path was taken.

ANSI escape
AuditLogger
audit
constructor
input validation
log injection
path traversal
sanitized

sanitized constructor parameter
audit logger validation
log injection prevention

Explanation: The 'sanitized' modifier comes BEFORE the type, AFTER 'as'. The correct form is 'incomingMessage as sanitized String'. See ek9 -h E50001 for details.


Question: Write me a QueryBuilder class with an addClause method that takes a sanitized String fragment, returning 'accepted' for clean fragments and 'rejected' for malicious ones (URL injection, LDAP injection, NoSQL operator injection).

Show me how the sanitized keyword works on a method parameter for a query builder.

Create a QueryBuilder class whose addClause method validates its input via the sanitized modifier.

Implement a method that protects query state from injection through call arguments.

Build a class with a sanitized-parameter method that classifies query fragments.

Answer:
The 'sanitized' modifier works on method parameters identically to constructors and functions. When a method receives malicious input, the parameter is UNSET, so the method can branch on 'fragment?' to handle the rejected case without exceptions.

CLASS WITH SANITIZED METHOD PARAMETER
  QueryBuilder
    accepted as Integer: 0
    rejected as Integer: 0

    default QueryBuilder()

    addClause()
      -> fragment as sanitized String
      <- result as String: "unset"

      if fragment?
        accepted: accepted + 1
        result: "accepted"
      else
        rejected: rejected + 1
        result: "rejected"

    getStats()
      <- rtn as String: `accepted=${accepted} rejected=${rejected}`

The class keeps running counters of accepted and rejected fragments, demonstrating that the object remains usable across many calls — clean fragments increment the accepted counter, malicious ones increment rejected, neither path throws.

DEMONSTRATING DIVERSE INJECTION TYPES
This example deliberately uses injection categories OTHER than SQL/XSS, to teach the model that 'sanitized' protects against any untrusted text:
  qb <- QueryBuilder()

  // Clean equality clause
  qb.addClause("name = 'alice'")           // accepted

  // URL injection (parameter pollution)
  qb.addClause("redirect=http://evil/?p=1") // rejected

  // LDAP injection
  qb.addClause("(|(uid=*)(uid=*))")          // rejected

  // NoSQL operator injection
  qb.addClause("$where: function() { return true }") // rejected

  // Clean range clause
  qb.addClause("age >= 18")                  // accepted

WHEN TO USE METHOD-LEVEL SANITIZATION
Use method-level sanitization when an object is constructed from trusted sources but later receives untrusted input through method calls — query builders, log appenders, file writers that accept paths, HTTP request builders that accept headers.

See Q1246 for sanitized constructor parameters. See Q1237 for sanitized function parameters. See Q272 for defense in depth.

defines module qa.security.querybuildermethod

  defines class

    QueryBuilder
      accepted as Integer: 0
      rejected as Integer: 0

      default QueryBuilder()

      addClause()
        -> fragment as sanitized String
        <- result as String: "unset"

        if fragment?
          accepted: accepted + 1
          result: "accepted"
        else
          rejected: rejected + 1
          result: "rejected"

      getStats()
        <- rtn as String: `accepted=${accepted} rejected=${rejected}`

      default operator ?

  defines program

    SanitizedQueryBuilderDemo()
      stdout <- Stdout()
      qb <- QueryBuilder()

      stdout.println("=== QueryBuilder Sanitization Demo ===")

      cleanEquality <- "name = 'alice'"
      stdout.println("Equality: " + qb.addClause(cleanEquality))

      urlInjection <- "redirect=http://evil/?p=1"
      stdout.println("URL: " + qb.addClause(urlInjection))

      ldapInjection <- "(|(uid=*)(uid=*))"
      stdout.println("LDAP: " + qb.addClause(ldapInjection))

      nosqlInjection <- "$where: function() { return true }"
      stdout.println("NoSQL: " + qb.addClause(nosqlInjection))

      cleanRange <- "age >= 18"
      stdout.println("Range: " + qb.addClause(cleanRange))

      stdout.println(qb.getStats())
      stdout.println("=== Complete ===")

Java: validation inside the method body, or runtime libraries like Apache Validator. Python: input validation with re.match before use. Rust: validate by attempting to construct a newtype from the input. C#: data annotations validators on parameters. EK9: 'sanitized' modifier on the parameter — input is unset on rejection, no exception, no manual validation code.

LDAP injection
NoSQL injection
QueryBuilder
URL injection
method
method validation
object protection
sanitized

sanitized method parameter
query builder validation
diverse injection categories

Explanation: EK9 method parameters use the '->' arrow syntax on a separate indented line, not parenthesised parameters. See ek9 -h E50001 for details.


Question: Implement me a chain-of-responsibility pattern using abstract function handlers, where each handler tries to process a String and either returns a result or returns unset to pass on to the next handler.

Create a chain of handlers that each get a chance to process a value.

Show me chain-of-responsibility in EK9 using abstract functions.

Build a series of handlers that try in order until one succeeds.

Write a pattern where multiple processors are tried until one returns a value.

Answer:
Chain of responsibility in EK9 uses an abstract function as the handler interface and a list of dynamic functions as the chain. A driver function walks the chain, returning the first non-unset result. Pure and concise — no class hierarchy needed.

HANDLER INTERFACE
  Handler() as pure abstract
    -> request as String
    <- rtn as String?

The handler returns String? — set when handled, unset when passing on.

DRIVER FUNCTION
The driver walks the chain and stops at the first set result, using guarded assignment so the first match wins:
  processChain() as pure
    ->
      request as String
      handlers as List of Handler
    <- rtn as String: String()
    for handler in handlers
      attempt <- handler(request)
      if attempt?
        rtn :=? attempt

The :=? operator only assigns if rtn is currently unset, so subsequent handler hits are ignored.

DYNAMIC FUNCTION HANDLERS
Each handler is a dynamic function with the abstract signature:
  emailHandler <- () is Handler as pure function
    rtn: request contains "@" <- "email: " + request : String()

  urlHandler <- () is Handler as pure function
    rtn: request contains "http" <- "url: " + request : String()

  fallbackHandler <- () is Handler as pure function
    rtn: "text: " + request

The ternary 'condition <- ifTrue : ifFalse' returns one or the other inline.

USAGE
  chain <- [emailHandler, urlHandler, fallbackHandler]
  stdout.println(processChain("alice@example.com", chain))
  stdout.println(processChain("http://ek9.io", chain))
  stdout.println(processChain("hello world", chain))

KEY ADVANTAGE
No class hierarchy, no setNext() boilerplate, no null checks. The handlers are values that compose into a List, and the driver is one short pure function. Adding a new handler is one line — instantiate a dynamic function and put it in the list.

See Q1238 for strategy pattern (single handler). See Q214 for strategy pattern basics. See Q284 for find-first via guarded assignment.

defines module qa.patterns.chainresponsibility

  defines function

    Handler() as pure abstract
      -> request as String
      <- rtn as String?

    processChain() as pure
      ->
        request as String
        handlers as List of Handler
      <- rtn as String: String()
      for handler in handlers
        attempt <- handler(request)
        if attempt?
          rtn :=? attempt

  defines program

    ChainOfResponsibilityDemo()
      stdout <- Stdout()

      emailHandler <- () is Handler as pure function
        if request contains "@"
          rtn: "email: " + request
        else
          rtn: String()

      urlHandler <- () is Handler as pure function
        if request contains "http"
          rtn: "url: " + request
        else
          rtn: String()

      fallbackHandler <- () is Handler as pure function
        rtn: "text: " + request

      chain <- [emailHandler, urlHandler, fallbackHandler]

      stdout.println(processChain("alice@example.com", chain))
      stdout.println(processChain("http://ek9.io", chain))
      stdout.println(processChain("hello world", chain))

Java: abstract Handler class with setNext(), recursive handle() — verbose. Python: list of callables iterated until one returns non-None. JavaScript: array of functions reduced over input. Kotlin: list of functions called sequentially. EK9: List of abstract function instances + a small pure driver, with :=? to capture the first hit.

abstract function
chain of responsibility
delegation
dynamic function
first match
handler
pattern
pipeline

chain of responsibility
handler list
first-match driver

Explanation: An abstract function with no body must be declared 'as abstract' (or 'as pure abstract'). 'as pure' alone is for functions WITH a body. See ek9 -h E07110 for details.


Question: Write me a pure function classifyTemperature that takes a Float celsius and returns a String label, declaring the return variable WITHOUT a default and proving the compiler that every branch of the if/else chain initialises it.

Show me how to satisfy data flow analysis when there is no default value on the return variable.

Create a function whose every branch must explicitly assign the return variable.

Implement a temperature classifier that uses an explicit final else to satisfy initialisation analysis.

Write a function with no default return where every branch covers initialisation.

Answer:
EK9 has TWO valid patterns for satisfying the compiler's data flow analysis on a return variable: declare with a default value (Pattern 1), or declare without a default and assign in EVERY branch including a final else (Pattern 2). This file demonstrates Pattern 2.

PATTERN 2: NO DEFAULT, MANDATORY ELSE
  classifyTemperature() as pure
    -> celsius as Float
    <- label as String?
    if celsius >= HOT_THRESHOLD
      label: "hot"
    else if celsius >= WARM_THRESHOLD
      label: "warm"
    else if celsius >= COOL_THRESHOLD
      label: "cool"
    else
      label: "cold"

The declaration 'label as String?' creates an UNSET return variable with no initial value. The compiler then requires that every reachable code path assigns 'label' before the function returns. Without the final 'else', a celsius below COOL_THRESHOLD would leave label uninitialised — and the compiler raises E08020.

WHY USE PATTERN 2 INSTEAD OF PATTERN 1
Pattern 2 is preferable when:
1. There is NO sensible default value for the return type.
2. You want the compiler to FORCE you to think about every case.
3. Adding a new branch should be a deliberate decision (no silent fallthrough).

Pattern 1 (default value, see Q1240) is preferable when:
1. There IS a sensible default that should apply to any unhandled case.
2. New branches add refinement to a baseline behaviour.

USAGE
  stdout.println(`30C: ${classifyTemperature(30.0)}`)   // hot
  stdout.println(`22C: ${classifyTemperature(22.0)}`)   // warm
  stdout.println(`15C: ${classifyTemperature(15.0)}`)   // cool
  stdout.println(`-5C: ${classifyTemperature(-5.0)}`)   // cold

KEY POINT
Magic literals (HOT_THRESHOLD, WARM_THRESHOLD, COOL_THRESHOLD) are extracted into a 'defines constant' block to satisfy E11064. Comparing against bare literals is rejected by EK9's quality analysis.

See Q1240 for Pattern 1 (default value). See Q633 for branch initialisation rules. See Q632 for definition order.

defines module qa.dataflow.temperatureclassify

  defines constant

    HOT_THRESHOLD <- 25.0

    WARM_THRESHOLD <- 18.0

    COOL_THRESHOLD <- 10.0

  defines function

    classifyTemperature() as pure
      -> celsius as Float
      <- label as String?
      if celsius >= HOT_THRESHOLD
        label: "hot"
      else if celsius >= WARM_THRESHOLD
        label: "warm"
      else if celsius >= COOL_THRESHOLD
        label: "cool"
      else
        label: "cold"

  defines program

    TemperatureClassifyDemo()
      stdout <- Stdout()

      stdout.println(`30C: ${classifyTemperature(30.0)}`)
      stdout.println(`22C: ${classifyTemperature(22.0)}`)
      stdout.println(`15C: ${classifyTemperature(15.0)}`)
      stdout.println(`-5C: ${classifyTemperature(-5.0)}`)

Java: definite assignment requires every branch to assign — but allows null. Rust: every match arm must assign or use Option. Go: zero-value initialisation always works (but may hide bugs). Python: NameError at runtime. EK9: compile-time enforcement — either Pattern 1 (default) or Pattern 2 (else mandatory), no third option.

E08020
branch initialisation
data flow
exhaustive
explicit else
if else chain
no default
return variable

explicit branch coverage
no-default return variable
mandatory else

Explanation: Without a final else and without a default value at declaration, label is uninitialised when celsius is below WARM_THRESHOLD. Either provide a default at declaration (Pattern 1) or add the final else (Pattern 2). See ek9 -h E08020 for details.


Question: Show me how to make a Repository class extensible so that InMemoryRepository can inherit from it without triggering E05030.

How do I mark a Repository class as extensible in EK9?

Create a Repository parent class that an InMemoryRepository can extend.

I got E05030 on Repository — how do I allow InMemoryRepository to extend it?

Make a Repository class open so other repositories can inherit from it.

Answer:
EK9 classes are CLOSED by default. To allow another class to extend them, add 'as open' to the parent class declaration. Without 'as open' the compiler raises E05030 'not open to be extended'.

PARENT CLASS AS OPEN
  Repository as open
    name as String: "<unnamed>"
    default Repository()

CHILD CLASS EXTENDS
  InMemoryRepository extends Repository
    default InMemoryRepository()

The 'extends' keyword only compiles if the parent has 'as open' (or is abstract — see Q1251). Any other class declaration is closed and produces E05030 when something tries to inherit from it.

WHEN TO USE 'AS OPEN'
Use 'as open' when you have a CONCRETE base class with a working default behaviour, and you WANT subclasses to provide alternative behaviour while still being usable on their own.

See Q1043 for diagnosing E05030. See Q784 for closed-by-default rationale. See Q1251 for abstract classes as an alternative.

defines module qa.classesandoop.openrepository

  defines class

    Repository as open
      name as String: "<unnamed>"

      default Repository()

      describe() as pure
        <- rtn as String: "Repository: " + name

      default operator ?

    InMemoryRepository extends Repository
      itemCount as Integer: 0

      default InMemoryRepository()

      override describe() as pure
        <- rtn as String: `InMemoryRepository with ${itemCount} items`

      default operator ?

  defines program

    OpenRepositoryDemo()
      stdout <- Stdout()

      repo <- InMemoryRepository()
      stdout.println(repo.describe())

Java: classes open by default, use 'final' to close. Kotlin: closed by default, use 'open' keyword (same as EK9). Scala: classes open by default, use 'final' to close. C#: classes open by default, use 'sealed' to close. Swift: classes open by default, use 'final' to close. EK9: closed by default like Kotlin, 'as open' to allow extension.

E05030
Repository
as open
closed
extends
extensible
inheritance

as open on class
making class extensible
E05030 fix

Explanation: Without 'as open', Repository is closed and InMemoryRepository cannot extend it. Add 'as open' to the parent declaration. See ek9 -h E05030 for details.

Oracle can generate an open base class and extending subclass with correct 'as open' modifier.


Question: Create me a Database abstract class that PostgresDatabase can extend, showing that 'as abstract' makes the class implicitly open to extension.

How does 'as abstract' relate to E05030 and the open/closed rule?

Show me that an abstract class does not need 'as open' to be extensible.

Write a Database abstract parent and a PostgresDatabase concrete child.

Make a class extensible by marking it as abstract instead of as open.

Answer:
An ABSTRACT class in EK9 is IMPLICITLY open. You do NOT need 'as open' on an abstract class — the 'as abstract' modifier already allows extension because an abstract class is meaningless without a concrete subclass.

ABSTRACT PARENT
  Database as abstract
    connectionString as String: String()

    default Database()

    connect() as abstract
      <- rtn as Boolean?

    protocol() as pure
      <- rtn as String: "generic"

An abstract class can have BOTH abstract methods (no body, subclass MUST implement) AND concrete methods (default behaviour subclass can override).

CONCRETE CHILD
  PostgresDatabase extends Database
    default PostgresDatabase()

    override connect() as pure
      <- rtn as Boolean: true

    override protocol() as pure
      <- rtn as String: "postgres"

The child must override every abstract method. Concrete methods are optional to override.

TWO WAYS TO MAKE A CLASS EXTENSIBLE
- 'as open' — concrete class, has a working default, subclasses are optional
- 'as abstract' — incomplete class, MUST be subclassed, implicitly open

You CANNOT write 'as abstract as open' or 'as open as abstract' — these are mutually exclusive syntactically.

See Q1250 for 'as open' on concrete classes. See Q1043 for diagnosing E05030. See Q784 for closed-by-default rationale.

defines module qa.classesandoop.abstractdatabase

  defines class

    Database as abstract
      connectionString as String: String()

      default Database()

      connect() as pure abstract
        <- rtn as Boolean?

      protocol() as pure
        <- rtn as String: "generic"

      default operator ?

    PostgresDatabase extends Database
      default PostgresDatabase()

      override connect() as pure
        <- rtn as Boolean: true

      override protocol() as pure
        <- rtn as String: "postgres"

      default operator ?

  defines program

    AbstractDatabaseDemo()
      stdout <- Stdout()

      db <- PostgresDatabase()
      stdout.println(`Protocol: ${db.protocol()}`)
      stdout.println(`Connected: ${db.connect()}`)

Java: abstract classes can be extended regardless of 'final'. Kotlin: abstract implies open. Scala: abstract implies extensible. C#: abstract classes are implicitly inheritable. Python: ABC classes work similarly. EK9: 'as abstract' makes the class implicitly open — no need for both modifiers.

Database
abstract class
as abstract
extends
implicitly open
inheritance

as abstract implies open
abstract class extension
abstract vs open

Explanation: You cannot combine 'as abstract' and 'as open'. An abstract class is implicitly open — 'as abstract' is sufficient on its own. See ek9 -h E07110 for details.

Oracle can generate an abstract class that is implicitly open for extension.


Question: Write me an open logWriter function and a dynamic function instance that overrides its behaviour, demonstrating that 'as open' on a function allows dynamic instances to extend it.

How do I make a function extensible in EK9 with 'as open'?

Show me a concrete function with 'as open' that a dynamic function can extend.

Create a logWriter function that allows a dynamic instance to override its body.

I got E05030 on my function — how do I allow a dynamic instance to extend it?

Answer:
FUNCTIONS in EK9 follow the same closed-by-default rule as classes. To allow a dynamic function instance to extend a concrete function, mark the function 'as open'. Without 'as open' the compiler raises E05030 when something tries to extend it.

OPEN CONCRETE FUNCTION
  logWriter() as open
    -> message as String
    <- formatted as String: "[default] " + message

The function has a body (default behaviour) AND is marked 'as open' so dynamic instances can override it.

DYNAMIC INSTANCE EXTENDING IT
  consoleVariant <- () is logWriter as function
    formatted: "[console] " + message

The '() is logWriter as function' creates a dynamic instance whose body replaces the default. The 'message' parameter and 'formatted' return variable come from the parent function's signature — they are visible inside the dynamic body.

USAGE
  defaultLine <- logWriter("hello")
  consoleLine <- consoleVariant("hello")
  stdout.println(defaultLine)
  stdout.println(consoleLine)
  // Output:
  // [default] hello
  // [console] hello

WHEN TO USE 'AS OPEN' ON A FUNCTION
Use it when you have a default implementation that works fine for most callers, but you want callers to be able to provide a specialised dynamic instance for specific needs. If you want to FORCE a custom implementation (no default), use 'as abstract' instead (see Q1253).

See Q1250 for 'as open' on classes. See Q1253 for 'as abstract' functions. See Q1043 for diagnosing E05030.

defines module qa.functionsandmethods.openlogwriter

  defines function

    logWriter() as open
      -> message as String
      <- formatted as String: "[default] " + message

  defines program

    OpenLogWriterDemo()
      stdout <- Stdout()

      consoleVariant <- () is logWriter as function
        formatted: "[console] " + message

      defaultLine <- logWriter("hello")
      consoleLine <- consoleVariant("hello")
      stdout.println(defaultLine)
      stdout.println(consoleLine)

Java: no standalone open functions, methods are open by default. Kotlin: top-level functions are not extensible. Python: functions can be reassigned but not 'extended'. EK9 is unusual in allowing function extension via 'as open' plus a dynamic instance — the parent function provides the default body, the dynamic instance overrides it.

E05030
as open
dynamic instance
extend function
function
logWriter

open function
dynamic function extension
as open on function

logWriter()
      -> message as String
      <- formatted as String: "[default] " + message

Explanation: Without 'as open', the function is closed and dynamic instances cannot extend it. Add 'as open' to the parent function. See ek9 -h E05030 for details.

Oracle can generate an open function that dynamic function instances can extend.


Question: Create me an abstract transformer function that a dynamic function instance can extend — showing that 'as abstract' on a function makes it implicitly open.

Does an abstract function need 'as open' in EK9?

Show me that an abstract function is implicitly extensible by dynamic instances.

Write an abstract transformer and a dynamic function that implements it.

How does 'as abstract' remove the need for 'as open' on a function?

Answer:
An ABSTRACT function in EK9 is IMPLICITLY open — just like an abstract class. You do NOT need to write 'as open' alongside 'as abstract' because an abstract function has no body and is useless without a concrete dynamic instance.

ABSTRACT FUNCTION
  transformer() as pure abstract
    -> input as String
    <- rtn as String?

An abstract function has a signature (parameters and return type) but NO body. It cannot be called directly — only through a dynamic instance that provides the body.

DYNAMIC INSTANCE PROVIDING THE BODY
  trimAndUppercase <- () is transformer as pure function
    rtn: input.trim().upperCase()

The '() is transformer as pure function' creates a dynamic instance that supplies the missing body. The 'input' parameter and 'rtn' return variable come from the abstract function's signature.

TWO WAYS TO MAKE A FUNCTION EXTENSIBLE
- 'as open' — concrete function with default body, dynamic instances are optional (Q1252)
- 'as abstract' — signature only, dynamic instances are REQUIRED

Abstract functions are typically used as STRATEGY interfaces — different dynamic instances are chosen at runtime (see Q1238 for the strategy pattern).

USAGE
  result <- trimAndUppercase("  hello ek9  ")
  stdout.println(result)
  // Output: HELLO EK9

See Q1252 for 'as open' functions. See Q1238 for strategy pattern using abstract functions. See Q1043 for E05030 diagnosis.

defines module qa.functionsandmethods.abstracttransformer

  defines function

    transformer() as pure abstract
      -> input as String
      <- rtn as String?

  defines program

    AbstractTransformerDemo()
      stdout <- Stdout()

      trimAndUppercase <- () is transformer as pure function
        rtn: input.trim().upperCase()

      result <- trimAndUppercase("  hello ek9  ")
      stdout.println(result)

Java: abstract methods on interfaces, lambdas implement the interface. Kotlin: abstract fun + lambda. Scala: abstract def + anonymous function. Rust: trait method without default. Python: abstractmethod decorator. EK9: 'as abstract' on a standalone function, dynamic instances supply the body.

abstract function
as abstract
dynamic instance
function
implicitly open
transformer

abstract function
as abstract implies open
dynamic instance body

transformer() as pure open
      -> input as String
      <- rtn as String?

Explanation: A function with no body must be 'as abstract'. 'as open' is for functions WITH a default body that can be overridden. See ek9 -h E07110 for details.

Oracle can generate an abstract function that dynamic functions can implement.


Question: Walk me through fixing error E05030 'not open to be extended' for three different cases: extending a closed class, extending a closed function, and extending a built-in type.

Give me three worked examples of fixing E05030.

The compiler says 'not open to be extended' — show me the fixes.

Walk me through the three common causes of E05030 and how to resolve each.

Answer:
E05030 'not open to be extended' is raised whenever you try to extend a type that is closed. There are THREE distinct scenarios and each has a different fix.

CASE 1: EXTENDING A CLOSED USER CLASS
Problem: You own the parent class but forgot 'as open':
  AuditLogger                     // CLOSED by default
    default AuditLogger()

  FileAuditLogger extends AuditLogger   // E05030
    default FileAuditLogger()

Fix: add 'as open' to the parent:
  AuditLogger as open               // Now extensible
    default AuditLogger()

CASE 2: EXTENDING A CLOSED USER FUNCTION
Problem: You try to extend a concrete function that is closed:
  formatter()                     // CLOSED by default
    -> input as String
    <- rtn as String: input.trim()

  upper <- () is formatter as function   // E05030 — formatter is closed
    rtn: input.trim().upperCase()

Fix: add 'as open' to the parent function:
  formatter() as open               // Now extensible
    -> input as String
    <- rtn as String: input.trim()

CASE 3: EXTENDING A BUILT-IN CLOSED TYPE
Problem: Built-in types (List, Dict, Optional, Result, String, Integer) are ALWAYS closed and can never be extended:
  StringList extends List of String   // E05030 — List is closed

Fix: use COMPOSITION instead — hold the built-in as a field:
  StringCollection
    items as List of String: List()

    add()
      -> item as String
      items += item

    size() as pure
      <- rtn as Integer: length items

CHOOSING THE RIGHT FIX
- Case 1 and Case 2 are fine to fix with 'as open' IF you own the parent AND inheritance is the right design.
- For Case 3 (built-in types) you MUST use composition. Built-ins are deliberately closed to prevent subclasses from breaking invariants.
- Whenever possible, prefer composition over inheritance even for your own classes.

See Q1043 for the E05030 diagnosis flow. See Q1250 for 'as open' on classes. See Q1252 for 'as open' on functions.

defines module qa.debugging.fixe05030cases

  defines class

    AuditLogger as open
      default AuditLogger()

      log()
        -> message as String
        stdout <- Stdout()
        stdout.println("[AUDIT] " + message)

      default operator ?

    FileAuditLogger extends AuditLogger
      default FileAuditLogger()

      override log()
        -> message as String
        stdout <- Stdout()
        stdout.println("[FILE-AUDIT] " + message)

      default operator ?

    StringCollection
      items as List of String: List() of String

      default StringCollection()

      add()
        -> item as String
        items += item

      size() as pure
        <- rtn as Integer: length items

      default operator ?

  defines function

    formatter() as open
      -> input as String
      <- rtn as String: input.trim()

  defines program

    FixE05030Demo()
      stdout <- Stdout()

      logger <- FileAuditLogger()
      logger.log("case 1: class extension via 'as open'")

      upper <- () is formatter as function
        rtn: input.trim().upperCase()
      stdout.println(`case 2: open function extended dynamically -> ${upper("  ek9  ")}`)

      collection <- StringCollection()
      collection.add("case 3: composition")
      collection.add("holds a List")
      stdout.println(`case 3: collection size -> ${collection.size()}`)

Java: final keyword closes classes, subclasses can bypass accidentally. Kotlin: closed by default like EK9, use 'open' keyword. Scala: final keyword closes. C#: sealed keyword closes. EK9: E05030 catches all three cases at compile time — no runtime surprise.

E05030
built-in
closed
composition
extends
fix
open
three cases

E05030 fix scenarios
open vs composition
built-in closed types

Explanation: Built-in types like List are always closed. Attempting to extend them causes a type resolution error because the compiler cannot resolve the extension. Use composition instead. See ek9 -h E07090 for details.


Question: Create me a Product record and use the SINGLE-dollar $ operator to convert it to a plain-text human-readable String.

Show me how to use the $ operator on a custom record for human-readable output.

Write a Product record with a $ operator that returns a plain String.

Give me a record example that uses just the $ operator, not $$.

How do I override operator $ to produce a String representation of a record?

Answer:
The SINGLE-dollar operator $ produces a PLAIN-TEXT String representation of a value. It is the EK9 equivalent of Java's toString() or Python's __str__. Override 'operator $' on a user-defined record to control what $ returns.

PRODUCT RECORD WITH $ OVERRIDE
  Product
    sku as String: String()
    name as String: String()
    price as Float: 0.0

    Product()
      ->
        sku as String
        name as String
        price as Float
      this.sku :=: sku
      this.name :=: name
      this.price :=: price

    override operator $ as pure
      <- rtn as String: `${sku} ${name} £${price}`

    default operator ?

USAGE
  product <- Product("SKU-42", "Widget", 9.99)
  plainText <- $product
  stdout.println(plainText)
  // Output: SKU-42 Widget £9.99

WHAT $ DOES
- Returns a String that is human-readable
- Uses backtick string interpolation for flexible formatting
- Is the OPPOSITE of $$ — $ is plain text, $$ is JSON (see Q1256)

This file uses ONLY the $ operator so the distinction from $$ is absolutely clear. Every use of the dollar sign here is single-dollar.

See Q1256 for the $$ JSON operator contrast. See Q1094 for $ and $$ in context. See Q887 for the JSON operator return type.

defines module qa.operators.dollarstringproduct

  defines record

    Product
      sku as String: String()
      name as String: String()
      price as Float: 0.0

      Product()
        ->
          sku as String
          name as String
          price as Float
        this.sku :=: sku
        this.name :=: name
        this.price :=: price

      operator $ as pure
        <- rtn as String: `${sku} ${name} £${price}`

      default operator ?

  defines program

    DollarStringProductDemo()
      stdout <- Stdout()

      product <- Product("SKU-42", "Widget", 9.99)
      plainText <- $product
      stdout.println(plainText)

Java: toString() override. Kotlin: override fun toString(). Python: __str__. Rust: impl Display. Scala: override def toString. C#: override ToString(). EK9: 'operator $' produces a plain-text String. 'operator $$' produces JSON — the two are DIFFERENT.

$
Product
dollar
human readable
plain text
string operator
toString

operator $
string conversion
plain text output

Explanation: '$' and '$$' are different operators: '$$' is the JSON operator and must return a JSON, while '$' returns plain text. See ek9 -h E07580 for details.


Question: Create me a Customer record and use the DOUBLE-dollar $$ operator to convert it to a JSON representation — not plain text.

Show me how to use the $$ operator on a custom record for JSON output.

Write a Customer record with a $$ operator that returns a JSON value.

Give me a record example that uses just the $$ operator, not $.

How do I override operator $$ to produce a JSON representation of a record?

Answer:
The DOUBLE-dollar operator $$ produces a JSON representation of a value. It is distinct from the single-dollar $ operator (which produces plain text). Override 'operator $$' on a user-defined record to control what $$ returns — typically constructing a JSON object from the record's fields.

CUSTOMER RECORD WITH $$ OVERRIDE
  Customer
    id as Integer: 0
    firstName as String: String()
    lastName as String: String()

    Customer()
      ->
        id as Integer
        firstName as String
        lastName as String
      this.id :=: id
      this.firstName :=: firstName
      this.lastName :=: lastName

    override operator $$ as pure
      <- rtn as JSON: JSON()
        rtn.object()
        rtn += JSON("id", $id)
        rtn += JSON("firstName", firstName)
        rtn += JSON("lastName", lastName)

    default operator ?

USAGE
  customer <- Customer(1001, "Ada", "Lovelace")
  jsonValue <- $$customer
  stdout.println($jsonValue)
  // Output: {"id":1001,"firstName":"Ada","lastName":"Lovelace"}

WHAT $$ DOES
- Returns a JSON value (type JSON, not String)
- Used to serialise records to wire format
- Is the OPPOSITE of $ — $$ is JSON, $ is plain text (see Q1255)

This file uses ONLY the $$ operator so the distinction from $ is absolutely clear. Every use of a dollar sign operator here is double-dollar.

NOTE ON THE DIFFERENCE
- $ returns String (plain text, for humans)
- $$ returns JSON (structured, for machines)
The two are DIFFERENT operators with DIFFERENT return types. Do not confuse them.

See Q1255 for the $ string operator contrast. See Q887 for the JSON operator return type. See Q1094 for $ and $$ in context.

defines module qa.operators.dollardollarjsoncustomer

  defines class

    Customer
      id as Integer: 0
      firstName as String: String()
      lastName as String: String()

      Customer()
        ->
          id as Integer
          firstName as String
          lastName as String
        this.id :=: id
        this.firstName :=: firstName
        this.lastName :=: lastName

      operator $$ as pure
        <- rtn as JSON: JSON()

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

  defines program

    DollarDollarJsonCustomerDemo()
      stdout <- Stdout()

      customer <- Customer(1001, "Ada", "Lovelace")
      jsonValue <- $$customer
      stdout.println(`JSON: ${jsonValue}`)

Java: custom toJson() methods or Jackson/Gson annotations. Kotlin: kotlinx.serialization @Serializable. Python: __json__ convention or dataclass asdict. Rust: serde Serialize. EK9: 'operator $$' is built-in syntax for JSON conversion, returning a JSON value directly.

$$
Customer
JSON conversion
JSON operator
double dollar
serialization

Explanation: Operator $ must return a String; to return JSON use the double-dollar $$ operator instead of $. See ek9 -h E07570 for details.


Question: Write me a Booking class with a SINGLE-dollar $ operator that returns a human-readable reservation line — no JSON, just plain text.

Show me another $ operator example on a custom class, not a record.

Give me a $-only Booking example — no $$ involved.

Override operator $ on a Booking class to produce a readable String line.

Create a worked example of $ (plain text only) on a reservation class.

Answer:
The SINGLE-dollar $ operator produces plain text. This file shows a Booking class where $ returns a formatted reservation line. NO $$ is used anywhere — this is a pure single-dollar example.

BOOKING CLASS WITH $ OVERRIDE
  Booking
    reference as String: String()
    guestName as String: String()
    nights as Integer: 0

    Booking()
      ->
        reference as String
        guestName as String
        nights as Integer
      this.reference :=: reference
      this.guestName :=: guestName
      this.nights :=: nights

    operator $ as pure
      <- rtn as String: `Booking ${reference} for ${guestName} — ${nights} nights`

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

USAGE
  booking <- Booking("BK-2024-0817", "Alice", 3)
  line <- $booking
  stdout.println(line)
  // Output: Booking BK-2024-0817 for Alice — 3 nights

KEY POINT
The return type of $ is always String. The return type of $$ is JSON. They are DIFFERENT operators. This example uses ONLY $ — there is no $$ anywhere in the code.

See Q1255 for another $-only example (Product record). See Q1256 for a $$-only example (Customer). See Q887 for the operator contracts.

defines module qa.operators.dollarstringbooking

  defines class

    Booking
      reference as String: String()
      guestName as String: String()
      nights as Integer: 0

      Booking()
        ->
          reference as String
          guestName as String
          nights as Integer
        this.reference :=: reference
        this.guestName :=: guestName
        this.nights :=: nights

      operator $ as pure
        <- rtn as String: `Booking ${reference} for ${guestName} — ${nights} nights`

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

  defines program

    DollarStringBookingDemo()
      stdout <- Stdout()

      booking <- Booking("BK-2024-0817", "Alice", 3)
      line <- $booking
      stdout.println(line)

Java: toString() override on the class. Kotlin: override fun toString(). Python: __str__. Rust: impl Display for Booking. C#: override ToString(). EK9: 'operator $ as pure' returning String — plain text, never JSON.

$
Booking
class
dollar
plain text
reservation
string operator

operator $ on class
plain text conversion
toString equivalent

operator $ as pure
        <- rtn as JSON: JSON()

Explanation: $ must return String, not JSON. JSON is the return type for $$. See ek9 -h E07580 for details.


Question: Create me an Invoice class with a DOUBLE-dollar $$ operator that returns a JSON value — no plain-text formatting, only JSON.

Show me another $$ operator example on a custom class.

Give me a $$-only Invoice example — no single $.

Override operator $$ on an Invoice class to return JSON.

Create a worked example of $$ (JSON only) on a billing class.

Answer:
The DOUBLE-dollar $$ operator produces JSON. This file shows an Invoice class where $$ returns a JSON value. NO $ is used anywhere — this is a pure double-dollar example.

INVOICE CLASS WITH $$ OVERRIDE
  Invoice
    invoiceNumber as String: String()
    amount as Float: 0.0
    paid as Boolean: false

    Invoice()
      ->
        invoiceNumber as String
        amount as Float
        paid as Boolean
      this.invoiceNumber :=: invoiceNumber
      this.amount :=: amount
      this.paid :=: paid

    operator $$ as pure
      <- rtn as JSON: JSON()

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

USAGE
  invoice <- Invoice("INV-2024-0042", 125.50, false)
  jsonValue <- $$invoice
  stdout.println(`Invoice as JSON: ${jsonValue}`)

KEY POINT
The return type of $$ is always JSON, never String. The return type of $ is always String, never JSON. They are DIFFERENT operators. This example uses ONLY $$ — there is no single $ operator here.

COMMON MISTAKE
It is tempting to think $$ is 'a stronger $' or 'escaped $'. It is NOT. It is a completely separate operator with a different purpose and a different return type.

See Q1256 for another $$-only example (Customer). See Q1255 for a $-only example (Product). See Q887 for E07580 on wrong JSON return type.

defines module qa.operators.dollardollarjsoninvoice

  defines class

    Invoice
      invoiceNumber as String: String()
      amount as Float: 0.0
      paid as Boolean: false

      Invoice()
        ->
          invoiceNumber as String
          amount as Float
          paid as Boolean
        this.invoiceNumber :=: invoiceNumber
        this.amount :=: amount
        this.paid :=: paid

      operator $$ as pure
        <- rtn as JSON: JSON()

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

  defines program

    DollarDollarJsonInvoiceDemo()
      stdout <- Stdout()

      invoice <- Invoice("INV-2024-0042", 125.50, false)
      jsonValue <- $$invoice
      stdout.println(`Invoice as JSON: ${jsonValue}`)

Java: custom toJson() or Jackson ObjectMapper. Kotlin: kotlinx.serialization. Scala: circe or play-json. Rust: serde Serialize trait. Python: json.dumps or __json__. Go: json.Marshaler interface. EK9: built-in 'operator $$' with enforced JSON return type.

$$
Invoice
JSON operator
billing
double dollar
serialization

operator $$ on class
JSON serialisation
billing example

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

Explanation: $$ must return JSON, not String. If you want a plain-text representation, use $ instead. See ek9 -h E07580 for details.


Question: Show me how to use the ++ and -- operators on an Integer counter, demonstrating that they are statement-only and modify the variable in place.

Increment and decrement an Integer using ++ and --.

Write a small program that uses ++ to count up and -- to count down.

Show me how Integer ++ works in EK9 — is it the same as Java?

Demonstrate that ++ on an Integer is statement-only and not an expression.

Answer:
EK9 supports ++ and -- on Integer (and many other types). They are STATEMENT-ONLY operators — they modify the variable in place and produce NO value. You cannot use them inside expressions.

BASIC USAGE
  visitorCount <- 0
  visitorCount++       // OK: standalone statement
  visitorCount++       // OK: each call increments by 1
  visitorCount--       // OK: decrement by 1

WHAT YOU CANNOT DO
  copy <- visitorCount++   // ERROR: ++ produces no value to assign
  if visitorCount++        // ERROR: ++ has no Boolean result
  total <- a + b++         // ERROR: ++ cannot be inside an expression

WHY STATEMENT-ONLY
In C/Java/JavaScript, ++ returns the OLD value (postfix) or NEW value (prefix). This creates subtle bugs:
  x = counter++   // C: x gets old value
  x = ++counter   // C: x gets new value
EK9 eliminates this ambiguity by making ++ purely a side-effect statement. To get the value, use the variable separately AFTER the increment.

CORRECT PATTERN
  visitorCount <- 0
  visitorCount++
  current <- visitorCount       // Read the value separately
  stdout.println(`After ++: ${current}`)

USAGE IN A LOOP
  retries <- 0
  maxRetries <- 3
  while retries < maxRetries
    retries++
    stdout.println(`Attempt ${retries}`)

This QA uses Integer specifically. See Q1260 for Float ++/--, Q1261 for Date ++/--, and Q1262 for Enumeration ++/--. The same statement-only semantics apply to every type that supports ++ and --.

See Q241 for mutation operators overview. See Q238 for the complete operator set.

defines module qa.operators.incrementinteger

  defines program

    IncrementIntegerDemo()
      stdout <- Stdout()

      visitorCount <- 0
      visitorCount++
      visitorCount++
      visitorCount++
      stdout.println(`After three ++: ${visitorCount}`)

      visitorCount--
      stdout.println(`After one --: ${visitorCount}`)

      retries <- 0
      maxRetries <- 3
      while retries < maxRetries
        retries++
        stdout.println(`Attempt ${retries}`)

C/C++: ++ and -- are expressions returning a value. Java: same as C, with prefix/postfix distinction. JavaScript: same. Python: no ++ or --, use += 1. Go: ++ is statement-only (no value), like EK9. Kotlin: inc()/dec() functions or += 1. Rust: no ++, use += 1. EK9: matches Go's design — statement-only, no value, no prefix/postfix ambiguity.

++
Integer
counter
decrement
in place
increment
statement only

Integer ++ --
statement-only mutation
in-place increment

      visitorCount++
      visitorCount++

Explanation: ++ is statement-only. It does not return a value, so it cannot be used as the right-hand side of an assignment. Increment first, then read the variable. See ek9 -h E07510 for details.


Question: Show me how to use ++ and -- on a Float temperature reading, demonstrating that floating-point types also support in-place increment and decrement.

Does Float support the same ++ -- operators as Integer in EK9?

Answer:
Float supports the ++ and -- operators, just like Integer. Each ++ adds 1.0 to the value; each -- subtracts 1.0. They are STATEMENT-ONLY and modify the variable in place — exactly the same semantics as Integer ++/--.

BASIC USAGE ON FLOAT
  reading as Float: 23.5
  reading++              // Now 24.5
  reading++              // Now 25.5
  reading--              // Now 24.5

The value changes by exactly 1.0 each time. There is no rounding, no precision loss beyond normal Float arithmetic.

SAMPLING SCENARIO
A temperature monitor that adjusts a setpoint by one degree at a time:
  setpoint as Float: 20.0

  // Three small adjustments up
  setpoint++
  setpoint++
  setpoint++
  // setpoint is now 23.0

  // One adjustment down
  setpoint--
  // setpoint is now 22.0

  stdout.println(`Final setpoint: ${setpoint}`)

NO EXPRESSION USE
Like Integer ++, Float ++ is statement-only. You cannot write 'next <- reading++' — that is a compile error. Increment first, then read.

WHAT IF YOU WANT FRACTIONAL INCREMENT?
Use the compound assignment += instead:
  reading as Float: 23.5
  reading += 0.1   // Now 23.6
The ++ operator always increments by 1.0. For other step sizes, use +=.

See Q1259 for Integer ++/--. See Q1261 for Date ++/--. See Q1262 for Enumeration ++/--. See Q241 for mutation operators overview.

defines module qa.operators.incrementfloat

  defines program

    IncrementFloatDemo()
      stdout <- Stdout()

      setpoint as Float: 20.0
      stdout.println(`Initial setpoint: ${setpoint}`)

      setpoint++
      setpoint++
      setpoint++
      stdout.println(`After three ++: ${setpoint}`)

      setpoint--
      stdout.println(`After one --: ${setpoint}`)

      reading as Float: 23.5
      reading++
      stdout.println(`Reading after ++: ${reading}`)

Java: float ++ supported, same statement semantics. C/C++: float ++ supported, increments by 1.0. Python: no ++ for any type. Rust: no ++ for any type, use += 1.0. Go: float ++ is statement-only. JavaScript: float ++ supported. EK9: matches the C-family conventions for Float — increment by 1.0, statement-only.

++
Float
decrement
in place
increment
temperature

Float ++ --
floating-point increment
in-place mutation

Explanation: ++ is statement-only. It cannot appear inside an expression. The compiler detects the invalid expression usage. To add 2.0, use 'reading += 2.0'. See ek9 -h E07950 for details.


Question: Show me how to use ++ on a Date to advance one day at a time, iterating through a week starting from a known date.

Increment a Date by one day using ++ in EK9.

Walk forward through dates one day at a time without using a Duration.

Answer:
Date supports the ++ and -- operators. Each ++ advances the date by ONE DAY; each -- moves it back by ONE DAY. Like all ++/-- operators in EK9, these are STATEMENT-ONLY and modify the variable in place.

ADVANCING ONE DAY AT A TIME
  startOfWeek <- 2025-03-10
  current <- Date(startOfWeek)

  current++   // 2025-03-11
  current++   // 2025-03-12
  current++   // 2025-03-13

The Date arithmetic understands month boundaries — incrementing 2025-03-31 by ++ produces 2025-04-01, not '2025-03-32'.

ITERATING A WEEK
  startOfWeek <- 2025-03-10
  current <- Date(startOfWeek)
  daysToShow <- 7
  daysShown <- 0

  while daysShown < daysToShow
    stdout.println(`Day ${daysShown + 1}: ${current}`)
    current++
    daysShown++

This prints seven consecutive dates without ever touching a Duration. The ++ operator IS the 'add one day' API for Date.

GOING BACKWARDS
  todayLike <- 2025-03-15
  yesterday <- Date(todayLike)
  yesterday--   // 2025-03-14

LEAP YEAR AND MONTH BOUNDARIES
  monthEnd <- 2024-02-28
  next <- Date(monthEnd)
  next++   // 2024-02-29 (2024 is a leap year)
  next++   // 2024-03-01

The Date type handles leap years and month lengths automatically. You never need to compute the next valid date yourself.

See Q1259 for Integer ++/--. See Q1260 for Float ++/--. See Q1262 for Enumeration ++/--. See Q241 for mutation operators overview.

defines module qa.operators.incrementdate

  defines program

    IncrementDateDemo()
      stdout <- Stdout()

      startOfWeek <- 2025-03-10
      current <- Date(startOfWeek)
      daysToShow <- 7
      daysShown <- 0

      while daysShown < daysToShow
        stdout.println(`Day ${daysShown + 1}: ${current}`)
        current++
        daysShown++

      yesterday <- 2025-03-15
      previousDay <- Date(yesterday)
      previousDay--
      stdout.println(`Yesterday: ${previousDay}`)

Java: LocalDate.plusDays(1) — method call, no operator. Kotlin: same as Java. Python: date + timedelta(days=1) — uses arithmetic with a delta. Rust: NaiveDate::succ_opt() — method returning Option. C#: DateTime.AddDays(1) — method call. JavaScript: setDate(getDate() + 1) — manual arithmetic. EK9: Date supports ++ directly — most concise way to step through days.

++
Date
calendar
increment
iterate days
next day

Date ++ --
date arithmetic
advance by day

Explanation: EK9 Date does not have an addDays method. The compiler cannot resolve the method call. Use the ++ operator to advance by one day, or += Duration() for larger steps. See ek9 -h E50060 for details.


Question: Show me how to use ++ on an enumeration to walk through ProcessState values (Pending → Running → Complete), demonstrating that walking past the last value makes the variable unset.

Walk through enumeration values using ++ in EK9.

Show me what happens when I increment past the last enum value.

Use the ++ and -- operators on a custom enum like ProcessState.

Answer:
EK9 enumerations support ++ (next value) and -- (previous value). Each ++ advances to the next declared value; -- moves to the previous one. When the variable would go past the FIRST or LAST declared value, it becomes UNSET — no exception, no wrap-around.

DEFINE THE ENUMERATION
  ProcessState
    Pending
    Running
    Complete

Four declared values in order. Pending is first; Complete is last.

WALKING FORWARD
  state as ProcessState: ProcessState.Pending
  stdout.println(`start: ${state}`)

  state++   // Running
  stdout.println(`after ++ : ${state}`)

  state++   // Complete
  stdout.println(`after ++ : ${state}`)

  state++   // unset — past the last value
  stdout.println(`past last is set: ${state?}`)

The critical safety property: incrementing past the last value does NOT throw and does NOT wrap to the first. The variable becomes UNSET, which the ? operator detects.

WALKING BACKWARDS
  state2 as ProcessState: ProcessState.Complete
  state2--   // Running
  state2--   // Pending
  state2--   // unset — before the first value
  stdout.println(`before first is set: ${state2?}`)

SAFE CYCLING PATTERN
  current as ProcessState: ProcessState.Pending
  while current?
    stdout.println(`processing: ${current}`)
    current++

The loop terminates automatically when current becomes unset after the last value. No manual bounds tracking, no off-by-one errors.

WHY THIS MATTERS
Most languages either throw on out-of-bounds, wrap to the first value, or require manual bounds checking. EK9's unset-on-boundary semantics compose naturally with the ? operator and guard expressions.

See Q1259 for Integer ++/--. See Q1260 for Float ++/--. See Q1261 for Date ++/--. See Q220 for full enumeration navigation.

defines module qa.operators.incrementenum

  defines type

    ProcessState
      Pending
      Running
      Complete

  defines program

    IncrementEnumDemo()
      stdout <- Stdout()

      state as ProcessState: ProcessState.Pending
      stdout.println(`start: ${state}`)

      state++
      stdout.println(`after ++ : ${state}`)

      state++
      stdout.println(`after ++ : ${state}`)

      state++
      stdout.println(`past last is set: ${state?}`)

      state2 as ProcessState: ProcessState.Complete
      state2--
      state2--
      state2--
      stdout.println(`before first is set: ${state2?}`)

Java: enum.ordinal() + 1 with manual bounds and ArrayIndexOutOfBoundsException risk. Kotlin: similar to Java. Rust: requires implementing Add or using strum crate. Python: no built-in next/previous on Enum. Go: iota integers with silent wrap-around. C#: cast to int and increment, no bounds checking. EK9: ++/-- with automatic boundary-to-unset semantics — the safest design in this list.

++
ProcessState
enum
enumeration
next value
previous value
unset boundary

enum ++ --
boundary-to-unset
safe enum walking

Explanation: EK9 enumerations do not have a next() method. The compiler cannot resolve the method call. Use the ++ operator to advance to the next declared value. See Q220 for the full enumeration navigation API. See ek9 -h E50060 for details.


Question: If I have a String variable that might be unset, how can I conditionally assign a default value to it only when it has not been set yet?

How do I assign a default to a String only when it is unset?

Show me the EK9 way to provide a fallback value for an unset String.

What is the operator that assigns only when the target is unset?

Give me a worked example of conditional default assignment on a String.

Answer:
EK9 has a dedicated operator for 'assign if unset': :=? (guarded assignment). It assigns the right-hand value to the variable ONLY if the variable is currently unset. If the variable already has a value, the assignment is silently skipped — the existing value is preserved.

THE OPERATOR
  variable :=? defaultValue

WORKED EXAMPLE
  greeting as String: String()       // unset
  greeting :=? "Hello, World"        // assigned, because greeting was unset
  stdout.println(greeting)            // "Hello, World"

  greeting :=? "Goodbye"              // SKIPPED, because greeting is now set
  stdout.println(greeting)            // still "Hello, World"

AFTER THE FIRST GUARDED ASSIGNMENT, THE VARIABLE IS SET AND ALL FUTURE :=? CALLS ARE NO-OPS. This is exactly what 'assign default if unset' should mean.

WHY NOT IF + ASSIGNMENT?
The long-form equivalent works but is more verbose:
  if not greeting?
    greeting: "Hello, World"

The :=? operator collapses this into one line. It is also IMPOSSIBLE to forget the negation — there is only one direction the operator can go.

NOT TO BE CONFUSED WITH OTHER OPERATORS
- :=  is plain assignment (always assigns, overwrites)
- :=? is guarded assignment (assigns only if unset)
- <-  is declaration + first assignment (creates a new variable)
- <?  is coalescing minimum (RETURNS the smaller value, completely different family)

See Q1264 for Integer guarded assignment. See Q1265 for Date. See Q1266 for custom record. See Q1226 for the contrast between :=? and <?.

defines module qa.operators.guardedassignstring

  defines function

    //Sourced rather than a literal, so 'title' is genuinely already-set at runtime without
    //the compiler being able to prove it and reject the ':=?' below as dead (E08095).
    loadStoredTitle()
      <- rtn as String: "Untitled"

  defines program

    GuardedAssignStringDemo()
      stdout <- Stdout()

      greeting as String: String()
      greeting :=? "Hello, World"
      stdout.println(`first :=? : ${greeting}`)

      greeting :=? "Goodbye"
      stdout.println(`second :=? : ${greeting}`)

      // Variable that already has a value at declaration
      title <- loadStoredTitle()
      title :=? "Default Title"
      stdout.println(`pre-set :=? : ${title}`)

Java: if (variable == null) variable = default. Kotlin: variable = variable ?: default. Python: variable = variable or default (works but conflates falsy with None). Rust: variable = variable.unwrap_or(default). JavaScript: variable = variable ?? default (nullish coalescing assignment). EK9: variable :=? default — direct, single operator, only assigns when target is unset.

:=?
String
conditional assignment
default value
fallback
guarded assignment
unset

String guarded assignment
default value pattern
:=? operator

Explanation: :=? is guarded ASSIGNMENT (assign if unset). <? is coalescing MINIMUM (returns the smaller value). They are completely different operators and serve different purposes. See ek9 -h E50001 for details.


Question: Walk me through using :=? on an Integer retry counter so that the counter only gets initialised on the first call and is preserved on subsequent calls.

How do I initialise an Integer counter only once using :=?

Show me a worked example of guarded assignment for a retry counter.

Initialise an Integer to a default only when it has not been set.

How does :=? behave on Integer values that may already be set?

Answer:
The :=? guarded assignment operator works on any type, including Integer. It assigns the right-hand value ONLY when the target is currently unset. This is exactly what you want for 'initialise once, never overwrite' patterns like retry counters, lazy caches, and configuration defaults.

WORKED EXAMPLE: RETRY COUNTER
  retryCount as Integer: Integer()    // unset

  // First call: assigns 0 because retryCount was unset
  retryCount :=? 0
  stdout.println(`first :=? 0 -> ${retryCount}`)

  // Second call: SKIPPED because retryCount is now set to 0
  retryCount :=? 99
  stdout.println(`second :=? 99 -> ${retryCount}`)

The second :=? does NOT change the value, even though 99 is different from 0. The operator only checks 'is the target unset?', not 'is the value different?'.

INCREMENTING ON TOP OF GUARDED INIT
A common pattern is to guard the initial value, then mutate freely afterwards:
  attempts as Integer: Integer()
  attempts :=? 0          // initialise once
  attempts++              // 1
  attempts++              // 2
  attempts++              // 3
  stdout.println(`attempts: ${attempts}`)

The first :=? guarantees the counter starts from 0 even if the variable was created without a default. After that, ++ takes over.

WHY THIS IS SAFER THAN PLAIN :=
Using := blindly would overwrite the existing count and lose progress:
  retryCount: 0     // Always overwrites — bad in restart-safe code
  retryCount :=? 0  // Only sets if unset — safe to call multiple times

For any value that should be 'set once and stay', :=? is the right operator.

See Q1263 for String guarded assignment. See Q1265 for Date. See Q1266 for custom record. See Q1226 for the contrast between :=? and <?.

defines module qa.operators.guardedassigninteger

  defines program

    GuardedAssignIntegerDemo()
      stdout <- Stdout()

      retryCount as Integer: Integer()
      retryCount :=? 0
      stdout.println(`first :=? 0 -> ${retryCount}`)

      retryCount :=? 99
      stdout.println(`second :=? 99 -> ${retryCount}`)

      attempts as Integer: Integer()
      attempts :=? 0
      attempts++
      attempts++
      attempts++
      stdout.println(`attempts after ++: ${attempts}`)

Java: if (count == null) count = 0; — needs Integer wrapper, not int. Kotlin: count = count ?: 0. Rust: let count = count.unwrap_or(0); — but this rebinds, not modifies. Python: count = count if count is not None else 0. EK9: count :=? 0 — single operator, in-place, idempotent.

:=?
Integer
default value
guarded assignment
initialise once
lazy init
retry counter

Integer guarded assignment
initialise-once pattern
:=? semantics

Explanation: Integer does not have a guardedSet method. The :=? operator is the correct way to perform guarded assignment — it assigns only when the target is unset. See ek9 -h E50060 for details.


Question: How can I record a 'first seen' Date that is set on the first observation and never overwritten on subsequent observations, using :=?

Record a Date only on the first call without overwriting on later calls.

How do I implement 'first seen' semantics for a Date field?

Set a Date once and ignore later attempts to change it.

Answer:
The :=? guarded assignment operator is ideal for 'first seen' or 'first occurrence' patterns where you want to capture the FIRST value and ignore any later updates. It works on Date the same way it works on every other type — assign only if the target is unset.

WORKED EXAMPLE: FIRST SEEN DATE
  firstSeen as Date: Date()        // unset

  // First observation: assigned because firstSeen was unset
  firstSeen :=? 2025-03-10
  stdout.println(`first :=? -> ${firstSeen}`)

  // Later observations: SKIPPED because firstSeen is now set
  firstSeen :=? 2025-04-15
  firstSeen :=? 2025-05-20
  stdout.println(`final firstSeen -> ${firstSeen}`)

The value remains 2025-03-10 throughout. The 'first seen' semantics are exactly what :=? provides — there is no need for an external 'has been seen' flag.

WHY THIS IS USEFUL
- Audit trails: record when a record was first created
- Caches: store the first computed value, never recompute
- Logs: capture the timestamp of the first error in a session
- Statistics: track the start time of an aggregation window

IN ALL THESE CASES, you want the FIRST value to win and all later values to be silently dropped. :=? does this in one line.

ALTERNATIVE WITHOUT :=?
  if not firstSeen?
    firstSeen: 2025-03-10

This works but is verbose. The :=? operator collapses the check and assignment into one symbol.

See Q1263 for String guarded assignment. See Q1264 for Integer. See Q1266 for custom record. See Q1226 for the contrast between :=? and <?.

defines module qa.operators.guardedassigndate

  defines program

    GuardedAssignDateDemo()
      stdout <- Stdout()

      firstSeen as Date: Date()
      firstSeen :=? 2025-03-10
      stdout.println(`first :=? -> ${firstSeen}`)

      firstSeen :=? 2025-04-15
      firstSeen :=? 2025-05-20
      stdout.println(`final firstSeen -> ${firstSeen}`)

Java: if (firstSeen == null) firstSeen = LocalDate.of(2025, 3, 10). Kotlin: firstSeen = firstSeen ?: LocalDate.of(2025, 3, 10). Python: firstSeen = firstSeen or date(2025, 3, 10) — but this conflates falsy with None. Rust: firstSeen.get_or_insert(NaiveDate::from_ymd_opt(2025, 3, 10).unwrap()). EK9: firstSeen :=? 2025-03-10 — direct, single line, type-safe Date literal.

:=?
Date
audit
first occurrence
first seen
guarded assignment
set once

Date guarded assignment
first-seen pattern
audit timestamp

Explanation: Date does not have a setIfAbsent method. The :=? operator is the correct way to perform guarded assignment — it assigns only when the target is unset. See ek9 -h E50060 for details.


Question: Show me how to use :=? on a custom class field — give me a worked example where a UserSession class has a sessionToken that is set on first login and never overwritten.

How does :=? work inside a record method that sets a field once?

Give me a UserSession example using guarded assignment on a token field.

Use :=? on a custom record field for set-once semantics.

Show me a method that initialises a record field with :=? only when unset.

Answer:
The :=? guarded assignment works on class fields just as it does on local variables. It is the natural way to implement set-once fields without writing manual 'is this set?' checks. (For data-only carriers without behaviour, use a record — but records cannot have methods, so this example uses a class because it needs a captureToken method.)

USERSESSION CLASS WITH SET-ONCE TOKEN
  UserSession
    userName as String: String()
    sessionToken as String: String()

    UserSession()
      -> userName as String
      this.userName :=: userName

    captureToken()
      -> token as String
      sessionToken :=? token

    default operator ?

The captureToken method uses :=? on the field. The first call sets sessionToken. Every subsequent call is silently ignored — the original token is preserved.

USAGE
  session <- UserSession("alice")

  // First login captures the token
  session.captureToken("abc-123-first")
  stdout.println(`after first login: ${session.currentToken()}`)

  // Subsequent logins do NOT replace the token
  session.captureToken("xyz-456-second")
  session.captureToken("def-789-third")
  stdout.println(`after more logins: ${session.currentToken()}`)

The sessionToken stays as 'abc-123-first' regardless of how many times captureToken is called.

WHY THIS IS BETTER THAN MANUAL CHECKS
You could write the same logic with an if-check:
  captureToken()
    -> token as String
    if not sessionToken?
      sessionToken: token

This works but is verbose and easy to get backwards. The :=? operator does the right thing in one symbol with no risk of missing the negation.

WHEN TO USE GUARDED ASSIGNMENT ON FIELDS
- Authentication tokens captured on first login
- Origin URLs captured on first redirect
- Created-at timestamps that should never change
- Cached computed values that should be set once
- Configuration that loads on first access

In all cases, the goal is 'first value wins, later values ignored'. :=? is the right tool.

See Q1263 for String guarded assignment. See Q1264 for Integer. See Q1265 for Date. See Q1226 for the contrast between :=? and <?.

defines module qa.operators.guardedassignrecord

  defines class

    UserSession
      userName as String: String()
      sessionToken as String: String()

      UserSession()
        -> name as String
        this.userName :=: name

      captureToken()
        -> token as String
        sessionToken :=? token

      currentToken() as pure
        <- rtn as String: sessionToken

      default operator ?

  defines program

    GuardedAssignRecordDemo()
      stdout <- Stdout()

      session <- UserSession("alice")

      session.captureToken("abc-123-first")
      stdout.println(`after first login: ${session.currentToken()}`)

      session.captureToken("xyz-456-second")
      session.captureToken("def-789-third")
      stdout.println(`after more logins: ${session.currentToken()}`)

Java: if (sessionToken == null) sessionToken = token; — verbose. Kotlin: sessionToken = sessionToken ?: token — also works, but reassigns. Rust: sessionToken.get_or_insert(token); — standard library helper. Python: if self.sessionToken is None: self.sessionToken = token. EK9: sessionToken :=? token — single operator, in-place, idempotent.

:=?
UserSession
first value wins
guarded assignment
record field
session token
set once

:=? on record field
set-once field pattern
session token capture

Explanation: String does not have a guardedSet method. The :=? operator is the correct way to perform guarded assignment — it assigns only when the field is unset. See ek9 -h E50060 for details.


Control Flow Without break/continue/return

Question: How do I find the first matching item in a list without break or return in EK9?

Write a find-first function using guarded assignment :=? in a for loop.

How does :=? help me stop at the first match in EK9?

Show me the EK9 pattern for returning the first item that satisfies a condition.

In Java I'd use break after finding the first match — what does EK9 use?

Answer:
EK9 has no break or return. To find the first matching item, declare the return variable with a default (unset) value, then use the guarded assignment operator ':=?' inside the loop. ':=?' only assigns when the target is currently unset — so the first match sets it and all subsequent matches are ignored.

PATTERN
  findFirst()
    -> items as List of String
    -> searchTerm as String
    <- rtn as String: String()
    for item in items
      if item contains searchTerm
        rtn :=? item

HOW IT WORKS
1. rtn starts as String() — unset
2. Loop iterates every item (no early exit)
3. First match: rtn :=? item assigns because rtn is unset
4. Second match: rtn :=? item is SKIPPED because rtn is already set
5. After the loop, rtn holds the first match (or remains unset)

CALLER PATTERN
Use a guard expression to check if a match was found:
  if found <- findFirst(names, searchFor)
    stdout.println(found)
  else
    stdout.println("No match")

See Q1239 for the stream pipeline alternative (filter + head). See Q985 for :=? basics.

defines module qa.without.findfirstguard

  defines function

    findFirstContaining() as pure
      ->
        items as List of String
        searchTerm as String
      <- rtn as String: String()
      for item in items
        if item?
          if item contains searchTerm
            rtn :=? item

  defines program

    FindFirstDemo()
      stdout <- Stdout()

      names <- List() of String
      names += "Java"
      names += "EK9-Language"
      names += "Python"
      names += "EK9-Compiler"

      searchFor <- "EK9"

      if found <- findFirstContaining(names, searchFor)
        stdout.println(`First match: ${found}`)
      else
        stdout.println("No match found")

      if noMatch <- findFirstContaining(names, "Rust")
        stdout.println(`Found: ${noMatch}`)
      else
        stdout.println("No Rust match — correct")

Java: for-loop with break on first match. Python: next(x for x in items if condition, default). Kotlin: items.firstOrNull { condition }. Go: for-range with break. Rust: items.iter().find(|x| condition). EK9: rtn :=? match inside a for loop — no break, no return, the guard operator handles it.

:=?
find-first
first match
guard
guarded assignment
loop
no break
no return

:=? guarded assignment in loops
find-first pattern
no break alternative

Explanation: EK9 has no return statement. Use a return variable declaration (rtn as Type: default) and :=? guarded assignment to set it once. See ek9 -h E01072 for details.

Explanation: EK9 has no break statement. The :=? operator achieves the same effect — once rtn is set, further :=? assignments are ignored. See ek9 -h E01070 for details.


Question: Write a generic Pair class with two type parameters A and B that holds two values.

Define a generic Pair of type (A, B) with a constructor, operator ?, and operator $.

How do I create a generic class with two type parameters in EK9?

Show me a Pair class that holds first and second values of different types.

In Java I'd write Pair<A, B> — what is the EK9 equivalent?

Answer:
Generic classes with multiple type parameters use 'of type (A, B)' syntax. Key rules:

1. MUST have a default constructor: 'default Pair() as pure'
2. MUST have a parameterised constructor with both type params
3. Constructors MUST be PUBLIC — private/protected on generics triggers E06060
4. If any constructor is pure, ALL must be pure (E05190)
5. Use 'operator $' (not 'override operator $') since there is no parent $ to override
6. Use 'default operator ?' for auto-generated isSet semantics

SYNTAX
  Pair of type (A, B)
    first as A?
    second as B?
    default Pair() as pure
    Pair() as pure
      ->
        a as A
        b as B
      first :=? a
      second :=? b
    operator $ as pure
      <- rtn as String: ...
    default operator ?

See Q194 for single-parameter generics. See Q645 for why generic constructors must be public. See Q1243 for two-param Transformer example.

defines module qa.genericsdeep.pairtwoparams

  defines class

    Pair of type (A, B)
      first as A?
      second as B?

      Pair() as pure
        first :=? A()
        second :=? B()

      Pair() as pure
        ->
          a as A
          b as B
        first :=? a
        second :=? b

      operator $ as pure
        <- rtn as String: `(${$first}, ${$second})`

      default operator ?

  defines program

    PairDemo()
      stdout <- Stdout()

      nameAge <- Pair("Alice", 30)
      stdout.println(`Pair: ${nameAge}`)

      coordPair <- Pair(3.14, 2.72)
      stdout.println(`Coordinates: ${coordPair}`)

      if nameAge?
        stdout.println("Pair is set")

Java: class Pair<A, B> { A first; B second; }. Kotlin: data class Pair<A, B>(val first: A, val second: B). Rust: struct Pair<A, B> { first: A, second: B }. Python: from typing import Generic, TypeVar. EK9: 'Pair of type (A, B)' with explicit constructors, 'default operator ?' and 'operator $'.

A
B
constructor
default operator
generic
of type
pair
two type parameters

multi-parameter generics
Pair of type (A, B)
generic constructor rules

Explanation: Generic type constructors must be public so the type can be instantiated with any valid type parameter. Private or protected constructors prevent proper instantiation. See ek9 -h E06060 for details.

Explanation: A non-abstract constructor must provide a body; removing the no-arg constructor's body leaves it with no implementation while it is not declared abstract. See ek9 -h E07110 for details.

Explanation: Generic classes have no parent $ operator to override. Use 'operator $' without 'override'. See ek9 -h E05110 for details.