What are dynamic functions and how do they differ from lambdas?

← Getting Started · Ref: Q52

EK9 dynamic functions look superficially like lambdas but are fundamentally different in four critical ways. Understanding these differences is key to understanding EK9's power.

1. NOMINALLY TYPED

Every dynamic function MUST implement a named abstract function type. You cannot create an anonymous untyped lambda. This ensures every closure has a documented, compiler-verified contract:

  myAdd <- () is mathOperation as pure function
    result:=? x + y

In Java, a lambda like '(x, y) -> x + y' is structurally typed — the compiler guesses what interface it matches. In EK9, you explicitly declare 'is mathOperation', making the intent clear.

2. PARAMETER INFERENCE

Dynamic functions inherit parameter names from their abstract parent. You do NOT re-declare them. The abstract function 'mathOperation' declares parameters 'x' and 'y', so the dynamic function body can use 'x' and 'y' directly. This is DRY — the contract is defined once.

3. BLOCK vs INLINE SYNTAX
Block form uses 'as function' keyword with an indented body:

  myAdd <- () is mathOperation as pure function
    result:=? x + y

Inline form wraps the body in parentheses — this is the closest EK9 gets to a lambda:

  myAdd <- () is mathOperation as pure (result:=? x + y)

Inline is for single expressions. Multi-statement bodies require block form.

4. QUALITY ENFORCEMENT

Dynamic functions are subject to the SAME compile-time quality rules as named functions: cyclomatic complexity less than 11, nesting less than 4, descriptive variable names. In Java and Python, lambdas have zero quality enforcement — you can write arbitrarily complex, unreadable lambdas.

BOTH 'is' AND 'extends' WORK
Both keywords are equivalent for function implementation:

  using_is <- () is mathOperation as pure function
    result:=? x + y
  using_extends <- () extends mathOperation as pure
    result:=? x - y

WHY NOT ANONYMOUS LAMBDAS?

EK9 deliberately chose this design:
- READABILITY: Every dynamic function declares what type it implements
- TYPE SAFETY: Compiler verifies the signature match (not structural guessing)
- QUALITY: Same enforcement as named functions
- TRACEABLE: Data flow is explicit, not hidden

The result is that EK9 dynamic functions are more verbose than '(x, y) -> x + y' but dramatically safer, more readable, and maintainable. See Q53 for how variable capture works. See Q51 for abstract function types. See Q59 for using dynamic functions in stream pipelines. See Q89 for stream pipeline basics. See Q115 for dynamic classes. See Q235 for complete stream operations reference. See Q601 for how dynamic functions replace nested functions.

Example

defines module qa.dynamicfunction

  defines function

    mathOperation() as pure abstract
      ->
        x as Float
        y as Float
      <- result as Float?

  defines program

    DynamicFunctionDemo()
      stdout <- Stdout()

      // Block form with 'as function'
      dynamicAdd <- () is mathOperation as pure function
        result:=? x + y

      stdout.println(`block add: ${dynamicAdd(7.0, 2.0)}`)

      // Block form with 'extends'
      dynamicSub <- () extends mathOperation as pure
        result:=? x - y

      stdout.println(`block sub: ${dynamicSub(7.0, 2.0)}`)

      // Inline form — closest EK9 gets to a lambda
      inlineMul <- () is mathOperation as pure (result:=? x * y)

      stdout.println(`inline mul: ${inlineMul(7.0, 2.0)}`)

      // Multiple inline functions in a list
      ops <- [
        () is mathOperation as pure (result:=? x + y),
        () is mathOperation as pure (result:=? x - y),
        () is mathOperation as pure (result:=? x * y)
      ]

      for op in ops
        stdout.println(`op(6.0, 3.0) = ${op(6.0, 3.0)}`)

Common mistakes

E08120 — If the abstract function is declared 'as pure', the dynamic function body cannot use mutation operators like +=. This triggers E08120 — mutating variables not allowed in pure scope. Use reassignment with :=? or : instead. See ek9 -h E08120 for details.

Incorrect:

dynamicAdd <- () is mathOperation as pure function
        result += x

Correct:

dynamicAdd <- () is mathOperation as pure function
        result:=? x + y
Other ways to ask this
  • How do EK9 dynamic functions differ from lambdas in other languages?
  • Why does EK9 use dynamic functions instead of lambdas?
  • What is the inline dynamic function syntax in EK9?

Coming from another language?

Java: lambdas are structurally typed against functional interfaces, (x, y) -> x + y lets compiler guess the target type, no explicit type declaration at usage, lambda bodies can be arbitrarily complex with no quality enforcement, method references (::) are a separate syntax. Python: lambda limited to single expression, def functions have no type relationship, closures capture by reference with notorious late-binding bug. JavaScript: arrow functions ((x, y) => x + y) are structural, no type contract, no quality enforcement, 'this' binding confusion, mutable closure state is major bug source. Rust: closures are structurally typed via Fn/FnMut/FnOnce traits, cannot name closure types without impl Trait, no compile-time quality limits. Go: anonymous functions func(x, y float64) float64 are structural, no type hierarchies, no quality enforcement. Kotlin: lambdas { x, y -> x + y } are structural, fun interface for SAM but structural matching, no quality enforcement on lambdas. Swift: closures { (x: Double, y: Double) -> Double in x + y } are structural, no nominal typing for closures. EK9: dynamic functions are NOMINALLY typed (must implement named abstract function), parameters inherited from abstract parent (DRY), block and inline syntax, same compile-time quality enforcement as named functions.

Keywords: migrate, swift, start, nominal, typed, dynamic, block, implement, first, closure, beginner, abstract, intro, quality, capture, anonymous, function, structural, enforcement, parameter, lambda, inference, inline