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