Dynamic Functions in EK9

A dynamic function is a function you define in any scope, rather than in a functions block. It is anonymous, you refer to it through a function delegate, and it always extends another function: an abstract function, or one declared as open.

Dynamic functions are a lightweight way to supply specific, varied behaviour. In some ways they are like lambdas.

Unlike a normal function, a dynamic function can capture variables, and so hold state (a little like a closure). That may go against the grain if you come from functional programming, and you don't have to use it. It earns its place in stream pipelines: a dynamic function can retain or collate state as the pipeline runs, rather than leaving everything to a single 'reduce' at the end.

Quality Enforcement: the compiler applies its quality limits to dynamic functions: cyclomatic complexity at most 45, cognitive complexity 35, nesting depth 6 and 150 statements. Variable names, captured ones included, must be descriptive (no temp, flag, data, value, buffer, object; E11031). A variable you capture but never use is an error (E11018), and captured variables count towards coupling (Ce). See Code Quality for the full list.

Example

The function example shows how standard functions and abstract functions are implemented, and the dictionary example uses dynamic functions in stream pipelines ('nameMapping', 'dateMapping', 'recordMapping' and 'functionMapping').

Here, dynamic functions replace functions you would otherwise declare in the functions block.

#!ek9
defines module site.dynamicFunctions.math

  defines function

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

  defines program

    DynamicMathExample1()
      stdout <- Stdout()
      stdout.println("Math Dynamic Operation Example1")
      fixedValue <- 9.8

      // is/extends and 'as function' are just syntactic sugar, examples of each
      // notice how the incoming and returning parameter names are inferred from mathOperation

      addFunction <- () is mathOperation as pure function
        result: x+y

      subtractFunction <- () extends mathOperation as pure
        result: x-y

      divideFunction <- () is mathOperation pure
        result: x/y

      multiplyFunction <- () is mathOperation pure
        result: x*y

      specialFunction <- (fixedValue) is mathOperation pure
        result: (x+fixedValue) * y^fixedValue

      // common super of mathOperation is inferred and ops is a 'List of mathOperation'
      ops <- [addFunction, subtractFunction, divideFunction, multiplyFunction, specialFunction]

      for op in ops
        stdout.println(`Result: ${op(21, 7)}`)

    DynamicMathExample2()
      stdout <- Stdout()
      stdout.println("Math Dynamic Operation Example2")
      fixedValue <- 9.8

      //Nearest EK9 gets to a lambda - wrap in parenthesis on single line.
      //Notice the last entry - 'captures' fixedValue and can use it in the function body
      ops <- [
        () is mathOperation as pure (result: x+y),
        () is mathOperation as pure (result: x-y),
        () is mathOperation as pure (result: x/y),
        () is mathOperation as pure (result: x*y),
        (fixedValue) is mathOperation pure (result: (x+fixedValue) * y^fixedValue)
        ]

      for op in ops
        stdout.println(`Result: ${op(21, 7)}`)

    DynamicMathExample3()
      stdout <- Stdout()
      stdout.println("Math Dynamic Operation Example3")
      fixedValue <- 9.8

      //Same example but uses an inline list in the for loop.
      for op in [
        () is mathOperation as pure (result: x+y),
        () is mathOperation as pure (result: x-y),
        () is mathOperation as pure (result: x/y),
        () is mathOperation as pure (result: x*y),
        (fixedValue) is mathOperation pure (result: (x+fixedValue) * y^fixedValue)
        ]
        stdout.println(`Result: ${op(21, 7)}`)

Running 'DynamicMathExample1' prints:

Math Dynamic Operation Example1
Result: 28.0
Result: 14.0
Result: 3.0
Result: 147.0
Result: 5.895375993827891E9

'DynamicMathExample2' and 'DynamicMathExample3' compute the same results with inline dynamic functions; 'DynamicMathExample2' prints:

Math Dynamic Operation Example2
Result: 28.0
Result: 14.0
Result: 3.0
Result: 147.0
Result: 5.895375993827891E9
Explanation

A capture shares the object it names - EK9 has no primitives - so a mutating operator (++, +=, :=:) on a captured variable is seen wherever that object is seen.

Why EK9 does it this way. This is similar to a lambda or closure, but not the same: you capture variables explicitly, where a lambda or closure captures them automatically. That is a deliberate design decision.

Why two ways to define functions?

Some functions are widely useful, so you name them and reuse them. Others are useful in only a few places and can be 'anonymous'. And sometimes you want a function to hold state, or to pass one back from a higher order function or class and pass it around like a variable.

Why EK9 does it this way. The approach gives the most flexibility, at the cost of some variation in syntax. At first EK9 had only standard functions; then abstract functions proved essential. Stream pipelines needed lightweight, 'lambda'-like functions, but to keep the pipeline readable and encourage reuse they were designed to be used through delegates. As a compromise, a single-line dynamic function can be wrapped in parentheses.

Create dynamic functions through higher order functions, so you can unit test them. For a trivial dynamic function, whether to unit test it is your call.

Next Steps

Where there are dynamic functions, there are Dynamic Classes. They differ a little in nature, for good reason.

Together, abstract functions, standard functions and dynamic functions give you a polymorphic, type-safe way to write flexible, reusable functionality.