學習 FC++:C++ 函數編程庫
為什么要實現函數編程,尤其是使用 FC++實現?
與 OOP 等其他編程模型相比,函數編程具有一些優點:
1.代碼簡潔
2.編程沒有副作用(沒有通過 set/get 例程操縱的全局/靜態變量)
3.可以進行快速的原型設計
4.FC++提供大量語法和庫函數,幫助 Haskell 程序員順利完成轉換
通過使用庫您并不能回避 C++本身沒有任何函數編程構造這一事實。FC++是最好的基于 C++的函數編程庫開放源碼實現,可以把它插入遺留的 C++代碼中。BSFC++等項目中已經使用了 FC++,BSFC++是一個用 C++進行函數大規模同步并行編程的庫。
下載和安裝
可以從 SourceForge 下載 FC++(見參考資料)。解壓壓縮的安裝文件(.zip)會產生一組頭文件。只需在用戶應用程序源代碼中包含 prelude.h 頭文件,就可以開始使用 FC++。清單 1 說明如何編譯使用 FC++代碼的源代碼。注意,這是依賴于頭文件的安裝,不涉及其他庫。
清單 1.編譯使用 FC++代碼的源代碼
注意:本文中的所有代碼已經用 FC++ 1.5 和 g++ 3.4.4 測試過。
理解 CFunType
函數編程模型允許函數接受其他函數作為參數。顯然,基本的 C/C++不允許這種語法。為了解決這個問題,FC++函數表示為符合特定編碼約定的類的實例,這就是 CFunType 發揮作用的地方。C++函數對象的特征是在類定義中有 operator ()。清單 2 給出一個示例。
清單 2. C++函數對象的典型用法
int operator()(int x){ return x*x; }
};
square sqr1;
int result = sqr1(5);
清單 2 中的實現的問題在于,準確地說,sqr1 的函數類型是 int —> int,但是 sqr1 的 C++類型是 struct square。FC++引入了模板 CFunType,使用它對類型簽名信息進行編碼。CFunType 的最后一個參數是函數的返回類型,其他參數是輸入類型信息,其次序與函數原型中的次序相同。清單 3 說明如何使用 CFunType 對 square 函數進行編碼。
清單 3.使用 CFunType 對求平方操作的函數簽名進行編碼
struct square : public CFunType<int, int> {
int operator()(int x){ return x*x; }
};
square sqr1;
int result = sqr1(5);
清單 4 是另一個示例,它在列表中插入一個整數并返回更新后的列表。
清單 4.使用 CFunType 對列表操作的函數簽名進行編碼
struct Insert : public CFunType<int,List<int>,List<int> > {
List<int> operator()( int x, const List<int>& l ) const {
// code for determining where to insert the data goes here
}
注意:清單 4 中的 List 數據類型是預定義的 FC++類型。
把函數轉換為對象
為了讓函數接受函數作為輸入參數,必須把函數轉換為對象。FC++在 CFunType 和 ptr_to_fun 例程的基礎上定義 FunN 類別的類,由 ptr_to_fun 例程實際執行轉換。請看一下清單 5。
清單 5.使用 ptr_to_fun 把函數轉換為 FC++函數對象
Fun2<int, int, int> mult1 = ptr_to_fun (&multiply);
int result = mult1(8, 9);
// result equals 72
與 CFunType 一樣,Fun2 的簽名暗示這個對象代表一個函數,此函數接受兩個整數輸入并返回一個整數。同樣,Fun3<int, double, double, string> 代表的函數接受一個整數和兩個雙精度數,返回一個字符串。
列表和惰性
列表操作是函數編程的核心。FC++定義了自己的列表數據類型,它不同于 Standard Template Library(STL)列表。FC++列表是惰性的。可以在 FC++中創建包含無限多元素的列表,但是只在需要時計算這些元素。清單 6 說明其意義。
清單 6.定義和使用惰性列表
List<int> even_and_greater_than_33 = filter (even, numbers);
assert (take(4, even_and_greater_than_33)) = list_with (34, 36, 38, 40);
enumFrom、filter、even、take 和 list_with 是 FC++中預定義的函數。在清單 6 中,enumFrom 返回一個從 33 開始的無限數字列表。filter 例程返回另一個無限列表,其中的數字是大于 33 的偶數。最后,take 例程實際上從這個列表中提取出前 4 個元素。顯然,列表實際上不會存儲無限多的數字—只在需要時計算元素。表 1 說明 FC++列表常用的一些函數。
表 1.與 FC++列表結合使用的函數
函數 | 說明 |
---|---|
head(<list>) |
返回列表的第一個元素 |
tail(<list>) |
返回一個列表,其中的元素與 <list> 相同,但是不包含第一個元素 |
cons(<element >,<list>) |
返回一個列表,其中的元素與 <list> 相同,但是在最前面添加了<element> |
NIL |
表示空列表 |
list_with(<element1, element2>,…,<elementN>) |
創建一個包含 N 個元素的列表 |
enumFrom(<element1>) |
創建一個從 element1 開始的無限列表 |
compose(<func1>,<func2>) |
compose (f, g)相當于 f(g(x)),其中的 f(x)和 g(x)是兩個函數 |
filter(<func1>,<list>) |
返回使用 <func1> 函數篩選 <list> 所獲得的元素列表 |
take(<N>,<list>) |
返回包含 <list> 中前 N 個元素的列表 |
map(<function>,<list>) |
把第一個 <function> 函數應用于第一個 <list> 的每個元素 |
清單 7 中的示例說明如何創建和顯示列表的內容。
清單 7.創建列表、檢查列表內容并顯示數據
#include "prelude.h"
int main()
{
int x=1,y=2,z=3;
List<int> li = cons(x,cons(y,cons(z,NIL)));
// head also removes the 1st element from the list
assert( head(li) == 1 );
// tail returns whatever is left of in the list, and list_with is
// used to define small sized list
assert( tail(li) == list_with(2,3));
while( li ){
std::cout << li.head()<< "";
li = li.tail();
}
return 0;
}
注意:在創建 li 列表時,cons 在一個列表的前面添加元素;通過依次添加z、y和x創建最終的列表。
更快的列表實現
FC++ 1.5 提供 List 數據結構的另一種變體,它稱為 OddList,在 list.h 中定義。OddList 的接口與 List 完全相同,但是它更快。所有對 List 進行操作的 FC++例程也適用于 OddList。OddList 的效率高是由于它緩存列表中的下一個節點。清單 8 演示 OddList 的一些比較微妙的用法。
清單 8.比較微妙的 OddList 用法
List<int> list1 = odd1.tail (); // always returns List<int>!!
OddList<int> odd2 = enumFrom (1);
List<int> list2 = odd2.delay(); // create a List<int> with same data as odd2
List<int> list3 = enumFrom (1);
OddList<int> odd3 = list3.force (); // creates an OddList<int> with same data as list3
OddList 不支持適用于 List 的 STL 式迭代器。關于 OddList 實現的詳細信息見參考資料。
創建自己的篩選器
如果希望在清單 6 中創建自己的篩選器(例如所有能夠被 100 整除的大于 33 的數字),那么只需定義自己的篩選函數,然后調用 ptr_to_fun 把它轉換為函數對象。清單 9 說明具體做法。
清單 9.創建自己的篩選器
return n % 100 ? false : true;
}
List<int> num = enumFrom(34);
List<int> my_nums = filter( ptr_to_fun(&div_by_100), num);
注意,FC++ List 和 filter 在本質上是完全泛化的,可以適用于任何數據類型。接下來,討論兩種基本的函數技術:currying 和合成。
currying
currying 這種函數編程技術把某一函數的一部分參數綁定到固定的值,由此創建新函數。清單 10 中的示例對 f 函數執行 currying。
清單 10.使用 currying 創建新函數
Fun2<int, int, int> f2 = ptr_to_fun (&multiply);
Fun1<int, int> f1 = curry2 (f2, 9);
std::cout << f1(4)<< std::endl; // equivalent to multiply(9, 4)
Fun1<int, int> f1_implicit = f2(9);
std::cout << f1_implicit(4)<< std::endl; // same as f1(4)
預定義的 curry2 例程把 f2 的第一個參數綁定到 9。FC++ 1.5 提供 curry1、curry2 和 curry3 操作符,它們把前 N 個參數綁定到特定的值。另外,FC++還定義綁定例程,它們通過預先固定現有函數的特定參數的值來創建新函數。例如,bind2and3of3 (f, 8, 9)相當于 f(x, 8, 9),其中的 f(x,y,z)是有 3 個輸入參數的函數。另一種對參數進行特化的方式是使用下劃線(_)。例如,greater (_, 10)相當于 f(x) = (x> 10)。注意,greater 是 FC++中預定義的。清單 11 給出一些 currying 示例。
清單 11.更多 currying 示例
List<int> int_gt_100 = filter(greater(_, 100), integers);
// This list will add 3 to all elements of integers.
List<int> plus_3 = map (plus(3), integers);
清單 12 中的代碼片段顯示一個數字的所有因子,包括這個數字本身。
清單 12.顯示一個數字的所有因子
#include "prelude.h"
using namespace fcpp;
#include <iostream>
using namespace std;
bool divisible( int x, int y){ return x%y==0; }
struct Factors : public CFunType<int,OddList<int> > {
OddList<int> operator()( int x) const {
return filter( curry2(ptr_to_fun(&divisible),x), enumFromTo(1,x));
}
} factors;
int main()
{
OddList<int> odd = factors(20);
while (odd){
cout << head(odd)<< endl;
odd = tail(odd);
}
return 0;
}
理解清單 12 的關鍵是這一行:return filter( curry2(divisible,x), enumFromTo(1,x));。這為 enumFrom(1, 20)返回的列表創建一個篩選器,讓 20 能夠整除的所有數字成為最終列表的一部分。curry2 例程把 20 綁定到 divisible 函數的第一個參數。注意,ptr_to_fun 把 divisible 轉換為函數對象,這樣就可以把它作為參數傳遞給 curry2。
合成
函數編程通過組合現有代碼創建新功能。compose ()操作符把兩個一元函數 f(x)和 g(x)組合成新函數 h(x),h(x)相當于 f(g(x))。例如,compose (head, tail)返回列表中的第二個元素。這是真正意義上的函數編程;g(x)作為 f(x)的參數。取自"Functional Programming with the FC++ Library"的清單 13(見參考資料)是一個使用合成的示例。
清單 13.使用 compose 和 tail 獲取列表的第二個元素
List<std::string> ls = cons(s, cons(t, cons(u, NIL)));
ls = compose(tail, tail)(ls); // tail(tail(ls));
assert (head(ls) == "qux"); // s, t are removed
清單 14 中的示例讓列表的所有元素遞增 2。
清單 14.使用 compose 遞增列表元素
map (compose(inc, inc), integers);
// this modifies integers to an infinite list [3, 4, 5 ...]
lambda 函數
討論函數編程就不能不提到 lambda 函數。lambda 抽象用來定義匿名函數。在不希望為小代碼段定義單獨的函數的情況下,可以使用這種技術。要想在代碼中使用 lambda 功能,需要定義 FCPP_ENABLE_LAMBDA 宏。清單 15 從現有代碼簡便地定義新的數學和邏輯函數。請注意定義 factorial 的方式。
清單 15.定義 lambda 函數
lambda(X)[ plus[multiplies[3,X],1] ]
// a new function where f(x) = x!(factorial x)
lambda(X)[ l_if[equal[X,0],1,multiplies[X,SELF[minus[X,1]]]] ]
清單 15 中的代碼的意義很明確。plus、multiplies 等例程是在 FC++庫中定義的,使用 lambda 操作符從現有代碼創建新功能。
結束語
FC++提供:CFunType 類型的對象,可以通過擴展它輕松地滿足函數編程的需要惰性列表的實現,其中可以容納數量無限的元素 head、tail、map、filter、ptr_to_fun 等函數編程操作符。使用 currying 操作符、lambda 或 compose 從現有函數創建新函數的能力。FC++惟一的缺點可能是缺少描述頭文件中定義的函數的標準化文檔。本文介紹了最有用的一些函數:compose、curry、bind、take、map、ptr_to_fun 和 filter。