暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

现代C++函数式编程之Partial application and curry

purecpp 2020-06-21
624

Partial application和curry是函数式编程中的两个重要的概念,用现代C++来实现它们不难,让我们从概念开始一步步来实现它们。


Partial application

  • Partial application

    对于一个N(N>1)元函数,通过预先传入一个或多个参数来把多元函数转变为更少一些元的函数甚至一元函数,可以看做是一个逐步消元的过程。


看这个说明似乎还是不明白到底什么是Partial application, 看一个python的Partial application的例子。


代码1-1:

from functools import partial

# A normal function
def f(a, b, c, d):
return 1000*a + 100*b + 10*c + d

# A partial function that calls f with
# a as 3, b as 1 and c as 4.
g = partial(f, 3, 1, 4)

# Calling g()
print(g(5))

3145



这个例子中我们定义了一个4元函数f,接着通过Partial application将f转换成1元函数g,最后通过g求值。


在C++11中我们已经有一个现成的Partial application函数了,它就是std::bind,代码1-1如果用C++11 std::bind实现的话是很简单的。


代码1-2:

#include <functional>

int foo(int a, int b, int c, int d) {
return 1000 * a + 100 * b + 10 * c + d;
}

void test_partial() {
auto g = std::bind(foo, 3, 1, 4, std::placeholders::_1);
std::cout << g(5) << "\n"; //3145
}

int main(){
test_partial();

return 0;
}




我们还可以通过多次Partial application来逐步消元。


代码1-3:

void test_partial() {
auto g = std::bind(foo, 3, 1, std::placeholders::_1, std::placeholders::_2);
auto h = std::bind(g, 4, std::placeholders::_1);
std::cout << h(5) << "\n"; //3145
}



虽然通过std::bind很容易实现Partial application,但是相比其他语言的用法显得有点不是很地道,每次要拖一个std::placeholders的尾巴,我们可以自己写一个curry函数让Partial application用起来更地道。


C++17实现Partial application

实现partial application思路比较简单,用一个对象将函数和初始的一部分参数保存起来,后面partial的时候将参数追加起来,所有的partial完成之后再调用函数即可。


代码2-1:

namespace purecpp {
template <typename F, typename... Args>
class partial_t {
public:
constexpr partial_t(F&& f, Args&&... args) : f_(std::forward<F>(f)),
args_(std::forward_as_tuple(args...)) {
}

template <typename... RestArgs>
constexpr decltype(auto) operator()(RestArgs&&... rest_args) {
return std::apply(f_, std::tuple_cat(args_,
std::forward_as_tuple(std::forward<RestArgs>(rest_args)...)));
}

private:
F f_;
std::tuple<Args...> args_;
};

template <typename Fn, typename... Args>
constexpr decltype(auto) partial(Fn&& fn, Args&&... args) {
return partial_t<Fn, Args...>(std::forward<Fn>(fn), std::forward<Args>(args)...);
}
}

//test code
int test(int x, int y, int z) {
return x + y + z;
}

void test_partial() {
using namespace purecpp;
auto f = partial(test, 5, 3);
auto r = f(7);

auto r1 = partial(test)(5, 3, 7);
auto r2 = partial(test, 5)(3, 7);
auto r3 = partial(test, 5, 3)(7);
}



这个partial就比较酷了,随便你怎么用Partial application逐步消元。


接下来我们来看看函数式编程中的另外一个概念--curry。


curry

  • curry

    curry是一种将多元函数转换为多个一元函数的技术。


如函数x = f(a, b, c)经过curry后将变成:

h = g(a)
j = h(b)
x = i(c)




或者变成:

x = g(a)(b)(c)




接着来看看pthon toolz中的curry。


代码3-1:

from toolz import curry
def mul(x, y):
return x * y

mul = curry(mul)
f = mul(2)
print(f(10))
print(curry(mul)(2)(10))

# output 20



接下来,让我们用C++17来实现这样的curry。


C++17实现curry

C++17实现curry的思路和实现partial的思路有点类似,需要创建一个curry对象来保存函数和参数,每次curry的时候就把参数追加起来,直到获得了完整的参数时完成函数调用。


代码4-1:

namespace purecpp {
template<class T, typename... Args>
using can_invoke_t = decltype(std::declval<T>()(std::declval<Args>()...));

template<typename T, typename... Args>
using can_invoke = std::is_detected<can_invoke_t, T, Args...>;

template <typename F, typename...Arguments>
struct curry_t {
template <typename...Args>
constexpr decltype(auto) operator()(Args&&...a) const {
curry_t<F, Arguments..., Args...> cur = { f_,
std::tuple_cat(args_, std::make_tuple(std::forward<Args>(a)...)) };

if constexpr (!can_invoke<F, Arguments..., Args...>::value) {
return cur;
}
else {
return cur();
}
}

constexpr decltype(auto) operator () () const {
return std::apply(f_, args_);
}

F f_;
std::tuple<Arguments...> args_;
};

template<typename F>
constexpr curry_t<F> curry(F&& f) {
return { std::forward<F>(f) };
}
}

//test code
void test_curry() {
using namespace purecpp;

auto f = curry(test)(1);
auto g = f(2);
auto result = g(3);
auto result1 = curry(test)(1)(2)(3);
}




这个curry可以将一个N元函数转换为N个一元函数了。等等,这个代码其实在C++17中编译不过的。因为用到了一个C++20的traits: std::is_detected,C++17中并没有这个traits, 不过可以用C++17去实现一个is_detected,代码要多一一些,从这一个细节也可以管中窥豹,C++20更加给力,我们应该使用最新的标准o(∩_∩)o


代码4-2:

struct nonesuch {
nonesuch() = delete;
~nonesuch() = delete;
nonesuch(const nonesuch&) = delete;
void operator=(const nonesuch&) = delete;
};

template<class Default, class AlwaysVoid,
template<class...> class Op, class... Args>
struct detector {
using value_t = std::false_type;
using type = Default;
};

template<class Default, template<class...> class Op, class... Args>
struct detector<Default, std::void_t<Op<Args...>>, Op, Args...> {
using value_t = std::true_type;
using type = Op<Args...>;
};

template<template<class...> class Op, class... Args>
using is_detected = typename detector<nonesuch, void, Op, Args...>::value_t;



把代码4-1中的std::is_detected替换为4-2中的is_detected,C++17版本的curry就可以编译过了。


结束

现在,你可以愉快地用partial和curry去做一些好玩的事了。


用现代C++(C++17)实现函数式编程中的概念是一件有趣的事,高度泛化,简洁优雅,现代C++永恒的魅力!


敬请期待后续的现代C++函数式编程系列文章。欢迎订阅purecpp社区公众号:purecpp


文章转载自purecpp,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论