Why does EK9 reject a deeply nested higher-order call chain like getMaker(x)(y)(z)?

← Code Quality · Ref: Q1342

EK9 allows single-level functional chaining (e.g. getTransformer("upper")("text") - a delegate-returning function whose delegate is then invoked) but rejects deeper nesting with E11069. A two-level chain such as getMaker("x")("seed")("text") forces inside-out mental evaluation: you must work out what getMaker("x") returns, then what calling that with ("seed") returns, then call THAT with ("text"). Each level hides a type transition. The fix is to extract each intermediate result into a named, typed local variable so every step is independently readable, inspectable, and reviewable. Single-level chaining remains valid and idiomatic.

See Q311 for quality checks.

Example

defines module qa.quality.call.chain

  defines function

    Transformer() as pure abstract
      -> input as String
      <- output as String?

    UpperTransformer() extends Transformer as pure
      -> input as String
      <- output as String: input.upperCase()

    Maker() as abstract
      -> seed as String
      <- transformer as Transformer?

    SimpleMaker() extends Maker
      -> seed as String
      <- transformer as Transformer: UpperTransformer

    getMaker()
      -> kind as String
      <- maker as Maker: SimpleMaker

      if kind == "simple"
        maker: SimpleMaker

    //THE FIX: extract each intermediate result into a named, typed local.
    //getMaker returns a Maker, the Maker returns a Transformer, the
    //Transformer returns a String. Each step is now independently clear,
    //instead of one inside-out chain getMaker("simple")("seed")("text").
    buildResult()
      <- result as String?
      maker <- getMaker("simple")
      transformer <- maker("seed")
      result: transformer("text")

  defines program

    ExcessiveCallChainDemo()
      stdout <- Stdout()
      result <- buildResult()
      stdout.println(`Result is ${result}`)

Common mistakes

E11069 — The expression is a call applied to the result of a call applied to the result of a call - two levels of nesting beyond a plain call. This forces inside-out evaluation and hides each type transition. Extract each step into a named typed local (maker, transformer) so every line documents its type and the final call is single-level. Single-level chaining such as getTransformer("upper")("text") is fine. See ek9 -h E11069 for details.

Incorrect:

result: getMaker("simple")("seed")("text")

Correct:

maker <- getMaker("simple")
      transformer <- maker("seed")
      result: transformer("text")
Other ways to ask this
  • What triggers E11069 EXCESSIVE_CALL_CHAIN?
  • Why can't I chain more than one functional call in EK9?
  • How do I fix a call applied to the result of a call applied to a call?

Coming from another language?

JavaScript/curried FP: f(a)(b)(c)(d) is unlimited and unchecked, leaving the reader to trace types inside-out. Haskell/Scala: deep currying is idiomatic but relies on a type-aware editor to reveal intermediate types. C: the same cognitive problem as nested pointer dereferences (*(**fp)(x))(y). EK9: compile-time error (E11069) at one level beyond single chaining, forcing extraction into named intermediate variables that document each type.

Keywords: intermediate, call, extract, chain, quality, E11069, higher-order, nested, delegate