(转)现代C++函数式编程

本文转自:http://geek.csdn.net/news/detail/96636c++

 

现代C++函数式编程

 

做者简介: 祁宇,武汉烽火云创软件技术有限公司研发中心技术总监,《深刻应用C++11》做者,C++开源社区purecpp.org创始人,致力于C++11的应用、研究和推广。乐于研究和分享技术,爱好C++,爱好开源。git


导读: 本文做者从介绍函数式编程的概念入手,分析了函数式编程的表现形式和特性,最终经过现代C++的新特性以及一些模板云技巧实现了一个很是灵活的pipeline,展现了现代C++实现函数式编程的方法和技巧,同时也体现了现代C++的强大威力和无限可能。github

概述

函数式编程是一种编程范式,它有下面的一些特征:算法

  • 函数是一等公民,能够像数据同样传来传去。
  • 高阶函数
  • 递归
  • pipeline
  • 惰性求值
  • 柯里化
  • 偏应用函数

C++98/03中的函数对象,和C++11中的Lambda表达式、std::function和std::bind让C++的函数式编程变得容易。咱们能够利用C++11/14里的新特性来实现高阶函数、链式调用、惰性求值和柯理化等函数式编程特性。本文将经过一些典型示例来说解如何使用现代C++来实现函数式编程。shell

高阶函数和pipeline的表现形式

高阶函数就是参数为函数或返回值为函数的函数,经典的高阶函数就是map、filter、fold和compose函数,好比Scala中高阶函数:编程

  • map数组

    numbers.map((i: Int) => i * 2)

    对列表中的每一个元素应用一个函数,返回应用后的元素所组成的列表。ruby

  • filterbash

    numbers.filter((i: Int) => i % 2 == 0)

    移除任何对传入函数计算结果为false的元素。markdown

  • fold

    numbers.fold(0) { (z, i) => a + i }

    将一个初始值和一个二元函数的结果累加起来。

  • compose

    val fComposeG = f _ compose g _ fComposeG("x")

    组合其它函数造成一个新函数f(g(x))。

上面的例子中,有的是参数为函数,有的是参数和返回值都是函数。高阶函数不只语义上更加抽象泛化,还能实现“函数是一等公民”,将函数像data同样传来传去或者组合,很是灵活。其中,compose还能够实现惰性求值,compose的返回结果是一个函数,咱们能够保存起来,在后面须要的时候调用。

pipeline把一组函数放到一个数组或是列表中,而后把数据传给这个列表。数据就像一个链条同样顺序地被各个函数所操做,最终获得咱们想要的结果。它的设计哲学就是让每一个功能就作一件事,并把这件事作到极致,软件或程序的拼装会变得更为简单和直观。 
Scala中的链式调用是这样的:

s(x) = (1 to x) |> filter (x => x % 2 == 0) |> map (x => x * 2)

用法和Unix Shell的管道操做比较像,|前面的数据或函数做为|后面函数的输入,顺序执行直到最后一个函数。

这种管道方式的函数调用让逻辑看起来更加清晰明了,也很是灵活,容许你将多个高阶函数自由组合成一个链条,同时还能够保存起来实现惰性求值。现代C++实现这种pipeline也是比较容易的,下面来说解如何充分借助C++11/14的新特性来实现这些高阶函数和pipeline。

实现pipeline的关键技术

根据前面介绍的pipeline表现形式,能够把pipeline分解为几部分:高阶函数,惰性求值,运算符|、柯里化和pipeline,把这几部分实现以后就能够组成一个完整的pipeline了。下面来分别介绍它们的实现技术。

高阶函数

函数式编程的核心就是函数,它是一等公民,最灵活的函数就是高阶函数,现代C++的算法中已经有不少高阶函数了,好比for_each, transform:

std::vector<int> vec{1,2,3,4,5,6,7,8,9} //接受一个打印的Lambda表达式 std::for_each(vec.begin(), vec.end(), [](auto i){ std::cout<<i<<std::endl; }); //接受一个转换的Lambda表达式 transform(vec.begin(), vec.end(), vec.begin(), [](int i){ return i*i; });

这些高阶函数不只能够接受Lambda表达式,还能接受std::function、函数对象、普通的全局函数,很灵活。须要注意的是,普通的全局函数在pipeline时存在局限性,由于它不像函数对象同样能够保存起来延迟调用,因此咱们须要一个方法将普通的函数转换为函数对象。std::bind也能够将函数转化为函数对象,可是bind不够通用,使用的时候它只能绑定有限的参数,若是函数自己就是可变参数的就没法bind了,因此,这个函数对象必须是泛化的,相似于这样:

class universal_functor 
{
public: 
    template <typename... Args> auto operator()(Args&&... args) const ->decltype(globle_func(std::forward<Args>(args)...)) { return globle_func(std::forward<Args>(args)...); } };

上面的函数对象内部包装了一个普通函数的调用,当函数调用的时候实际上会调用普通函数globle_func,可是这个代码不通用,它没法用于其余的函数。为了让这个转换变得通用,咱们能够借助一个宏来实现function到functor的转换。

#define define_functor_type(func_name) class tfn_##func_name {\ public: template <typename... Args> auto operator()(Args&&... args) const ->decltype(func_name(std::forward<Args>(args)...))\ { return func_name(std::forward<Args>(args)...); } } //test code int add(int a, int b) { return a + b; } int add_one(int a) { return 1 + a; } define_functor_type(add); define_functor_type(add_one); int main() { tnf_add add_functor; add_functor(1, 2); //result is 3 tfn_add_one add_one_functor; add_one_functor(1); //result is 2 return 0; }

咱们先定义了一个宏,这个宏根据参数来生成一个可变参数的函数对象,这个函数对象的类型名为tfn_加普通函数的函数名,之因此要加一个前缀tfn_,是为了不类型名和函数名重名。define_functor_type宏只是定义了一个函数对象的类型,用起来略感不便,还能够再简化一下,让使用更方便。咱们能够再定义一个宏来生成转换后的函数对象:

#define make_globle_functor(NAME, F) const auto NAME = define_functor_type(F); //test code make_globle_functor(fn_add, add); make_globle_functor(fn_add_one, add_one); int main() { fn_add(1, 2); fn_add_one(1); return 0; }

make_globle_functor生成了一个能够直接使用的全局函数对象,使用起来更方便了。用这个方法就能够将普通函数转成pipeline中的函数对象了。接下来咱们来探讨实现惰性求值的关键技术。

惰性求值

惰性求值是将求值运算延迟到须要值时候进行,一般的作法是将函数或函数的参数保存起来,在须要的时候才调用函数或者将保存的参数传入函数实现调用。现代C++里已经提供能够保存起来的函数对象和lambda表达式,所以须要解决的问题是如何将参数保存起来,而后在须要的时候传给函数实现调用。咱们能够借助std::tuple、type_traits和可变模版参数来实现目标。

template<typename F, size_t... I, typename ... Args> inline auto tuple_apply_impl(const F& f, const std::index_sequence<I...>&, const std::tuple<Args...>& tp) { return f(std::get<I>(tp)...); } template<typename F, typename ... Args> inline auto tuple_apply(const F& f, const std::tuple<Args...>& tp) -> decltype(f(std::declval<Args>()...)) { return tuple_apply_impl(f, std::make_index_sequence<sizeof... (Args)>{}, tp); } int main() { //test code auto f = [](int x, int y, int z) { return x + y - z; }; //将函数调用须要的参数保存到tuple中 auto params = make_tuple(1, 2, 3); //将保存的参数传给函数f,实现函数调用 auto result = tuple_apply(f, params); //result is 0 return 0; }

上面的测试代码中,咱们先把参数保存到一个tuple中,而后在须要的时候将参数和函数f传入tuple_apply,最终实现了f函数的调用。tuple_apply实现了一个“魔法”将tuple变成了函数的参数,来看看这个“魔法”具体是怎么实现的。

tuple_apply_impl实现的关键是在于可变模版参数的展开,可变模版参数的展开又借助了std::index_sequence

运算符operator|

pipeline的一个主要表现形式是经过运算符|来将data和函数分隔开或者将函数和函数组成一个链条,好比像下面的unix shell命令:

ps auwwx | awk '{print $2}' | sort -n | xargs echo

C++实现相似的调用能够经过重载运算符来实现,下面是data和函数经过|链接的实现代码:

template<typename T, class F> auto operator|(T&& param, const F& f) -> decltype(f(std::forward<T>(param))) { return f(std::forward<T>(param)); } //test code auto add_one = [](auto a) { return 1 + a; }; auto result = 2 | add_one; //result is 3

除了data和函数经过|链接以外,还须要实现函数和函数经过|链接,咱们经过可变参数来实现:

template<typename... FNs, typename F> inline auto operator|(fn_chain<FNs...> && chain, F&& f) { return chain.add(std::forward<F>(f)); } //test code auto chain = fn_chain<>() | (filter >> [](auto i) { return i % 2 == 0; }) | ucount | uprint;

其中fn_chain是一个能够接受任意个函数的函数对象,它的实现将在后面介绍。经过|运算符重载咱们能够实现相似于unix shell的pipeline表现形式。

柯里化

函数式编程中比较灵活的一个地方就是柯里化(currying),柯里化是把多个参数的函数变换成单参数的函数,并返回一个新函数,这个新函数处理剩下的参数。以Scala的柯里化为例:

  • 未柯里化的函数
def add(x:Int, y:Int) = x + y add(1, 2)  // 3 add(7, 3)  // 10
  • 柯里化以后
def add(x:Int) = (y:Int) => x + y add(1)(2)  // 3 add(7)(3)  // 10

currying以后add(1)(2)等价于add(1,2),这种currying默认是从左到右的,若是但愿从右到左呢,然而大部分编程语言没有实现更灵活的curring。C++11里面的std::bind能够实现currying,但要实现向左或向右灵活的currying比较困难,能够借助tuple和前面介绍的tuple_apply来实现一个更灵活的currying函数对象。

template<typename F, typename Before = std::tuple<>, typename After = std::tuple<>> class curry_functor { private: F f_; ///< main functor Before before_; ///< curryed arguments After after_; ///< curryed arguments public: curry_functor(F && f) : f_(std::forward<F>(f)), before_(std::tuple<>()), after_(std::tuple<>()) {} curry_functor(const F & f, const Before & before, const After & after) : f_(f), before_(before), after_(after) {} template <typename... Args> auto operator()(Args... args) const -> decltype(tuple_apply(f_, std::tuple_cat(before_, make_tuple(args...), after_))) { // execute via tuple return tuple_apply(f_, std::tuple_cat(before_, make_tuple(std::forward<Args>(args)...), after_)); } // currying from left to right template <typename T> auto curry_before(T && param) const { using RealBefore = decltype(std::tuple_cat(before_, std::make_tuple(param))); return curry_functor<F, RealBefore, After>(f_, std::tuple_cat(before_, std::make_tuple(std::forward<T>(param))), after_); } // currying from righ to left template <typename T> auto curry_after(T && param) const { using RealAfter = decltype(std::tuple_cat(after_, std::make_tuple(param))); return curry_functor<F, Before, RealAfter>(f_, before_, std::tuple_cat(after_, std::make_tuple(std::forward<T>(param)))); } }; template <typename F> auto fn_to_curry_functor(F && f) { return curry_functor<F>(std::forward<F>(f)); } //test code void test_count() { auto f = [](int x, int y, int z) { return x + y - z; }; auto fn = fn_to_curry_functor(f); auto result = fn.curry_before(1)(2, 3); //0 result = fn.curry_before(1).curry_before(2)(3); //0 result = fn.curry_before(1).curry_before(2).curry_before(3)(); //0 result = fn.curry_before(1).curry_after(2).curry_before(3)(); //2 result = fn.curry_after(1).curry_after(2).curry_before(2)(); //1 } 

从测试代码中能够看到这个currying函数对象,既能够从左边currying又能够从右边currying,很是灵活。不过使用上还不太方便,没有fn(1)(2)(3)这样方便,咱们能够经过运算符重载来简化书写,因为C++标准中不容许重载全局的operater()符,而且operater()符没法区分究竟是从左边仍是从右边currying,因此咱们选择重载<<和>>操做符来分别表示从左至右currying和从右至左currying。

// currying from left to right template<typename UF, typename Arg> auto operator<<(const UF & f, Arg && arg) -> decltype(f.template curry_before<Arg>(std::forward<Arg>(arg))) { return f.template curry_before<Arg>(std::forward<Arg>(arg)); } // currying from right to left template<typename UF, typename Arg> auto operator>>(const UF & f, Arg && arg) -> decltype(f.template curry_after<Arg>(std::forward<Arg>(arg))) { return f.template curry_after<Arg>(std::forward<Arg>(arg)); }

有了这两个重载运算符,测试代码能够写得更简洁了。

void test_currying()
{
    auto f = [](int x, int y, int z) { return x + y - z; }; auto fn = fn_to_curry_functor(f); auto result = (fn << 1)(2, 3); //0 result = (fn << 1 << 2)(3); //0 result = (fn << 1 << 2 << 3)(); //0 result = (fn << 1 >> 2 << 3)(); //2 result = (fn >> 1 >> 2 << 3)(); //1 }

curry_functor利用了tuple的特性,内部有两个空的tuple,一个用来保存left currying的参数,一个用来保存right currying的参数,不断地currying时,经过tuple_cat把新currying的参数保存到tuple中,最后调用的时候将tuple成员和参数组成一个最终的tuple,而后经过tuple_apply实现调用。有了前面这些基础设施以后咱们实现pipeline也是水到渠成。

pipeline

经过运算符|重载,咱们能够实现一个简单的pipeline:

template<typename T, class F> auto operator|(T&& param, const F& f) -> decltype(f(std::forward<T>(param))) { return f(std::forward<T>(param)); } //test code void test_pipe() { auto f1 = [](int x) { return x + 3; }; auto f2 = [](int x) { return x * 2; }; auto f3 = [](int x) { return (double)x / 2.0; }; auto f4 = [](double x) { std::stringstream ss; ss << x; return ss.str(); }; auto f5 = [](string s) { return "Result: " + s; }; auto result = 2|f1|f2|f3|f4|f5; //Result: 5 }

这个简单的pipeline虽然能够实现管道方式的链式计算,可是它只是将data和函数经过|链接起来了,尚未实现函数和函数的链接,而且是当即计算的,没有实现延迟计算。所以咱们还须要实现经过|链接函数,从而实现灵活的pipeline。咱们能够经过一个function chain来接受任意个函数并组成一个函数链。利用可变模版参数、tuple和type_traits就能够实现了。

template <typename... FNs> class fn_chain { private: const std::tuple<FNs...> functions_; const static size_t TUPLE_SIZE = sizeof...(FNs); template<typename Arg, std::size_t I> auto call_impl(Arg&& arg, const std::index_sequence<I>&) const ->decltype(std::get<I>(functions_)(std::forward<Arg>(arg))) { return std::get<I>(functions_)(std::forward<Arg>(arg)); } template<typename Arg, std::size_t I, std::size_t... Is> auto call_impl(Arg&& arg, const std::index_sequence<I, Is...>&) const ->decltype(call_impl(std::get<I>(functions_)(std::forward<Arg>(arg)), std::index_sequence<Is...>{})) { return call_impl(std::get<I>(functions_)(std::forward<Arg>(arg)), std::index_sequence<Is...>{}); } template<typename Arg> auto call(Arg&& arg) const-> decltype(call_impl(std::forward<Arg>(arg), std::make_index_sequence<sizeof...(FNs)>{})) { return call_impl(std::forward<Arg>(arg), std::make_index_sequence<sizeof...(FNs)>{}); } public: fn_chain() : functions_(std::tuple<>()) {} fn_chain(std::tuple<FNs...> functions) : functions_(functions) {} // add function into chain template< typename F > inline auto add(const F& f) const { return fn_chain<FNs..., F>(std::tuple_cat(functions_, std::make_tuple(f))); } // call whole functional chain template <typename Arg> inline auto operator()(Arg&& arg) const -> decltype(call(std::forward<Arg>(arg))) { return call(std::forward<Arg>(arg)); } }; // pipe function into functional chain via | operator template<typename... FNs, typename F> inline auto operator|(fn_chain<FNs...> && chain, F&& f) { return chain.add(std::forward<F>(f)); } #define tfn_chain fn_chain<>() //test code void test_pipe() { auto f1 = [](int x) { return x + 3; }; auto f2 = [](int x) { return x * 2; }; auto f3 = [](int x) { return (double)x / 2.0; }; auto f4 = [](double x) { std::stringstream ss; ss << x; return ss.str(); }; auto f5 = [](string s) { return "Result: " + s; }; auto compose_fn = tfn_chain|f1|f2|f3|f4|f5; //compose a chain compose_fn(2); // Result: 5 }

测试代码中用一个fn_chain和运算符|将全部的函数组合成了一个函数链,在须要的时候调用,从而实现了惰性求值。

fn_chain的实现思路是这样的:内部有一个std::tuple

template<typename Arg, std::size_t I> auto call_impl(Arg&& arg, const std::index_sequence<I>&) const ->decltype(std::get<I>(functions_)(std::forward<Arg>(arg))) { return std::get<I>(functions_)(std::forward<Arg>(arg)); } template<typename Arg, std::size_t I, std::size_t... Is> auto call_impl(Arg&& arg, const std::index_sequence<I, Is...>&) const ->decltype(call_impl(std::get<I>(functions_)(std::forward<Arg>(arg)), std::index_sequence<Is...>{})) { return call_impl(std::get<I>(functions_)(std::forward<Arg>(arg)), std::index_sequence<Is...>{}); }

在调用call_impl的过程当中,将std::index_sequence不断展开,先从tuple中获取第I个function,而后调用得到第I个function的执行结果,将这个执行结果做为下次调用的参数,不断地递归调用,直到最后一个函数完成调用为止,返回最终的链式调用的结果。

至此咱们实现具有惰性求值、高阶函数和currying特性的完整的pipeline,有了这个pipeline,咱们能够实现经典的流式计算和AOP,接下来咱们来看看如何利用pipeline来实现流式的mapreduce和灵活的AOP。

实现一个pipeline形式的mapreduce和AOP

前面的pipeline已经能够实现链式调用了,要实现pipeline形式的mapreduce关键就是实现map、filter和reduce等高阶函数。下面是它们的具体实现:

// MAP template <typename T, typename... TArgs, template <typename...>class C, typename F> auto fn_map(const C<T, TArgs...>& container, const F& f) -> C<decltype(f(std::declval<T>()))> { using resultType = decltype(f(std::declval<T>())); C<resultType> result; for (const auto& item : container) result.push_back(f(item)); return result; } // REDUCE (FOLD) template <typename TResult, typename T, typename... TArgs, template <typename...>class C, typename F> TResult fn_reduce(const C<T, TArgs...>& container, const TResult& startValue, const F& f) { TResult result = startValue; for (const auto& item : container) result = f(result, item); return result; } // FILTER template <typename T, typename... TArgs, template <typename...>class C, typename F> C<T, TArgs...> fn_filter(const C<T, TArgs...>& container, const F& f) { C<T, TArgs...> result; for (const auto& item : container) if (f(item)) result.push_back(item); return result; }

这些高阶函数还须要转换成支持currying的functor,前面咱们已经定义了一个普通的函数对象转换为柯里化的函数对象的方法:

template <typename F> auto fn_to_curry_functor(F && f) { return curry_functor<F>(std::forward<F>(f)); }

经过下面这个宏让currying functor用起来更简洁:

#define make_globle_curry_functor(NAME, F) define_functor_type(F); const auto NAME = fn_to_curry_functor(tfn_##F()); make_globle_curry_functor(map, fn_map); make_globle_curry_functor(reduce, fn_reduce); make_globle_curry_functor(filter, fn_filter);

咱们定义了map、reduce和filter支持柯里化的三个全局函数对象,接下来咱们就能够把它们组成一个pipeline了。

void test_pipe() { //test map reduce vector<string> slist = { "one", "two", "three" }; slist | (map >> [](auto s) { return s.size(); }) | (reduce >> 0 >> [](auto a, auto b) { return a + b; }) | [](auto a) { cout << a << endl; }; //test chain, lazy eval auto chain = tfn_chain | (map >> [](auto s) { return s.size(); }) | (reduce >> 0 >> [](auto a, auto b) { return a + b; }) | ([](int a) { std::cout << a << std::endl; }); slist | chain; }

上面的例子实现了pipeline的mapreduce,这个pipeline支持currying还能够任意组合,很是方便和灵活。

有了这个pipeline,实现灵活的AOP也是很容易的:

struct person { person get_person_by_id(int id) { this->id = id; return *this; } int id; std::string name; }; void test_aop() { const person& p = { 20, "tom" }; auto func = std::bind(&person::get_person_by_id, &p, std::placeholders::_1); auto aspect = tfn_chain | ([](int id) { cout << "before"; return id + 1; }) | func | ([](const person& p) { cout << "after" << endl; }); aspect(1); }

上面的测试例子中,核心逻辑是func函数,咱们能够在func以前或以后插入切面逻辑,切面逻辑能够不断地加到链条前面或者后面,实现很巧妙,使用很常灵活。

总结

本文经过介绍函数式编程的概念入手,分析了函数式编程的表现形式和特性,最终经过现代C++的新特性和一些模版元技巧实现了一个很是灵活的pipeline,展现了现代C++实现函数式编程的方法和技巧,同时也提现了现代C++的强大威力和无限的可能性。文中完整的代码能够从个人GitHub(https://github.com/qicosmos/cosmos/blob/master/modern_functor.hpp)上查看。

本文的代码和思路参考和借鉴了http://vitiy.info/templates-as-first-class-citizens-in-cpp11/,在此表示感谢。


2016 年 9 月 23-24 日,由 CSDN 和创新工场联合主办的“MDCC 2016 移动开发者大会• 中国”(Mobile Developer Conference China)将在北京• 国家会议中心召开,来自 iOS、Android、跨平台开发、产品设计、VR 开发、移动直播、人工智能、物联网、硬件开发、信息无障碍10个领域的技术专家将分享他们在各自行业的真知灼见。

从 8 月 8 日起至 9 月 4 日,MDCC 大会门票处于 6.8 折优惠票价阶段,五人以上团购更有特惠,限量供应(票务详情连接6.8折优惠,欲购从速!

评论
 

已有2条评论

  • 最新
yshuise  2小时前

我有不少年没搞c++了,因此也记不得那么多了!

0
yshuise  2小时前

函数式编程可不是这个。 
而是用元编程模拟if while for等语句

0
 
 
 
发布到 主题  发布 评论
相关文章
相关标签/搜索