导读:本文作者从介绍函数式编程的概念入手,分析了函数式编程的表现形式和特性,最终通过现代C++的新特性以及一些模板云技巧实现了一个非常灵活的pipeline,展示了现代C++实现函数式编程的方法和技巧,同时也体现了现代C++的强大威力和无限可能。
函数式编程是一种编程范式,它有下面的一些特征:
C++98/03中的函数对象,和C++11中的Lambda表达式、std::function和std::bind让C++的函数式编程变得容易。我们可以利用C++11/14里的新特性来实现高阶函数、链式调用、惰性求值和柯理化等函数式编程特性。本文将通过一些典型示例来讲解如何使用现代C++来实现函数式编程。
高阶函数就是参数为函数或返回值为函数的函数,经典的高阶函数就是map、filter、fold和compose函数,比如Scala中高阶函数:
上面的例子中,有的是参数为函数,有的是参数和返回值都是函数。高阶函数不仅语义上更加抽象泛化,还能实现“函数是一等公民”,将函数像data一样传来传去或者组合,非常灵活。其中,compose还可以实现惰性求值,compose的返回结果是一个函数,我们可以保存起来,在后面需要的时候调用。
pipeline把一组函数放到一个数组或是列表中,然后把数据传给这个列表。数据就像一个链条一样顺序地被各个函数所操作,最终得到我们想要的结果。它的设计哲学就是让每个功能就做一件事,并把这件事做到极致,软件或程序的拼装会变得更为简单和直观。Scala中的链式调用是这样的:
s(x)=(1tox)|>filter(x=>x%2==0)|>map(x=>x*2)用法和UnixShell的管道操作比较像,|前面的数据或函数作为|后面函数的输入,顺序执行直到最后一个函数。
这种管道方式的函数调用让逻辑看起来更加清晰明了,也非常灵活,允许你将多个高阶函数自由组合成一个链条,同时还可以保存起来实现惰性求值。现代C++实现这种pipeline也是比较容易的,下面来讲解如何充分借助C++11/14的新特性来实现这些高阶函数和pipeline。
根据前面介绍的pipeline表现形式,可以把pipeline分解为几部分:高阶函数,惰性求值,运算符|、柯里化和pipeline,把这几部分实现之后就可以组成一个完整的pipeline了。下面来分别介绍它们的实现技术。
函数式编程的核心就是函数,它是一等公民,最灵活的函数就是高阶函数,现代C++的算法中已经有很多高阶函数了,比如for_each,transform:
std::vectorvec{1,2,3,4,5,6,7,8,9}//接受一个打印的Lambda表达式std::for_each(vec.begin(),vec.end(),[](autoi){std::cout<classuniversal_functor{public:templateautooperator()(Args&&...args)const->decltype(globle_func(std::forward(args)...)){returngloble_func(std::forward(args)...);}};上面的函数对象内部包装了一个普通函数的调用,当函数调用的时候实际上会调用普通函数globle_func,但是这个代码不通用,它无法用于其他的函数。为了让这个转换变得通用,我们可以借助一个宏来实现function到functor的转换。
#definedefine_functor_type(func_name)classtfn_##func_name{\public:templateautooperator()(Args&&...args)const->decltype(func_name(std::forward(args)...))\{returnfunc_name(std::forward(args)...);}}//testcodeintadd(inta,intb){returna+b;}intadd_one(inta){return1+a;}define_functor_type(add);define_functor_type(add_one);intmain(){tnf_addadd_functor;add_functor(1,2);//resultis3tfn_add_oneadd_one_functor;add_one_functor(1);//resultis2return0;}我们先定义了一个宏,这个宏根据参数来生成一个可变参数的函数对象,这个函数对象的类型名为tfn_加普通函数的函数名,之所以要加一个前缀tfn_,是为了避免类型名和函数名重名。define_functor_type宏只是定义了一个函数对象的类型,用起来略感不便,还可以再简化一下,让使用更方便。我们可以再定义一个宏来生成转换后的函数对象:
#definemake_globle_functor(NAME,F)constautoNAME=define_functor_type(F);//testcodemake_globle_functor(fn_add,add);make_globle_functor(fn_add_one,add_one);intmain(){fn_add(1,2);fn_add_one(1);return0;}make_globle_functor生成了一个可以直接使用的全局函数对象,使用起来更方便了。用这个方法就可以将普通函数转成pipeline中的函数对象了。接下来我们来探讨实现惰性求值的关键技术。
惰性求值是将求值运算延迟到需要值时候进行,通常的做法是将函数或函数的参数保存起来,在需要的时候才调用函数或者将保存的参数传入函数实现调用。现代C++里已经提供可以保存起来的函数对象和lambda表达式,因此需要解决的问题是如何将参数保存起来,然后在需要的时候传给函数实现调用。我们可以借助std::tuple、type_traits和可变模版参数来实现目标。
templateinlineautotuple_apply_impl(constF&f,conststd::index_sequence&,conststd::tuple&tp){returnf(std::get(tp)...);}templateinlineautotuple_apply(constF&f,conststd::tuple&tp)->decltype(f(std::declval()...)){returntuple_apply_impl(f,std::make_index_sequence{},tp);}intmain(){//testcodeautof=[](intx,inty,intz){returnx+y-z;};//将函数调用需要的参数保存到tuple中autoparams=make_tuple(1,2,3);//将保存的参数传给函数f,实现函数调用autoresult=tuple_apply(f,params);//resultis0return0;}上面的测试代码中,我们先把参数保存到一个tuple中,然后在需要的时候将参数和函数f传入tuple_apply,最终实现了f函数的调用。tuple_apply实现了一个“魔法”将tuple变成了函数的参数,来看看这个“魔法”具体是怎么实现的。
tuple_apply_impl实现的关键是在于可变模版参数的展开,可变模版参数的展开又借助了std::index_sequence
pipeline的一个主要表现形式是通过运算符|来将data和函数分隔开或者将函数和函数组成一个链条,比如像下面的unixshell命令:
psauwwx|awk'{print$2}'|sort-n|xargsechoC++实现类似的调用可以通过重载运算符来实现,下面是data和函数通过|连接的实现代码:
templateautooperator|(T&¶m,constF&f)->decltype(f(std::forward(param))){returnf(std::forward(param));}//testcodeautoadd_one=[](autoa){return1+a;};autoresult=2|add_one;//resultis3除了data和函数通过|连接之外,还需要实现函数和函数通过|连接,我们通过可变参数来实现:
templateinlineautooperator|(fn_chain&&chain,F&&f){returnchain.add(std::forward(f));}//testcodeautochain=fn_chain<>()|(filter>>[](autoi){returni%2==0;})|ucount|uprint;其中fn_chain是一个可以接受任意个函数的函数对象,它的实现将在后面介绍。通过|运算符重载我们可以实现类似于unixshell的pipeline表现形式。
函数式编程中比较灵活的一个地方就是柯里化(currying),柯里化是把多个参数的函数变换成单参数的函数,并返回一个新函数,这个新函数处理剩下的参数。以Scala的柯里化为例:
defadd(x:Int,y:Int)=x+yadd(1,2)//3add(7,3)//10defadd(x:Int)=(y:Int)=>x+yadd(1)(2)//3add(7)(3)//10currying之后add(1)(2)等价于add(1,2),这种currying默认是从左到右的,如果希望从右到左呢,然而大部分编程语言没有实现更灵活的curring。C++11里面的std::bind可以实现currying,但要实现向左或向右灵活的currying比较困难,可以借助tuple和前面介绍的tuple_apply来实现一个更灵活的currying函数对象。
//curryingfromlefttorighttemplateautooperator<<(constUF&f,Arg&&arg)->decltype(f.templatecurry_before(std::forward(arg))){returnf.templatecurry_before(std::forward(arg));}//curryingfromrighttolefttemplateautooperator>>(constUF&f,Arg&&arg)->decltype(f.templatecurry_after(std::forward(arg))){returnf.templatecurry_after(std::forward(arg));}有了这两个重载运算符,测试代码可以写得更简洁了。
voidtest_currying(){autof=[](intx,inty,intz){returnx+y-z;};autofn=fn_to_curry_functor(f);autoresult=(fn<<1)(2,3);//0result=(fn<<1<<2)(3);//0result=(fn<<1<<2<<3)();//0result=(fn<<1>>2<<3)();//2result=(fn>>1>>2<<3)();//1}curry_functor利用了tuple的特性,内部有两个空的tuple,一个用来保存leftcurrying的参数,一个用来保存rightcurrying的参数,不断地currying时,通过tuple_cat把新currying的参数保存到tuple中,最后调用的时候将tuple成员和参数组成一个最终的tuple,然后通过tuple_apply实现调用。有了前面这些基础设施之后我们实现pipeline也是水到渠成。
通过运算符|重载,我们可以实现一个简单的pipeline:
templateautooperator|(T&¶m,constF&f)->decltype(f(std::forward(param))){returnf(std::forward(param));}//testcodevoidtest_pipe(){autof1=[](intx){returnx+3;};autof2=[](intx){returnx*2;};autof3=[](intx){return(double)x/2.0;};autof4=[](doublex){std::stringstreamss;ss<fn_chain的实现思路是这样的:内部有一个std::tuple
templateautocall_impl(Arg&&arg,conststd::index_sequence&)const->decltype(std::get(functions_)(std::forward(arg))){returnstd::get(functions_)(std::forward(arg));}templateautocall_impl(Arg&&arg,conststd::index_sequence&)const->decltype(call_impl(std::get(functions_)(std::forward(arg)),std::index_sequence{})){returncall_impl(std::get(functions_)(std::forward(arg)),std::index_sequence{});}在调用call_impl的过程中,将std::index_sequence不断展开,先从tuple中获取第I个function,然后调用获得第I个function的执行结果,将这个执行结果作为下次调用的参数,不断地递归调用,直到最后一个函数完成调用为止,返回最终的链式调用的结果。
至此我们实现具备惰性求值、高阶函数和currying特性的完整的pipeline,有了这个pipeline,我们可以实现经典的流式计算和AOP,接下来我们来看看如何利用pipeline来实现流式的mapreduce和灵活的AOP。
前面的pipeline已经可以实现链式调用了,要实现pipeline形式的mapreduce关键就是实现map、filter和reduce等高阶函数。下面是它们的具体实现:
//MAPtemplateclassC,typenameF>autofn_map(constC&container,constF&f)->C()))>{usingresultType=decltype(f(std::declval()));Cresult;for(constauto&item:container)result.push_back(f(item));returnresult;}//REDUCE(FOLD)templateclassC,typenameF>TResultfn_reduce(constC&container,constTResult&startValue,constF&f){TResultresult=startValue;for(constauto&item:container)result=f(result,item);returnresult;}//FILTERtemplateclassC,typenameF>Cfn_filter(constC&container,constF&f){Cresult;for(constauto&item:container)if(f(item))result.push_back(item);returnresult;}这些高阶函数还需要转换成支持currying的functor,前面我们已经定义了一个普通的函数对象转换为柯里化的函数对象的方法:
templateautofn_to_curry_functor(F&&f){returncurry_functor(std::forward(f));}通过下面这个宏让curryingfunctor用起来更简洁:
#definemake_globle_curry_functor(NAME,F)define_functor_type(F);constautoNAME=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了。
voidtest_pipe(){//testmapreducevectorslist={"one","two","three"};slist|(map>>[](autos){returns.size();})|(reduce>>0>>[](autoa,autob){returna+b;})|[](autoa){cout<>[](autos){returns.size();})|(reduce>>0>>[](autoa,autob){returna+b;})|([](inta){std::cout<有了这个pipeline,实现灵活的AOP也是很容易的:
structperson{personget_person_by_id(intid){this->id=id;return*this;}intid;std::stringname;};voidtest_aop(){constperson&p={20,"tom"};autofunc=std::bind(&person::get_person_by_id,&p,std::placeholders::_1);autoaspect=tfn_chain|([](intid){cout<<"before";returnid+1;})|func|([](constperson&p){cout<<"after"<我有很多年没搞c++了,所以也记不得那么多了!
函数式编程可不是这个。而是用元编程模拟ifwhilefor等语句