Back

compose

Overview

The compose function creates a new operation by chaining multiple operations together in a functional programming style. Unlike pipe which applies operations in left-to-right order, compose applies them in right-to-left order (mathematical composition). This function provides an alternative way to combine operations when working with Serie objects.

Function Signatures


// Base case for compose - just returns the value
template <typename T> 
auto compose(T&& value);

// Apply operation and then recursively apply the rest
template <typename T, typename F, typename... Rest>
auto compose(T&& value, F&& operation, Rest&&... rest);

// Create a composition of operations (base case - single operation)
template <typename F> 
auto make_compose(F&& operation);

// Create a composition of multiple operations
template <typename F, typename... Rest>
auto make_compose(F&& first, Rest&&... rest);
        

Parameters

Parameter Type Description
value T&& The initial value to be transformed by the operations.
operation F&& (callable) A function or callable object that transforms the value.
rest... Rest&&... Additional operations to be applied in sequence.
first F&& (callable) The first operation to apply in the composition.

Return Value

Returns the result of applying all the operations to the input value in right-to-left order. The exact return type depends on the operations and the input value type.

For make_compose, returns a callable object that represents the composition of the provided operations. When this callable is invoked with a value, it will apply all operations in the composition to that value in right-to-left order.

Example Usage

Basic Composition Example

#include <dataframe/Serie.h>
#include <dataframe/core/compose.h>
#include <iostream>

int main() {
    // Create a Serie of numbers
    df::Serie<int> numbers{1, 2, 3, 4, 5};
    
    // Define operations
    auto add_one = [](int x) { return x + 1; };
    auto multiply_by_two = [](int x) { return x * 2; };
    auto square = [](int x) { return x * x; };
    
    // Use compose directly (operations applied right-to-left)
    // This is equivalent to: square(multiply_by_two(add_one(3)))
    // add_one: 3 -> 4
    // multiply_by_two: 4 -> 8
    // square: 8 -> 64
    auto result = df::compose(3, square, multiply_by_two, add_one);
    std::cout << "Result: " << result << std::endl;  // Output: 64
    
    return 0;
}
Composition with Serie Objects

#include <dataframe/Serie.h>
#include <dataframe/core/compose.h>
#include <dataframe/core/map.h>
#include <dataframe/core/filter.h>
#include <iostream>

int main() {
    // Create a Serie of numbers
    df::Serie<int> numbers{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    
    // Define Serie transformations
    auto double_values = [](const df::Serie<int>& s) {
        return s.map([](int x, size_t) { return x * 2; });
    };
    
    auto keep_even = [](const df::Serie<int>& s) {
        return df::filter([](int x, size_t) { return x % 2 == 0; }, s);
    };
    
    auto add_ten = [](const df::Serie<int>& s) {
        return s.map([](int x, size_t) { return x + 10; });
    };
    
    // Use compose with Serie transformations (right-to-left order)
    // First add_ten, then keep_even, then double_values
    auto result = df::compose(numbers, double_values, keep_even, add_ten);
    
    // Print result
    std::cout << "Result: ";
    result.forEach([](int x, size_t) {
        std::cout << x << " ";
    });
    std::cout << std::endl;
    // Output: Result: 24 28 32 36 40
    
    return 0;
}
Using make_compose

#include <dataframe/Serie.h>
#include <dataframe/core/compose.h>
#include <iostream>

int main() {
    // Create a Serie
    df::Serie<double> values{1.1, 2.2, 3.3, 4.4, 5.5};
    
    // Define operations for individual elements
    auto add_one = [](double x) { return x + 1.0; };
    auto multiply_by_two = [](double x) { return x * 2.0; };
    auto square = [](double x) { return x * x; };
    
    // Create a composed operation
    auto composed_op = df::make_compose(square, multiply_by_two, add_one);
    
    // Apply the composed operation to each element
    auto result = values.map([&composed_op](double x, size_t) {
        return composed_op(x);
    });
    
    // Print result
    std::cout << "Original values: ";
    values.forEach([](double x, size_t) { std::cout << x << " "; });
    std::cout << std::endl;
    
    std::cout << "After composed operation: ";
    result.forEach([](double x, size_t) { std::cout << x << " "; });
    std::cout << std::endl;
    // For each element x:
    // add_one: x -> x+1
    // multiply_by_two: (x+1) -> 2*(x+1)
    // square: 2*(x+1) -> (2*(x+1))²
    
    return 0;
}
Composition with Series Transformations

#include <dataframe/Serie.h>
#include <dataframe/core/compose.h>
#include <dataframe/math/random.h>
#include <dataframe/core/map.h>
#include <dataframe/core/filter.h>
#include <dataframe/core/sort.h>
#include <iostream>

int main() {
    // Generate a Serie of random values
    auto random_data = df::random_uniform<double>(100, 0.0, 100.0);
    
    // Define Serie transformations
    auto keep_above_50 = [](const df::Serie<double>& s) {
        return df::filter([](double x, size_t) { return x > 50.0; }, s);
    };
    
    auto sort_ascending = [](const df::Serie<double>& s) {
        return df::sort(s, df::SortOrder::ASCENDING);
    };
    
    auto take_top_10 = [](const df::Serie<double>& s) {
        size_t count = std::min(size_t(10), s.size());
        std::vector<double> top;
        for (size_t i = s.size() - count; i < s.size(); ++i) {
            top.push_back(s[i]);
        }
        return df::Serie<double>(top);
    };
    
    auto round_values = [](const df::Serie<double>& s) {
        return s.map([](double x, size_t) { return std::round(x * 10.0) / 10.0; });
    };
    
    // Create a pipeline using make_compose
    auto data_pipeline = df::make_compose(
        round_values,      // Round to one decimal place
        take_top_10,       // Take the top 10 values
        sort_ascending,    // Sort in ascending order
        keep_above_50      // Filter values > 50
    );
    
    // Run the pipeline
    auto result = data_pipeline(random_data);
    
    // Print result
    std::cout << "Top 10 values above 50 (rounded): ";
    result.forEach([](double x, size_t) {
        std::cout << x << " ";
    });
    std::cout << std::endl;
    
    return 0;
}

Comparison to pipe

The DataFrame library provides two ways to chain operations: compose and pipe. Both have their use cases:

Feature compose pipe
Operation Order Right-to-left (mathematical composition) Left-to-right (sequential processing)
Readability Better for mathematical transformations where composition order is natural Better for sequential data processing workflows
Syntax compose(value, op1, op2, op3) pipe(value, op1, op2, op3) or value | op1 | op2 | op3

Choose the approach that best matches your mental model of the data transformation process.

Implementation Notes

  • The compose function applies operations in reverse order compared to pipe (right-to-left).
  • Perfect forwarding is used to preserve value categories and avoid unnecessary copies.
  • The make_compose function creates a reusable composition that can be applied to multiple values.
  • Composition works with any compatible operations where the output of one can be used as input to the next.
  • Type deduction automatically determines the return type based on the operations and input value.

Related Functions