Join us today!

Advanced Interfaces...
 
Notifications
Clear all

Advanced Interfaces: Functional Interfaces - Part 2 (Context & Capturing)

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

In Part 1, we explored the concept of functional interfaces, where we used an interface with a single abstract method to pass logic around like a variable. We looked at a pure Aggregate  function that summed or multiplied values, or returned the largest value in an array.

Those examples were "stateless", meaning that the operations depended only on the arguments passed to them (a and b in (a, b) => <expression>). In the real world, however, logic rarely exists in a vacuum. Functions often need context: thresholds, configuration values, or system parameters that aren't part of the function arguments themselves.

In modern languages like C#, Java, Python, and C++, this is solved through a concept called closures or lambda capturing. This is when a function "captures" variables from its surrounding scope so it can use them later, even after the original scope has ended.
 
Here's an example in C++ that demonstrates capturing:
#include <iostream>

int main() 
{
    int scale = 10;
    
    // Capture by value (copy)
    auto scaleByValue = [scale](int a, int b) { return (a + b) * scale; };

    // Capture by reference (observes changes)
    auto scaleByRef = [&scale](int a, int b) { return (a + b) * scale; };

    scale = 20;

    std::cout << scaleByValue(1, 2) << std::endl; // 30 (uses original scale = 10)
    std::cout << scaleByRef(1, 2) << std::endl;   // 60 (uses updated scale = 20)

    return 0;
}
In this example:
- scaleByValue captures scale by value, storing a copy of 10 that won't change even when the original variable is modified.
- scaleByRef captures scale by reference, observing changes to the original variable.

This flexibility allows functions to carry context with them, making them far more powerful and adaptable.
 

The Challenge in Structured Text

 
Unfortunately, as discussed in Part 1, ST doesn't support closures or lambda capturing. We can't create inline functions that automatically capture surrounding variables. However, we can achieve similar functionality using two approaches:
  1. Function blocks with state - storing context as member variables
  2. Fluent interfaces - chainable methods that configure and return the function block

The fluent interface approach is particularly elegant, as it allows us to configure operations inline, similar to how we'd use closures in modern languages.

 

Approach 1: Direct Configuration

 

Let's start with the straight-forward approach of configuring function blocks before use:
{attribute 'no_explicit_call' := 'calling this function block directly is not allowed'}
FUNCTION_BLOCK FB_SumIfInRangeFunction IMPLEMENTS I_BiFunction
    VAR_INPUT
        nLowerThreshold,
        nUpperThreshold : INT;
    END_VAR

    METHOD Apply : INT
        VAR_INPUT
            nA, nB : INT;
        END_VAR

        // Only add nB if it's within the range
        IF nB >= nLowerThreshold AND nB <= nUpperThreshold THEN
            Apply := nA + nB;
        ELSE
            Apply := nA;
        END_IF
    END_METHOD
END_FUNCTION_BLOCK

 

Example Usage

 

PROGRAM P_Example2_Direct
VAR
    arNumbers       : ARRAY[0..4] OF INT := [15, 8, 23, 12, 19];
    fbSum           : FB_SumIfInRangeFunction;
    nFilteredSum    : INT;
END_VAR

// Configure the thresholds
fbSum.nLowerThreshold := 10;
fbSum.nUpperThreshold := 20;

// Apply operation
nFilteredSum := P_Operations.Aggregate(arNumbers, fbSum);
// Result: 15 + 12 + 19 = 46 (8 and 23 are excluded)

 

This works, but it's limiting and requires separate configuration steps. We can do better.

 

Approach 2: Fluent Interface (Method Chaining)

 

A fluent interface allows us to chain method calls together, configuring the function block inline. The key is that configuration methods return a reference to the function block itself (THIS^), enabling multiple calls to be chained together.

By defining separate interfaces for different configuration capabilities, we can create a flexible and type-safe fluent API that guides developers toward correct usage.
 
First, let's define interfaces that represent different configuration capabilities:
INTERFACE I_GreaterThanCondition
    METHOD AnyGreaterThan : I_BiFunction
        VAR_INPUT
            nThreshold : INT;
        END_VAR
    END_METHOD
END_INTERFACE
INTERFACE I_LessThanCondition
    METHOD AnyLessThan : I_BiFunction
        VAR_INPUT
            nThreshold : INT;
        END_VAR
    END_METHOD
END_INTERFACE
INTERFACE I_InRangeCondition
    METHOD AnyInBetween : I_BiFunction
        VAR_INPUT
            nLowerThreshold, nUpperThreshold : INT;
        END_VAR
    END_METHOD
END_INTERFACE
Now we can implement a function block that supports multiple configuration interfaces:
{attribute 'no_explicit_call' := 'calling this function block directly is not allowed'}
FUNCTION_BLOCK FB_SumFunction 
IMPLEMENTS I_BiFunction, I_GreaterThanCondition, I_LessThanCondition, I_InRangeCondition
    VAR_STAT CONSTANT
        _nMIN_INT : INT := -32768;
        _nMAX_INT : INT := 32767;
    END_VAR
    VAR
        _nLowerThreshold : INT := _nMIN_INT;
        _nUpperThreshold : INT := _nMAX_INT;;
    END_VAR
    
    // Sets the minimum threshold (inclusive)
    // Values must be GREATER THAN OR EQUAL to this threshold
    METHOD AnyGreaterThan : I_BiFunction
        VAR_INPUT
            nThreshold : INT;
        END_VAR
        AnyGreaterThan   := THIS^;

        _nLowerThreshold := nThreshold;
        _nUpperThreshold := _nMAX_INT;
    END_METHOD

    // Sets the maximum threshold (inclusive)
    // Values must be LESS THAN OR EQUAL to this threshold
    METHOD AnyLessThan : I_BiFunction
        VAR_INPUT
            nThreshold : INT;
        END_VAR
        AnyLessThan      := THIS^;

        _nLowerThreshold := _nMIN_INT;
        _nUpperThreshold := nThreshold;
    END_METHOD

    // Sets the lower and upper threshold (inclusive)
    // Values must be BETWEEN these thresholds
    METHOD AnyInBetween : I_BiFunction
        VAR_INPUT
            nLowerThreshold, nUpperThreshold : INT;
        END_VAR
        AnyInBetween     := THIS^;

        _nLowerThreshold := nLowerThreshold;
        _nUpperThreshold := nUpperThreshold;
    END_METHOD

    // Resets the configuration so that all values are allowed
    METHOD Everything : I_BiFunction
        VAR_INPUT
        END_VAR
        Everything := THIS^;

        _nLowerThreshold := _nMIN_INT;
        _nUpperThreshold := _nMAX_INT;
    END_METHOD

    METHOD Apply : INT
        VAR_INPUT
            nA, nB : INT;
        END_VAR
        // Only add nB if it's within the configured range
        IF nB >= _nLowerThreshold AND nB <= _nUpperThreshold THEN 
            Apply := nA + nB;
        ELSE
            Apply := nA;
        END_IF
    END_METHOD
END_FUNCTION_BLOCK

 

Example Usage

 

Now we can configure and use operations in clean, readable expressions:
ROGRAM P_Example2_Fluent
VAR
    arNumbers        : ARRAY[0..4] OF INT := [15, 8, 23, 12, 19];
    fbSum            : FB_SumFunction;
    nFilteredSum1,
    nFilteredSum2,
    nFilteredSum3    : INT;
END_VAR

// Configure and apply operations inline
nFilteredSum1 := P_Operations.Aggregate(arNumbers, fbSum.AnyLessThan(20));
// Result: 15 + 8 + 12 + 19 = 54 (23 is excluded)

nFilteredSum2 := P_Operations.Aggregate(arNumbers, fbSum.AnyGreaterThan(10));
// Result: 15 + 23 + 12 + 19 = 69 (8 is excluded)

nFilteredSum3 := P_Operations.Aggregate(arNumbers, fbSum.AnyInBetween(10, 20));
// Result: 15 + 12 + 19 = 46 (8 and 23 are excluded)

nSum          := P_Operations.Aggregate(arNumbers, fbSum.Everything());
// Result: 15 + 8 + 23 + 12 + 19 = 77 (all values are included)

 

tcxae example usage example 2

 

Benefits of the Fluent Approach

 

The fluent interface pattern provides several advantages:

  1. Readability: Configuration reads like natural language
  2. Type Safety: Each interface clearly defines what operations are available
  3. Inline Configuration: No need for separate setup steps
  4. Discoverability: Separate interfaces guide developers through available options
  5. Immutability Simulation: Each call appears to create a configured version without mutating the original (though technically it does modify the instance)
  6. Composability: Easy to build complex behaviors from simple building blocks
 

Real World Applications

 
Context-aware operations with fluent interfaces are invaluable across many automation scenarios:

Data Transformation & Mapping

// Transform raw ADC values to engineering units with calibration offset
fCalibratedTemps := P_DataTransform.Map(
    nRawSensorData,
    fbADCToTemp.WithOffset(nZeroOffset).WithGain(fCalibrationGain)
);

Filtering & Selection

// Select production batches exceeding quality threshold
nCount := P_DataFilter.Filter(
    arAllBatches,
    fbQualityCheck
        .AnyGreaterThan(nPremiumThreshold)
        .WithMargin(fSafetyMargin),
    arPremiumBatches
);

// Find active alarms above critical severity
nCount := P_DataFilter.Filter(
    arAlarmList,
    fbAlarmFilter
        .AnyGreaterThan(E_Severity.High)
        .IsActive()
        .NotAcknowledged(),
    arCriticalAlarms
);

Validation & Predicates

// Verify all safety sensors are within safe range
bSystemSafe := P_SafetyCheck.Every(
    arSafetySensors,
    fbSafetyValidator
        .AnyInBetween(nSafeMin, nSafeMax)
        .WithHysteresis(nHysteresisBand)
);

Sorting & Ranking

// Sort machines by efficiency, excluding standby units
P_EquipmentSort.Sort(
    arMachines,
    fbEfficiencyCompare.AnyGreaterThan(nMinEfficiency).ByDescending()
);

// Rank production runs by yield within quality band
P_BatchSort.Sort(
    arProductionRuns,
    fbYieldCompare.AnyInBetween(nMinQuality, nMaxQuality).ByYield()
);

Search & Analysis

// Find first sensor reading exceeding alarm threshold
ipFirstAlarm := P_DataSearch.Find(
    arSensorReadings,
    fbThresholdCheck.AnyGreaterThan(nAlarmThreshold).Confirmed(T#2s)
);

// Identify optimal batch by cost-to-quality ratio
ipBestBatch := P_BatchAnalysis.FindOptimal(
    arBatches,
    fbCostAnalysis.AnyGreaterThan(nMinYield).WithQualityWeight(0.6)
);

Statistical Aggregation

// Calculate weighted average excluding statistical outliers
fWeightedAvg := P_Statistics.Aggregate(
    arMeasurements,
    fbWeightedAvg
        .AnyInBetween(fMean - 3*fStdDev, fMean + 3*fStdDev)
        .WithWeights(arWeights)
);

// Compute RMS vibration in bearing frequency range
fBearingVibration := P_VibrationAnalysis.Aggregate(
    arFFTData,
    fbRMS
        .AnyInBetween(fBearingFreqLow, fBearingFreqHigh)
        .WithHanningWindow()
);
The key advantage across all these scenarios is that different functional interfaces (Map, Filter, Every, Some, None, Sort, Find, Aggregate) provide the appropriate abstraction for each use case, while fluent configuration makes the code expressive and maintainable.
 

Conclusion

 
While Structured Text doesn't support closures or lambda capturing, we can achieve remarkably similar functionality using function blocks with fluent interfaces. By:

  1. Storing context as member variables within function blocks
  2. Exposing configuration through dedicated interfaces for type safety and discoverability
  3. Returning references to the functional interface (THIS^) to enable inline configuration

We create expressive, readable code that rivals modern functional programming languages. The fluent interface pattern provides:

  • Inline configuration that mimics closure capturing
  • Type-safe composition through interface segregation
  • Natural, readable syntax that expresses intent clearly
  • Flexible, reusable components that adapt to different contexts

This approach bridges the gap between traditional PLC programming and modern functional paradigms, bringing greater modularity, maintainability, and expressiveness to industrial automation code without sacrificing the determinism and reliability that automation systems demand.

In Part 3, we'll explore how to extend this pattern further with function composition and higher-order functions, building even more powerful abstractions that allow us to combine simple operations into complex processing pipelines.

Reply
Share: