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