How do dynamic functions replace nested functions in EK9?

← Functions and Methods · Ref: Q601

EK9 does not support nested function definitions. You cannot define a named function inside another function. Dynamic functions with closures serve the same purpose: they are created inline, capture variables from the enclosing scope, and exist only within that scope.

WHY NO NESTED FUNCTIONS

Nested function definitions complicate scope analysis and name resolution. They create hidden coupling between the inner function and the outer function's variables. EK9 keeps function definitions flat (at module level) to make dependencies explicit.

DYNAMIC FUNCTIONS AS LOCAL HELPERS

Define an abstract function type at module level, then create dynamic implementations inline where needed:

  Validator as pure abstract
    -> text as String
    <- valid as Boolean?

Inside a program or function body, create a dynamic function:

  checker <- (minLen) is Validator as pure function
    valid:=? length text >= minLen

The dynamic function captures 'minLen' from the enclosing scope and implements the Validator contract. It behaves exactly like a nested function: scoped locally, accessing outer variables, callable only within the enclosing scope.

INLINE SINGLE-EXPRESSION FORM

For simple helpers, use inline syntax:

  notEmpty <- () is Validator as pure (valid:=? length text > 0)

MULTIPLE LOCAL HELPERS

Create several dynamic functions in the same scope, each capturing different values:

  short <- (shortLen: 3) is Validator as pure (valid:=? length text >= shortLen)
  long <- (longLen: 10) is Validator as pure (valid:=? length text >= longLen)

POLYMORPHIC USE

Because all dynamic functions implement the same abstract type, they can be stored in lists and iterated polymorphically.

See Q52 for dynamic function syntax. See Q53 for closure capture mechanics. See Q115 for dynamic classes. See Q590 for dynamic function extension. See Q596 for function vs method distinction.

Example

defines module qa.functionsAndMethods.dynamicAsNested

  defines function

    //Abstract function type: the contract for validators
    Validator as pure abstract
      -> text as String
      <- valid as Boolean?

    //Named implementation for comparison
    NotBlankValidator is Validator as pure
      -> text as String
      <- valid as Boolean: length text > 0

  defines program

    DynamicAsNestedDemo()
      stdout <- Stdout()

      // === DYNAMIC FUNCTION AS LOCAL HELPER ===

      //Captures minLen from enclosing scope
      minLen <- 5
      lengthChecker <- (minLen) is Validator as pure function
        valid:=? length text >= minLen

      stdout.println(`"hello" valid (min 5): ${lengthChecker("hello")}`)
      stdout.println(`"hi" valid (min 5): ${lengthChecker("hi")}`)

      // === INLINE SINGLE-EXPRESSION FORM ===

      notEmpty <- () is Validator as pure (valid:=? length text > 0)
      stdout.println(`"" not empty: ${notEmpty("")}`)
      stdout.println(`"x" not empty: ${notEmpty("x")}`)

      // === MULTIPLE LOCAL HELPERS WITH DIFFERENT CAPTURES ===

      shortLen <- 3
      longLen <- 10
      shortChecker <- (shortLen) is Validator as pure (valid:=? length text >= shortLen)
      longChecker <- (longLen) is Validator as pure (valid:=? length text >= longLen)

      testWord <- "medium"
      stdout.println(`"${testWord}" short enough: ${shortChecker(testWord)}`)
      stdout.println(`"${testWord}" long enough: ${longChecker(testWord)}`)

      // === POLYMORPHIC: all are Validator type ===

      validators <- [NotBlankValidator, notEmpty, lengthChecker, shortChecker, longChecker]
      for validator in validators
        stdout.println(`validates "test": ${validator("test")}`)

Common mistakes

E05150 — When the abstract function is declared as pure, all implementations must also be pure. Omitting 'as pure' triggers E05150 because 'pure' in super requires 'pure' for this definition. See ek9 -h E05150 for details.

Incorrect:

NotBlankValidator is Validator
      -> text as String
      <- valid as Boolean: length text > 0

Correct:

NotBlankValidator is Validator as pure
      -> text as String
      <- valid as Boolean: length text > 0

E50060 — Boolean has no toString() method in EK9. Use the $ prefix operator or string interpolation. See ek9 -h E50060 for details.

Incorrect:

stdout.println(lengthChecker("hello").toString())

Correct:

stdout.println(`"hello" valid (min 5): ${lengthChecker("hello")}`)
Other ways to ask this
  • Can I define a function inside another function in EK9?
  • What replaces nested functions or local functions in EK9?
  • How do I create helper functions scoped to another function?

Coming from another language?

Java: no nested methods (until Java 22 unnamed classes), inner classes as workaround, lambdas for inline behaviour. Python: def inside def creates nested functions, closures capture by reference (late-binding bug). JavaScript: function inside function creates closures, common pattern but 'this' binding is error-prone. Rust: closures defined inline with |args| body, cannot define named fn inside fn (unstable), closures capture by reference or move. Go: anonymous functions inline, func() {} closures, capture by reference. Kotlin: local functions (fun inside fun) supported, closures capture mutable state. Swift: nested functions supported, closures for inline behaviour. EK9: no nested function definitions, dynamic functions with explicit capture serve the same purpose, scoped to enclosing function, polymorphic through abstract function types.

Keywords: function, capture, anonymous, dynamic, parameter, inline, nested, method, helper, local, abstract, closure, scope