Join us today!

Advanced Interfaces...
 
Notifications
Clear all

Advanced Interfaces: Functional Interfaces - Part 1

1 Posts
1 Users
1 Reactions
1,196 Views
Posts: 8
Topic starter
(@fisothemes)
Eminent Member
Joined: 3 years ago
[#1494]

Lately, I’ve been thinking a lot about how to abstract complexity and improving the flexibility of code in industrial automation systems. With systems growing in size and complexity, it's important to find a balance between control and modularity. I’ve been exploring different approaches to achieve this, and I decided to start by talking about functional interfaces - a powerful tool that can help you break down large systems into smaller, reusable components.

Many modern programming languages, like Rust, Java, Python, or C#, have what is known as first-class functions. These are functions that can be passed around just like variables, meaning they can be assigned to variables, passed as arguments to other functions, or even returned as values from functions. This flexibility allows for more dynamic and reusable code. This is the fundamental concept behind lambdas, closures, and anonymous functions, which are compact ways to define behaviour on the fly without needing to declare a full function or method. By embedding logic directly where it’s needed, they allow developers to write cleaner, more concise, and more modular code.

Here’s an example of an anonymous function in C# that reduces a list of numbers:

using System;
using System.Linq;

int[] numbers = {2, 1, 3, 5, 4};

// Using an anonymous function to sum the array
int sum = numbers.Aggregate((a, b) => a + b);

// Using an anonymous function to get the product of the array
int product = numbers.Aggregate((a, b) => a * b);

// Output: 15
Console.WriteLine(sum);

// Output: 120
Console.WriteLine(product);

 

In this example, the Aggregate method uses a lambda expression to define how two numbers should be combined, either summing ((a, b) => a + b) or multiplying ((a, b) => a * b) the values. The power here is that the Aggregate method itself is decoupled from the specific operation. It doesn’t care whether you’re summing or multiplying numbers. This makes the logic highly reusable and flexible, allowing the function to process data in different ways based on the behaviour you pass in.

Unfortunately, Structured Text (ST) doesn’t have first-class functions (sort of...), but we can still introduce some of these principles through functional interfaces. Although ST doesn’t support lambdas, closures, or anonymous functions, functional interfaces can give us a way to mimic some of this functionality.

What is a Functional Interface?

A functional interface is an interface that contains exactly one abstract method. This single-method contract allows for a concise and focused interaction between components, making it ideal for situations where you need to define a single behaviour or action. While functional interfaces can have other members like default methods or constants (these aren't supported in ST), they are designed around the core idea of one abstract method, ensuring they remain lightweight and purpose-driven.

Here's an example of a functional interface in Java:

@FunctionalInterface
public interface BiFunction<T, U, R> {
     R apply(T t, U u); 
}

 

This BiFunction interface represents a function that accepts two arguments (T  and U ) and returns a result (R). The Apply method is the single abstract method that makes this a functional interface.

Here's an example of using this interface:

import java.util.function.BiFunction;

public class Main
{
    public static void main(String[] args) {
        // Define a BiFunction that adds two integers
        BiFunction<Integer, Integer, Integer> foo = (a, b) -> a + b;

        // Invoke the BiFunction to add two integers
        Integer result = foo.apply(2, 3);

        // Output: 5
        System.out.println(result);
    }
}

In this example, we create a BiFunction to add two integers. The lambda expression (a, b) => a + b defines the behaviour, and the apply method calls the function with the specified arguments.

Application in Industrial Automation

In automation systems, functional interfaces can be used to define well-scoped behaviours that can be applied across different components or modules. By encapsulating specific functionality within a clear, single-purpose interface, functional interfaces help make systems easier to maintain, extend, and understand. This modularity also enables greater flexibility when it comes to introducing new behaviours or modifying existing ones without affecting the overall system.

In this section, I’ll walk you through a simple example that demonstrate the use of functional interfaces in Codesys/TwinCAT's implementation of the IEC 61131-3 Structured Text language. I’ll be replicating the aggregate function discussed earlier to perform operations like summing and finding the maximum in an array of numbers.

 

Define the Functional Interface

 

First, let's define the I_BiFunction interface that will serve as a blueprint for any function that performs an operation on two integers and returns an integer result:

INTERFACE I_BiFunction
    METHOD Apply : INT 
    VAR_INPUT 
        nA, nB : INT;
    END_VAR 
END_INTERFACE

This interface contains a single method, Apply , which takes two integers as inputs (nA , nB ) and returns an integer result.

 

Implement a Method that Utilises the Functional Interface

 

Next, we’ll create the P_Operations  program that contains a method called Aggregate. This method will iterate over an array of integers and apply the operation defined by the I_BiFunction interface to aggregate the values in the supplied array.

{attribute 'no_explicit_call' := 'calling this program directly is not allowed'}
PROGRAM P_Operations
    METHOD Aggregate : INT
    VAR_IN_OUT CONSTANT
        {warning disable C0228}
        arNumbers : ARRAY [*] OF INT;
    END_VAR
    VAR_INPUT
        ipOperation : I_BiFunction;
    END_VAR
    VAR
        i : __XINT;
    END_VAR

    // Exit the function call if the operation is not supplied.
    IF ipOperation = 0 THEN RETURN; END_IF

    // Initialise the result with the first element of the array.
    Aggregate := arNumbers[LOWER_BOUND(arNumbers, 1)];

    // Loop through the array and apply the operation defined by the interface.
    FOR i := LOWER_BOUND(arNumbers, 1) + 1 TO UPPER_BOUND(arNumbers, 1) DO
        Aggregate := ipOperation.Apply(Aggregate, arNumbers[i]);
    END_FOR

    END_METHOD
END_PROGRAM

Explanation:

  • The ipOperations  input parameter is an instance of the I_BiFunction interface, which defines the operation to be applied (such as summing or multiplying).

  • The method initialises the result (Aggregate) with the first element of the array.

  • It then iterates through the rest of the array, applying the operation (via ipOperations.Apply(...)) to the running result and each element of the array.

 

Implementing the Operations

 

Now, let’s implement specific operations by creating function blocks that implement the I_BiFunction interface. We will use these to perform different actions on the array elements.

Product Operation

{attribute 'no_explicit_call' := 'calling this function block directly is not allowed'}
FUNCTION_BLOCK FB_ProductFunction IMPLEMENTS I_BiFunction
    METHOD Apply : INT
    VAR_INPUT
        nA, nB : INT;
    END_VAR

    // Return the product of two values
    Apply := nA * nB;

    END_METHOD
END_FUNCTION_BLOCK

Maximum Value Operation

{attribute 'no_explicit_call' := 'calling this function block directly is not allowed'}
FUNCTION_BLOCK FB_MaximumValueFunction IMPLEMENTS I_BiFunction
    METHOD Apply : INT
    VAR_INPUT
        nA, nB : INT;
    END_VAR

    // Return the maximum of two values
    Apply := MAX(nA, nB);

    END_METHOD
END_FUNCTION_BLOCK

 

Example Usage

 

Now that we've defined the functional interface and its implementations, we can use the P_Operations program to apply different operations to an array of numbers.

PROGRAM P_Example1
VAR
    nProduct, 
    nLargest    : INT;
    arNumbers   : ARRAY[0..4] OF INT := [2, 1, 3, 5, 4];
    fbProduct   : FB_ProductFunction;
    fbLargest   : FB_MaximumValueFunction;
END_VAR

nProduct := P_Operations.Aggregate(arNumbers, fbProduct);
nLargest := P_Operations.Aggregate(arNumbers, fbLargest);

 

tcxae example usage example 1

 

Real World Applications

 
Here are some practical examples of how functional interfaces shine in industrial automation:
 
Data Processing
// Transform sensor data through different conversion functions
fCelsius    := P_DataTransform.Map(nRawSensorData, fbADCToCelsius);
fFahrenheit := P_DataTransform.Map(nRawSensorData, fbADCToFahrenheit);
 
Batch Operations
// Apply different operations to production batches
bAllPassed := P_BatchProcessor.Every(arQualityChecks, fbMeetsStandard);
bAnyFailed := P_BatchProcessor.Some(arQualityChecks, fbBelowThreshold);
 
Validation and Filtering
// Filter arrays based on different validation criteria
// Filter(<source>, <predicate>, <destination>) -> <Count>
nCount := P_DataFilter.Filter(arSensorData, fbWithinTolerance, arValidReadings);
nCount := P_DataFilter.Filter(arSensorData, fbOutsideExpected, arOutliers);
nCount := P_DataFilter.Filter(arAlarms, fbHighSeverity, arCritical);
 
Validation and Filtering
// Sort machine data by different criteria
P_DataSort.Sort(arMachines, fbByEfficiency);
P_DataSort.Sort(arMachines, fbByUptime);
P_DataSort.Sort(arMachines, fbByProductionCount);

The key advantage here is that the processing logic (Aggregate, Map, Filter, etc.) remains unchanged while the behaviour is completely customisable through different function block implementations.

Conclusion

Functional interfaces bring a powerful abstraction mechanism to Structured Text programming. While ST lacks first-class functions, lambdas, and closures, functional interfaces allow us to:

  1. Decouple algorithms from operations: The Aggregate method doesn't know or care whether it's summing, multiplying, or finding maximums. It just applies the provided operation.

  2. Increase code reusability: Write the processing logic once, then reuse it with countless different behaviors by simply implementing new function blocks.

  3. Improve testability: Each operation is encapsulated in its own function block, making it easy to test in isolation.

  4. Enhance maintainability: Changes to specific operations don't affect the core processing logic, and vice versa.

  5. Create more expressive code: The intent becomes clear when you read P_Operations.Aggregate(arNumbers, fbProduct) versus a hardcoded multiplication loop.

While we've focused on simple, stateless operations in this part, functional interfaces become even more powerful when combined with context and state. In Part 2, we'll explore how to add context and capturing to our functional interfaces, allowing operations to carry configuration and state-mimicking the closure behaviour found in modern programming languages.

This approach bridges the gap between traditional PLC programming and modern software engineering practices, allowing for greater flexibility and maintainability in industrial automation systems without sacrificing the determinism and reliability that automation demands.

Reply
Share: