Dynamic Functions in EK9

These are just functions, but rather than being declared in the functions block they can be defined and used in any scope. They are by their nature 'anonymous' but can be referenced as a function delegate and they must always extend an abstract function.

The main use of these dynamic functions is to provide a very light weight and easy mechanism to provide specific and varied functionality. You could view these as similar to lambdas in some ways.

But there is one significant and valuable difference to normal functions; dynamic functions can (but don't have to) capture variables and can therefore hold state (a little like a closure). This gives them different capabilities to normal functions as demonstrated in the following example.

This fact may go against the grain for many with a functional background (you don't have to use them!). They have real power and value when building stream pipelines; using dynamic functions where you want to retain/collate state information as part of pipeline process rather than just depending on a single 'reduce' at the end of the pipeline gives you flexibility in processing.

Example

This function example shows how standard functions and abstract functions can be implemented. There is also an example that shows how dynamic functions can be used in stream pipelines (see 'nameMapping', 'dateMapping', 'recordMapping' and 'functionMapping').

The example below highlights how dynamic functions can provide a lightweight alternative to declaring functions in the functions block.

#!ek9
defines module introduction

  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 syntatic 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)}`)
//EOF
        

The code above will produce the following output:

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

The use of pure is useful as it makes it very clear that nothing is 'mutated'. If you are going to use a functional approach or are very keen on immutability the use of pure is essential in EK9.

The add, subtract, divide and multiply functions are quite short and to the point, note that the incoming and returning parameters do not need to be declared. They are just assumed. The syntax:
addFunction <- () is mathOperation
is just a declaration of a new function delegate 'addFunction' as a function that extends (is a) 'mathOperation'.

The suffix syntax of 'as function' is optional, 'addFunction' shows this the other functions omit this syntax. This is developer choice, but it is quite obvious that a new dynamic function has been defined because it is/extends 'mathOperation' and that is a function.

The 'specialFunction' has a completely different mathematical operation; importantly it captures a variable. The variable must be named; it cannot be a literal such as 9.8. It has to be named so that it can be addressed in the function body. Any number of variables can be captured and used in the function body.

This is similar (but not the same) to a lambda/closure in many ways, but the 'capturing' of variables is explicit and not automatic like lambdas and closures. This is a deliberate design decision.

Why two ways to define functions?

Some functions are widely useful and should therefore be named and reused. Others really are just useful in a few contexts and can therefore be 'anonymous'. But there are times when you want to hold state in functions or pass them back from higher order functions/classes and pass them around like variables.

The approach in EK9 gives maximum flexibility; albeit at the cost of variation in syntax. Initially EK9 only had standard functions, then it became obvious that abstract functions were essential. With the introduction of stream pipelines that concept of 'lambda' like lightweight functions were needed, but so as not to distract from the pipeline and encourage reuse - they were designed to be used via delegates. But as a compromise, a single line dynamic function can be wrapped in parentheses.

When defining dynamic functions it is best to create them via higher functions and that way they can be unit tested. Clearly for trivial dynamic functions you have to make the call on unit testing!

Next Steps

As you would expect, if there are dynamic functions; there are Dynamic Classes. These are sightly different in nature however (with good reason).

But the combinations of abstract functions, standard functions and dynamic functions provide a polymorphic and type safe way to implement flexible/reusable functionality.