下面用一個例子來用函數式方式實現某個需求,看下在函數式的思想下是如何一層層進行抽象的:
/*
* 需求:從給定的無序整數序列中取出所有大于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;
}