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 wide spread 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.

It removes the need for static methods on classes. It enables 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. While you may not build a huge library of functions; 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 that 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 and should 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.

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 'divide', 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
    
    divide() is mathOperation as pure
      -> value as Float
      <- result as Float: value / 2.6
          
    suitableMathOperation() as pure
      <- op as mathOperation: multiply
        
  defines program
    ShowHighOrderFunction()
      stdout <- Stdout()
      
      mathOp <- suitableMathOperation()
      calculatedResult <- mathOp(9)
      stdout.println("The result is " + $ calculatedResult)
//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.

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. An example of this is shown below.

#!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() as pure
      -> value as Float
      <- op as mathOperation: value < 21 <- heavyMultiply else multiply
        
  defines program
    ShowAlternativeFunction()
      stdout <- Stdout()
      
      value <- 9
      mathOp <- suitableMathOperation(value)
      calculatedResult <- mathOp(value)
      stdout.println("The result is " + $ calculatedResult)
//EOF

The function returned by 'suitableMathOperation' now 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.

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

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