Functions in EK9

The Function was briefly outlined in the function section in structure. It is shown in more detail here.

Uses for Functions

A function is in general a stateless unit of processing. Though with EK9 functions and more specifically dynamic functions can be made stateful. In addition, you can develop generic/template functions.

The examples of functions detailed here are:

The creation of a library of functions that are small, specific and compact can really provide a strong 'core' of capability in software development. Prior to the widespread use of Object-Oriented programming, they were the cornerstone of development.

If you are from an Object-Oriented background you may find the introduction of functions a little strange at first. Just think of them as a class with one public method. They really solve the issues around trying to decide 'where to put functionality', which can be an issue when only classes can be used.

They remove the need for static methods on classes. They enable small discrete 'chunks' of code to find a home; decoupled from any class and housed right into a module as a peer of any class you define.

EK9 depends heavily on functions for stream pipelines and also flexible behaviour in objects. You will probably find the functions you do build get heavily used/reused.

In general functions tend to be much simpler and constrained than classes, this brings more agility and much easier unit testing. With EK9 development it is recommended to balance a number of functions with a range of records, classes, enumerations and components; as this can provide a great deal of flexibility in developing solutions.

Simple Functions

These are the basic functions; and are very similar to functions in the 'C' language. They are typically stateless and only process data that is passed in. They may optionally return a value.

In general these are pure in nature and only work on the data passed in, return a value and typically have no side effects. These really are the very best functions to have, because they are so simple, testable and predictable (there is just very little to go wrong).

#!ek9
defines module introduction

  defines function
    multiplier() as pure
      <- value as Float: 5.6
      
  defines program
    ShowSimpleFunction()
      stdout <- Stdout()
      
      stdout.println("The multiplier is " + $ multiplier())
      
      calculatedResult <- 6 * multiplier()
      stdout.println(`The result is ${calculatedResult}`)
//EOF

The example above shows a very simple function called 'multiplier'. It just returns a fixed value of 5.6. That value is then printed out and also used in a simple calculation in the program 'SimpleFunction'. Clearly a constant could have been used here.

Abstract Functions

A function that is defined as being abstract is really just a signature of a function. Unlike methods on classes; function names cannot be overloaded, the names can be used again in different modules. But within a module the name of a function must be unique.

The point of the abstract function is to create an abstract concept to enable polymorphism for functions. While useful for simple functions; it is a critical concept for dynamic functions.

#!ek9
defines module introduction

  defines function
  
    mathOperation() as pure abstract
      -> value as Float
      <- result as Float?

    multiply() is mathOperation as pure
      -> value as Float
      <- result as Float: value * 5.6
      
  defines program
    ShowAbstractFunction()
      stdout <- Stdout()
      
      calculatedResult <- multiply(9)
      stdout.println(`The result is ${calculatedResult}`)
//EOF

The example above shows the 'multiply' function that is a 'mathOperation'. The use of the 'mathOperation' nature is not actually used in this example. See later examples where it is applied. But this does not change the fact that the code now makes it obvious that 'multiply' is a type of 'mathOperation'. These abstract functions are revisited again in dynamic functions.

The key point here is the introduction of an abstract concept; i.e. the 'mathOperation', that's the value being created. For some programmers the idea of a function having some sort of type may seem strange. I'm not referring to the type the function may return here (i.e. Void or Integer for example), but the actual function itself.

You will notice that the key word pure has been used as well, this keyword is important if you are really concerned about immutability. EK9 does not use 'final' or 'const' or anything like that. If you really want to drive immutability then make as much of your code as pure as possible. Never reassign variables, always allocate and initialise in a single step where possible.

Higher Order Functions/Function Delegates

This example now makes use of the abstract function 'mathOperation' in the sense that it defines a higher order function 'suitableMathOperation' (i.e. a function that returns a function).

The function 'suitableMathOperation' declares a return parameter of 'mathOperation' meaning that it could return any function that meets that signature. In this case it returns 'multiply'. Note it does not call 'multiply' but returns it as a 'delegate'. It could have been configured to return 'heavyMultiply', rather than 'multiply' as both meet the same signature.

The program 'ShowHighOrderFunction' calls the higher order function 'suitableMathOperation' and holds the return value as a function delegate. It has no idea what type of 'mathOperation' it has the delegate for. It can then call the delegate (mathOp(9)), to get the calculated result.

There is a more practical demonstration of higher order function use in the fully worked CLI example

#!ek9
defines module introduction

  defines function

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

    multiply() is mathOperation as pure
      -> value as Float
      <- result as Float: value * 5.6

    heavyMultiply() is mathOperation as pure
      -> value as Float
      <- result as Float: value * 7.99

    suitableMathOperation()
      -> value as Float
      <- op as mathOperation: value < 21 <- heavyMultiply else multiply

  defines program
    ShowHighOrderFunction()
      stdout <- Stdout()

      value <- 9
      mathOp <- suitableMathOperation(value)
      calculatedResult <- mathOp(value)
      stdout.println(`The result is ${calculatedResult}`)

      //Now a dynamic function example.
      inlineFunction <- (
        v1: 12,
        v2: 64,
        v3: 90
        ) is mathOperation as (result: v1 + v2 + v3 * value)

      additionalCalculation <- inlineFunction(value)
      // 12 + 64 + 90*9
      stdout.println(`The additional result is ${additionalCalculation}`)
//EOF

The concept of functions delegates is very powerful. When used in conjunction with other constructs it enables you to alter behaviour on an object by object basis. Rather than just using traditional mechanisms, such as inheritance.

Higher order functions are most useful when given some sort of discriminator parameter, so that they can alter the actual function returned. But even in the example above it should be obvious that the program 'ShowHighOrderFunction' has been decoupled from the actual math operation it will be using; a level of abstraction/decoupling has been added. There is another example of this here, this shows how function delegates can be used with Lists and can also be called via stream pipelines. The use of functions is also shown with Dictionaries.

But care must be taken with the levels of abstraction being created. There is a fine balance between abstraction and complexity. Most forms of abstraction provide some value but come at the cost of some increase in cognitive loading.
It is important to try and balance those two opposing forces.

If there was now a need to alter the multiply functionality; for example if the values was within certain ranges then one multiplier was to be used, but in other ranges another multiplier was to be applied, then this could be altered.

The function returned by 'suitableMathOperation' varies based on the value it will be used with. Any sort of discriminator could be used for this capability.

A ternary operator is used to check if the value is less than 21. If so it returns 'heavyMultiply' otherwise it returns 'multiply'. Because both 'heavyMultiply' and 'multiply' are a 'mathOperation', they are compatible with the return type for 'op'. So here is a case where the effective behaviour of 'suitableMathOperation' has been varied without redefinition/extension etc.

Note the use of the ternary operation, these were developed in part to aid in making pure assignments easier to accomplish.

The final part of this example shows a dynamic function as a delegate called 'inlineFunction'. This captures a number of values; v1, v2 and v3. It then uses a single inline operation to calculate the result. It too, is a 'mathOperation'.

Hopefully, if you've read the previous examples; you will start to see how the EK9 syntax with ternary operations and use of <- aid in enabling the developer to quickly see assignments.

Summary

As you can see the power of abstract functions combined with higher order functions gives you a great range of possibilities to alter processing in a very refined and controlled manner.

The syntax and even the concept of using a function like it is a variable in one context, but then calling the function via the delegate (variable) may seem quite alien to some developers that have not seen this approach before. But give it time, and you will find the capability quite useful in many situations. Importantly this is type safe.

There is more detail on functions in relation to generics/templates and dynamic functions. But they are also shown being used in classes in the form of delegates.

Aside

As a point of interest you maybe thinking; what about writing:

  • suitableMathOperation(value1)(value2))

After all suitableMathOperation(value1) returns a function delegate and so that just needs a parameter (value2).
You can write this, but take care because someFunction(value1)(value2)(other)()(paramX) is going to be pretty difficult to understand when you employ many layers of higher functions! You are moving into Lisp territory (death by parentheses)!

Next Steps

Now for all the Object-Oriented programmers; Classes! Though defining concrete functions from an abstract function is Object-Oriented; and there are lots of other OO constructs to come besides classes.