本文轉自:http://geek.csdn.net/news/detail/96636c++
做者簡介: 祁宇,武漢烽火雲創軟件技術有限公司研發中心技術總監,《深刻應用C++11》做者,C++開源社區purecpp.org創始人,致力於C++11的應用、研究和推廣。樂於研究和分享技術,愛好C++,愛好開源。git
導讀: 本文做者從介紹函數式編程的概念入手,分析了函數式編程的表現形式和特性,最終經過現代C++的新特性以及一些模板雲技巧實現了一個很是靈活的pipeline,展現了現代C++實現函數式編程的方法和技巧,同時也體現了現代C++的強大威力和無限可能。github
函數式編程是一種編程範式,它有下面的一些特徵:算法
C++98/03中的函數對象,和C++11中的Lambda表達式、std::function和std::bind讓C++的函數式編程變得容易。咱們能夠利用C++11/14裏的新特性來實現高階函數、鏈式調用、惰性求值和柯理化等函數式編程特性。本文將經過一些典型示例來說解如何使用現代C++來實現函數式編程。shell
高階函數就是參數爲函數或返回值爲函數的函數,經典的高階函數就是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了。下面來分別介紹它們的實現技術。
函數式編程的核心就是函數,它是一等公民,最靈活的函數就是高階函數,現代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
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:
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已經能夠實現鏈式調用了,要實現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條評論
我有不少年沒搞c++了,因此也記不得那麼多了!
函數式編程可不是這個。
而是用元編程模擬if while for等語句