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 light weight alternative to declaring functions in the functions block.

#!ek9
defines module introduction
  defines type
    List of mathOperation

  defines function

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

  defines program
        
    DynamicMathExample1()
      stdout <- Stdout()
      stdout.println("Math Dynamic Operation Example1")
      fixedValue <- 9.8
      
      //You can use 'is' or 'extends', 'as' and 'function' are optional
      addFunction <- () is mathOperation as pure function
        result: x+y

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

      multiplyFunction <- () is mathOperation as pure
        result: x*y
      
      //Capture 'fixedValue' so it can be used in the dynamic function
      specialFunction <- (fixedValue) is mathOperation as pure
        result: (x+fixedValue) * y^fixedValue
      
      //Make a list of mathOperation functions (polymorphism of functions)      
      for op in [addFunction, subtractFunction, divideFunction, multiplyFunction, specialFunction]
        stdout.println(`Result: ${op(21, 7)}`)
        
    DynamicMathExample2()
      stdout <- Stdout()
      stdout.println("Math Dynamic Operation Example2")
      fixedValue <- 9.8
      
      //It is possible to delare the list with the dynamic functions in - if the function is a one liner               
      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)}`)        
//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) '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.

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. Finally 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 parenthesis.

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.