一、curry
函数柯里化(Currying),以及它的逆过程反柯里化(Uncurrying),是一种函数转换技术。在之前探讨模板元编程时,我们已经对它有过初步的分析。简单来说,柯里化就是把一个接收多个参数的函数,转换成一系列只接收单个参数的函数链。它与“部分应用”(Partial Application)有所不同:柯里化会生成嵌套的单参数函数链,而部分应用则是直接固定原函数的部分参数,得到一个参数更少的新函数。前者类似于实现链式调用以达到相同的最终结果;后者则可以类比于有默认参数的函数,用模板偏特化的概念来理解会更容易。我们先来看一个 Python 的例子:
from toolz import curry
def add(x, y):
return x + y
add = curry(add)
func = add(100)
print(func(100))
print(curry(add)(200)(200))
# 输出 200, 400
二、模板编程的实现
之前我们分析过部分应用的实现,现在我们可以对比一下,看看在 C++ 中如何具体实现柯里化。
1. 简单的lambda实现
这是一种最直观的方式,通过嵌套的 Lambda 表达式手动构造柯里化函数。
#include <iostream>
#include <functional>
auto mul = [](int x) {
return [x](int y) {
return [x, y](int z) {
return x * y * z;
};
};
};
int main() {
std::cout << mul(10)(10)(10) << std::endl;
return 0;
}
2. 基础的模板实现(含函数反柯里化)
这个方法更为通用,利用模板元编程的编译期计算和递归,可以处理任意参数数量的函数,并实现了反柯里化的功能。
#include <functional>
#include <iostream>
#include <type_traits>
class Curry {
public:
// curry api
template <typename Func> static auto curry(Func func){ return curried(func); }
// uncurry api
template <typename CurryFunc> static auto uncurry(CurryFunc curryFunc){
return [curryFunc](auto... args) { return recursiveCall(curryFunc, args...); };
}
private:
template <typename Func, typename... Args> static auto curried(Func func, Args... args){
if constexpr (std::is_invocable_v<Func, Args...>) {
return std::invoke(func, args...);
} else {
return [func, args...](auto pn) { return curried(func, args..., pn); };
}
}
template <typename Func, typename P, typename... Args> static auto recursiveCall(Func func, P p1, Args... args){
if constexpr (sizeof...(args) == 0) {
// last parameter
return func(p1);
} else {
// recursive call parameters
return recursiveCall(func(p1), args...);
}
}
};
int testMul(int a, int b, int c, int d){ return a * b * c * d; }
int main() {
// test curry
auto testCurry = Curry::curry(testMul);
auto firstFunc = testCurry(10);
auto secondFunc = firstFunc(20);
auto thirdFunc = secondFunc(5);
int result = thirdFunc(6);
std::cout << "testMul function result:" << result << std::endl;
// chain call
std::cout << "Chain call: " << testCurry(10)(10)(10)(10) << std::endl;
// uncurry
auto testUncurry = Curry::uncurry(testCurry);
std::cout << "testUncurry call result: " << testUncurry(10, 10, 10, 10) << std::endl;
return 0;
}
3. C++20 标准实现
这个版本充分利用了 C++20 的 if constexpr 和完美转发等特性,代码更简洁现代。
#include <functional>
#include <iostream>
int testAdd(int a, int b, int c, int d){ return a + b + c + d; }
template <typename F> auto curry(F &&f){
if constexpr (std::is_invocable_v<F>) {
return f();
} else {
return [f = std::forward<F>(f)]<typename T>(T &&t) mutable {
return curry([f = std::forward<decltype(f)>(f), t = std::forward<T>(t)]<typename... Ts>(Ts &&...ts) mutable -> std::invoke_result_t<F, T, Ts...> {
return std::invoke(f, std::forward<decltype(t)>(t), std::forward<decltype(t)>(ts)...);
});
};
}
}
int main() {
auto func = curry(testAdd);
std::cout << func(1)(2)(3)(4) << std::endl; // output: 10
}
柯里化的实现方式多种多样,上面只是提供了一些核心思路。例如,对于函数指针、左值引用、右值引用等不同类型参数的处理,都需要进一步封装和完善。这里的例子算是抛砖引玉。同样地,标准库中的 std::bind 系列工具也能在柯里化中发挥作用,具体可以参考关于“部分应用”实现的相关资料。
三、分析说明
函数(反)柯里化技术能够实现延迟计算或加载,有助于提高代码的模块化程度和复用性。通过灵活运用 Lambda 表达式和模板元编程,我们可以将多参数逐步收敛到单个参数。在某些场景下,这种表达方式能更好地体现代码逻辑,增强可读性。同时,它还支持多个函数的动态组合与链式调用,极大地丰富了高阶函数的应用场景。
当然,一项技术不能只谈优点。链式调用会增加函数调用栈的深度,带来一定的运行时开销。而且,过深的嵌套也会增加调试时定位问题的难度。此外,引入复杂的模板元编程也可能会显著增加编译时间以及最终生成的代码体积。
四、应用场景
函数柯里化在实际开发中有不少典型的应用场景:
- 函数组合:将多个简单函数动态组合起来,实现更复杂的功能(Function Composition),可以实现类似管道处理的效果。
- 延迟计算:用于处理特定的延迟计算或懒加载需求。
- 函数式编程:在类函数式编程语言或库的实现中,柯里化是基础特性之一。
这类函数式编程技术,在模板元编程领域应用尤为广泛。有兴趣的开发者可以将多种元编程技巧组合起来,构建出更强大、更合理的应用方案。
五、总结
本文不再给出额外的应用例程,感兴趣的读者可以尝试将 Python 中相关的函数式编程用法迁移到 C++ 程序中来。在应用层面上,这已经没有太大的障碍。通过对函数部分应用和柯里化的分析与实现,我们可以在 C++ 中模拟出函数式编程的诸多特性。这不仅有助于开发者更好地理解现代 C++ 标准和模板元编程技巧,也能有目的地将其应用到实际的工程实践中,提升代码质量。
欢迎在 云栈社区 交流更多关于 C++ 与函数式编程的实践经验。