How do dynamic classes work in EK9?

← Classes and OOP · Ref: Q115

Dynamic classes in EK9 are anonymous inline classes that implement traits. They capture variables from the enclosing scope, similar to dynamic functions but for trait implementations.

DYNAMIC CLASS BASICS

Create an anonymous class inline using '() with trait of':

  response <- () with trait of HTTPResponse
    override content() as pure
      <- rtn as String: "hello"
    default operator ?

CAPTURE VARIABLES

Capture values from the enclosing scope. Any type can be captured — String, Boolean, Integer, or any other type:

  handler <- (message: msg) with trait of Handler
    override handle()
      <- rtn as String: message
    default operator ?

MULTIPLE CAPTURES

Capture multiple variables of different types in a single dynamic class:

  logger <- (logging: enableLog, prefix: tag) with trait of Handler
    override handle()
      <- rtn as String: `[${prefix}] logging=${logging}`
    default operator ?

CAPTURE FIXED VALUES

Literal values and function returns must use named captures:

  configured <- (maxRetries: 3, label: "RETRY") with trait of Handler

Simple variable identifiers can be unnamed, but expressions must be named.

IMPLEMENTING TRAITS INLINE

Dynamic classes can implement any trait. Override abstract methods and provide a default operator. The trait delegation pattern also works:

  (delegate: baseHandler) with trait of Handler by delegate

WHEN TO USE DYNAMIC CLASSES

Use dynamic classes for one-off trait implementations, especially in service responses, callbacks, and strategy patterns where a full named class would be excessive.

See Q52 for dynamic functions. See Q106 for traits. See Q108 for implementing traits. See Q112 for services using dynamic classes. See Q233 for dynamic class dependency injection. See Q262 for observer pattern with dynamic function listeners. See Q263 for factory pattern with dynamic class products.

Example

defines module qa.oop.dynamicclasses

  defines trait

    Handler
      handle() as abstract
        <- rtn as String?

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

    Transformer
      transform() as abstract
        -> input as String
        <- rtn as String?

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

  defines class

    DefaultHandler with trait of Handler
      override handle()
        <- rtn as String: "default"
      default operator ?

  defines function

    getPrefix()
      <- rtn as String: "AUTO"

  defines program

    DynamicClassDemo()
      stdout <- Stdout()

      // === DYNAMIC CLASS: anonymous trait implementation ===

      greeting <- () with trait of Handler
        override handle()
          <- rtn as String: "Hello from dynamic class"
        default operator ?

      stdout.println(greeting.handle())

      // === CAPTURE: single String variable ===

      msg <- "Captured message"
      captured <- (message: msg) with trait of Handler
        override handle()
          <- rtn as String: message
        default operator ?

      stdout.println(captured.handle())

      // === CAPTURE: single Boolean variable ===

      verbose <- true
      detailed <- (showDetail: verbose) with trait of Handler
        override handle()
          <- rtn as String: `Detail: ${showDetail}`
        default operator ?

      stdout.println(detailed.handle())

      // === CAPTURE: multiple variables (Boolean + String) ===

      enableLog <- true
      logTag <- "LOG"
      logger <- (logging: enableLog, prefix: logTag) with trait of Handler
        override handle()
          <- rtn as String: `[${prefix}] logging=${logging}`
        default operator ?

      stdout.println(logger.handle())

      // === CAPTURE: fixed literal values ===

      configured <- (maxRetries: 3, label: "RETRY") with trait of Handler
        override handle()
          <- rtn as String: `${label}: max ${maxRetries}`
        default operator ?

      stdout.println(configured.handle())

      // === CAPTURE: function return value ===

      autoPrefix <- getPrefix()
      auto <- (prefix: autoPrefix) with trait of Handler
        override handle()
          <- rtn as String: `[${prefix}] auto-configured`
        default operator ?

      stdout.println(auto.handle())

      // === CAPTURE: Boolean with Transformer trait ===

      shouldUpper <- true
      transformer <- (upper: shouldUpper) with trait of Transformer
        override transform()
          -> input as String
          <- rtn as String: `upper=${upper} input=${input}`
        default operator ?

      stdout.println(transformer.transform("hello"))

      // === DELEGATION ===

      base <- DefaultHandler()
      delegated <- (delegate: base) with trait of Handler by delegate

      stdout.println(delegated.handle())

Common mistakes

E05120 — Dynamic classes implementing a trait must use 'override' for abstract trait methods. The inline implementation of Handler's handle() method requires 'override'. See ek9 -h E05120 for details.

Incorrect:

handle()

Correct:

override handle()

E07140 — A dynamic class implementing Handler must override all abstract methods. Omitting handle() leaves the abstract method unimplemented, triggering E07140. See ek9 -h E07140 for details.

Incorrect:

default operator ?

Correct:

override handle()
          <- rtn as String: "Hello from dynamic class"
        default operator ?

E50040 — Dynamic classes are concrete inline implementations — they cannot contain abstract methods. Declaring a method 'as abstract' inside a dynamic class triggers E50040. See ek9 -h E50040 for details.

Incorrect:

badMethod() as abstract
          <- rtn as String?

Correct:

override handle()
          <- rtn as String: message

E50020 — Dynamic classes use 'with trait of' to implement traits. DefaultHandler is a class, not a trait — using it triggers E50020 because the genus is incompatible. Only traits can be used with dynamic class syntax. See ek9 -h E50020 for details.

Incorrect:

() with trait of DefaultHandler

Correct:

() with trait of Handler

E06240 — When capturing variables in a dynamic class, either all captures must be named or all must be unnamed. Mixing named captures (logging: enableLog) with unnamed captures (logTag) triggers E06240. See ek9 -h E06240 for details.

Incorrect:

(logging: enableLog, logTag) with trait of Handler

Correct:

(logging: enableLog, prefix: logTag) with trait of Handler

E06230 — Literal values and expressions must use named captures. Unnamed captures only work for simple variable identifiers. Capturing literal '3' or '"RETRY"' without naming triggers E06230. See ek9 -h E06230 for details.

Incorrect:

(3, "RETRY") with trait of Handler

Correct:

(maxRetries: 3, label: "RETRY") with trait of Handler

E02040 — Each capture field name must be unique within a dynamic class. Using 'logging' as the name for two different captures creates duplicate fields, triggering E02040. See ek9 -h E02040 for details.

Incorrect:

(logging: enableLog, logging: logTag) with trait of Handler

Correct:

(logging: enableLog, prefix: logTag) with trait of Handler
Other ways to ask this
  • How do I create anonymous classes in EK9?
  • What are inline trait implementations in EK9?
  • How do I capture variables in a dynamic class?
  • How do I implement a trait inline without a named class?

Coming from another language?

Java: anonymous inner classes with 'new Interface() { ... }', captures effectively final variables from enclosing scope. Python: no anonymous classes, use lambdas or nested classes, closures capture by reference. Rust: closures implement Fn traits with move semantics, no anonymous struct implementations. Go: no anonymous interface implementations, closures capture variables. Kotlin: object expressions with 'object : Interface { ... }', captures from enclosing scope automatically. EK9: '() with trait of T' creates inline class with explicit named captures (name: value), supports any type including Boolean and Integer, delegation support via 'by', concise syntax for one-off implementations.

Keywords: implement, capture, callback, scope, closure, class, delegation, anonymous, object-oriented, inline, dynamic, trait