簡單的c++函數式編碼

下面用一個例子來用函數式方式實現某個需求,看下在函數式的思想下是如何一層層進行抽象的:

/*
 * 需求:從給定的無序整數序列中取出所有大于10且小于50的偶數,對其加倍后求和
 * 例如:
 * 原始序列:[32, 14, 3, 21, 109, 50, 25, 26, 18]
 * 操作一:過濾,留下大于10小于50:[32, 14, 21, 25, 26, 18]
 * 操作二:過濾,留下偶數:[32, 14, 26, 18]
 * 操作三:加倍:[64, 28, 52, 36]
 * 操作四:求和:180
*/

面向過程的一般寫法:

int f1(const vector<int> &input)
{
    int sum = 0;
    for(int ele : input) {
        if (ele > 10 && ele < 50) {
            if (ele % 2 == 0) {
                sum += ele * 2;
            }
        }
    }
    return sum;
}

這種寫法,第一眼看過去是不知道該函數在做什么,需要一點點代碼分析。原因就在于缺乏抽象。
我們先來進行最容易想到的第一層抽象:

bool isBetween(int input)
{
    return input > 10 && input < 50;
}

bool isEven(int input)
{
    return input % 2 == 0;
}

int f2(const vector<int> &input)
{
    int sum = 0;
    for(int ele : input) {
        if (isBetween(ele)) {
            if (isEven(ele)) {
                sum += ele * 2;
            }
        }
    }
    return sum;
}

比之前好一些么?大概也就只強了那么一點點……

下面展示下使用stl算法庫的寫法,相對來說每個操作的可讀性變強了不少:

int f3(const vector<int> &input)
{
    vector<int> filtRange;
    copy_if(begin(input), end(input), back_inserter(filtRange), isBetween);  // 操作一,通過copy_if過濾,條件是isBetween
    vector<int> filtEven;
    copy_if(begin(filtRange), end(filtRange), back_inserter(filtEven), isEven);  // 操作二,通過copy_if過濾,條件是isEven
    vector<int> doubleValue;
    transform(begin(filtEven), end(filtEven), back_inserter(doubleValue), [](int x) { return x * 2; });  //操作三,通過transform修改每條數據,修改方式是×2
    return accumulate(begin(doubleValue), end(doubleValue), 0);  // 操作四,從0開始累加
}

使用stl算法庫看起來已經很好閱讀了,那么對于函數式來說,我們還有什么可以抽象的呢?
抽象一方面是為了增強可讀性,另一方面是為了增強普適性,便于復用。
從復用角度來看,之前的isBetween只能判斷在10和50之間,不能適用其他范圍,因此進一步的抽象可以優化這里:

// 過濾器
using Filter = function<bool(int input)>;

// 生成一種過濾器的函數
Filter isBetween(int left, int right)
{
    // 返回值是一個函數
    return [=](int input) {
        return input > left && input < right;
    };
}

這里的isBetween是之前的isBetween的抽象,該函數調用的返回值其實就是原來的isBetween函數。

這樣,完整調用流程就變成了這樣:

int f4(const vector<int> &input)
{
    vector<int> filtRange;
    copy_if(begin(input), end(input), back_inserter(filtRange), isBetween(10, 50));
    vector<int> filtEven;
    copy_if(begin(filtRange), end(filtRange), back_inserter(filtEven), isEven);
    vector<int> doubleValue;
    transform(begin(filtEven), end(filtEven), back_inserter(doubleValue), [](int x) { return x * 2; });
    return accumulate(begin(doubleValue), end(doubleValue), 0);
}

isBetween(10, 50)相比之前的isBetween,明確了判斷是在10到50之間,相比之前讀起來更直觀一些;同時也可以在別的代碼處對不同的范圍條件復用。
這里其實就用到了函數式的基礎——將函數作為返回值。難道所謂的函數式就這???

來吧,展示

如果我們要做進一步抽象,考慮到這里都是對一個數據序列做操作,一共四步操作:前兩步操作都是過濾,第三步操作是對每條數據做轉換,第四步操作是對所有數據一起做個整合;
我們把“過濾”、“數據轉換”、“數據整合”作為一個抽象層級,再利用pipeline方式做形式化處理。對于過濾,我們定義如下形式:

// 輸入一個序列和過濾器,輸出過濾后的序列
vector<int> operator | (const vector<int> &input, Filter filter)
{
    vector<int> output;
    copy_if(begin(input), end(input), back_inserter(output), filter);
    return output;
}

使用過濾器之后,我們的完整處理流程形式如下:

int f5(const vector<int> &input)
{
    auto filt = input | isBetween(10, 50) | isEven;  // isBetween(10, 50)和isEven是兩個過濾器,對input做過濾后的結果是filt
    vector<int> doubleValue;
    transform(begin(filt), end(filt), back_inserter(doubleValue), [](int x) { return x * 2; });
    return accumulate(begin(doubleValue), end(doubleValue), 0);
}

對于數據轉換,我們做如下定義:

// 數據轉換器
using Transformer = function<int(int input)>;

// 輸入一個序列和轉換器,輸出轉換后的序列
vector<int> operator | (const vector<int> &input, Transformer trans)
{
    vector<int> output;
    transform(begin(input), end(input), back_inserter(output), trans);
    return output;
}

然后我們再對乘2動作再做一次抽象級別的提升,可指定任意倍數擴展:

Transformer multiplyBy(int x)
{
    return [x](int input) {
        return input * x;
    };
}

注意到multiplyBy也是一個高階函數,它返回了一個轉換器函數。

這時候我們的完整處理流程變成了如下形式:

int f6(const vector<int> &input)
{
    auto out = input | Filter(isBetween(10, 50)) | Filter(isEven) | Transformer(multiplyBy(2));
    return accumulate(begin(out), end(out), 0);
}

至此,我們閱讀上面的代碼,已經可以“口述”了:

對序列input元素按是否在10到50之間過濾,再按是否偶數過濾,再做乘2轉換得到新序列out;返回out序列從0開始的累加結果

直接口述代碼,意味著我們不需要再去思考這段代碼的意圖,閱讀代碼變得簡單。
這就體現出函數式宣稱的一大好處:描述做什么,而非怎么做

我們再來看看數據整合怎么實現:

template<typename T>
using FoldFunc = function<T(const T&, int)>;

template<typename T>
struct Fold {
    Fold(FoldFunc<T> f, const T &in) : func(f), init(in) {};
    FoldFunc<T> func;  // 折疊函數,表示數據整合的方法
    T init;  // 初值
};

template<typename T>
T operator | (const vector<int> &input, const Fold<T> &fold)
{
    T result = fold.init;
    for (int i : input) {
        result = fold.func(result, i);
    }
    return result;
}

完成數據整合之后,處理的完整流程如下:

// 對于累加來說,折疊函數就是Add:
int Add(int a, int b)
{
    return a + b;
}

int f7(const vector<int> &input)
{
    return input | Filter(isBetween(10, 50)) | Filter(isEven) | Transformer(multiplyBy(2)) | Fold<int>(Add, 0);
}

我們可以看到,一行代碼就完成了整個功能,且達成了“口述”代碼流程:

“過濾input中10到50之間的偶數,再乘2之后從0開始累加”。

對比我們的原始需求描述:

“從給定的無序整數序列中取出所有大于10且小于50的偶數,對其加倍后求和”

不能說完全相同,只能說是一模一樣

可能有同學有疑問,accumulate已經很直觀了,搞個Fold沒看出來有多大好處呀?
其實這東西在函數式中是很基礎和常見的。比如我們打印一個vector,可以利用Fold這樣實現:

void print(const vector<int> &input)
{
    string content = input | Fold<string>([](const string &s, int i) { return s + " " + to_string(i); }, "[");
    cout << content << " ]" << endl;
}

可能有的同學會說了,你這個打印只能打印int元素的vector,其他的搞不定!
說起來,我們上面的Fold其實限定了一個條件:每個元素的類型和折疊后的結果類型是一致的。
如果我們放開這個限制,比如如下形式定義,就可以搞定其他情況了:

template<typename T, typename U>
using FoldFunc2 = function<T(const T&, const U&)>;

template<typename T, typename U>
struct Fold2 {
    Fold2(FoldFunc2<T, U> f, const T &in) : func(f), init(in) {};
    FoldFunc2<T, U> func;
    T init;
};

template<typename T, typename U>
T operator | (const vector<U> &input, const Fold2<T, U> &fold)
{
    T result = fold.init;
    for (const U &i : input) {
        result = fold.func(result, i);
    }
    return result;
}

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,001評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,786評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,986評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,204評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,964評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,354評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,410評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,554評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,106評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,918評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,093評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,648評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,342評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,755評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,009評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,839評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,107評論 2 375

推薦閱讀更多精彩內容