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.
← Design Patterns and Idioms · Ref: Q1248
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.
Example
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))
Common mistakes
E07110 — 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.
Incorrect:
Handler() as pure
Correct:
Handler() as pure abstract
Other ways to ask this
- 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.
Coming from another language?
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.
Keywords: abstract function, first match, pipeline, pattern, dynamic function, handler, delegation, chain of responsibility