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
#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;
}
#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;
}
#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;
}
#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
composefunction applies operations in reverse order compared topipe(right-to-left). - Perfect forwarding is used to preserve value categories and avoid unnecessary copies.
- The
make_composefunction 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.