在面向對象的編程中,如果我們需要復用其他的類,我們可以通過繼承來實現。而在函數式編程中我們也可以采取不同的方式來復用這些函數。今天的教程將會講述兩種方式,其中一個就是組合,將多個函數組合成為一個函數,另一個則是之前我們介紹過的部分應用,當然我們將會講述如何將其高級化,來符合我們的使用要求。
組合
顧名思義,組合就是將函數A的結果傳遞給函數B。但是我們並不關注函數A的結果,當然大多數一定會這樣去做:
1 var r1 = funcA(1); 2 var r2 = funcB(r1);
這樣顯然不是我們希望的那樣,假設我們后面需要經常利用到這樣的函數。問題就出現了,所以我們就需要利用組合來將他們合成一個新的函數,首先我們先寫出兩個用來組合的函數:
1 public static int FuncA(int x) 2 { 3 return x + 3; 4 } 5 6 public static int FuncB(int x) 7 { 8 return x + 6; 9 }
如果我們不借助任何的自動化函數,我們可以通過這樣的寫法來進行組合:
Func<int,int> funcC = x => FuncB(FuncA(x));
但是我們這里無法使用var,因為C#的自動推斷類型無法推斷出這個類型。這樣我們就有了一個新的函數funcC,我們可以試着執行這個函數看看最終的結果。上面我們通過手動的方式完成了組合,下面我們將編寫一個自動化的函數來完成這個操作:
1 public static Func<T1, T3> Compose<T1, T2, T3>(Func<T1, T2> func1, Func<T2, T3> func2) 2 { 3 return x => func2(func1(x)); 4 }
接着我們利用這個函數來實現上面的功能:
var funcC = Compose<int, int, int>(FuncA, FuncB);
但是我們發現我們需要提供泛型參數,而不能依賴類型推斷。但如果FuncA和FuncB在此之前顯式的聲明過則不需要提供泛型參數,例如將FuncA和FuncB寫成如下的方式:
Func<int, int> FuncA = x => x + 2; Func<int, int> FuncB = x => x + 6;
這樣在調用Compose函數就不需要提供泛型參數了,順便在這里介紹下其他語言下如何實現相同的功能,在F#中通過 FuncB >> FuncA 來實現,而在Haskell中則是用過 FuncA . FuncB來實現,相比C#來說實現起來就非常的簡單。通過上面的例子我們也發現了一個問題,就是函數A的返回類型必須和函數B的參數類型一致,並且在這個函數鏈中只有首個函數可以擁有多個參數,其他的函數只能擁有一個函數。當然函數鏈的最后一個函數可以是Action,就是說可以沒有返回值,下面筆者寫一個可以將三個函數進行組合的自動化函數:
1 public static Func<T1, T4> Compose<T1, T2, T3, T4>(Func<T1, T2> func1, Func<T2, T3> func2, Func<T3, T4> func3) 2 { 3 return x => func3(func2(func1(x))); 4 }
當然實際開發中我們並不需要寫,可以直接利用FCSLib中提供的函數。
高級的部分應用
學習過《函數式編程之部分應用》的人一定知道,部分應用就是將需要多個參數的函數,拆成一個函數鏈,每個函數鏈都只需要一個參數,假如FuncA需要三個參數,則使用部分應用后調用這個函數就需要按照如下的方式來使用FuncA(2)(3)(2),所以下面的內容筆者不會重復的介紹已經介紹過的內容,如果讀者沒有學習過,可以進入到上面對應的頁面中進行學習。
我們知道在C#中如果傳入部分應用這個自動化函數中的參數是方法,類型推斷會無法工作,那么我們就需要輸入繁瑣的類型參數,比如下面這種情況:
Functional.Curry<Converter<int,int>,Ienumerable<int>,Ienumerable<int>>(Functional.Map<int,int>);
讀者會發現類型參數就占據的一半,上面我們也介紹了如何解決這個問題,所以我們可以寫個已經顯式聲明過類型的函數來封裝下Map函數:
public static Func<Converter<int, int>, IEnumerable<int>, IEnumerable<int>> MapDelegate<T1, T2>() { return Map<T1, T2>; }
這樣我們在調用Curry函數就不需要提供類型參數了:
Functional.Curry(Functional.MapDelegate<int,int>());
至此,類型推斷的問題我們就解決了。在實際開發中部分應用雖然十分有用,但是在某些情形下卻十分的麻煩,比如函數Filter需要兩個算法,最后一個參數為數據。在實際使用中我們都會將兩個算法賦進去,而在后面的使用中僅僅只會改變對應的數據,但是在采用部分應用后就顯得麻煩了,下面是Filter函數的實現:
1 public static IEnumerable<R> Filter<T,R>(Func<T,R> map,Func<T, bool> compare, IEnumerable<T> datas) 2 { 3 foreach (T item in datas) 4 { 5 if (compare(item)) 6 { 7 yield return map(item); 8 } 9 } 10 }
具體的功能就是通過compare函數判斷是否符合條件,然后通過map函數返回需要的部分。我們可以通過如下的方式來調用這個函數:
1 foreach (int x in Filter<int, int>(x => x, x => x <= 10, new int[] { 2, 3, 1, 4, 5, 3, 34 })) 2 { 3 Console.WriteLine(x); 4 } 5 Console.ReadKey();
在采用部分應用前,我們先寫出這個函數的Delegate版本,這樣我們就可以利用類型推斷了:
1 public static Func<Func<T, R>, Func<T, bool>, IEnumerable<T>, IEnumerable<R>> FilterDelegate<T, R>() 2 { 3 return Filter<T, R>; 4 }
然后我們就可以輕松的使用Currey函數將其部分應用了,這里筆者直接自己實現了一個Currey函數,並沒有使用FCSLib中提供的。讀者可以參考下:
1 public static Func<T1,Func<T2,Func<T3,R>>> Currey<T1,T2,T3,R>(Func<T1,T2,T3,R> func) 2 { 3 return x => y => z => func(x, y, z); 4 }
最后我們通過實際的使用來看看:
1 var f = Currey(FilterDelegate<int, int>()); 2 foreach (int x in f(x => x)(x => x <= 10)(new int[] { 2, 3, 1, 4, 5, 3, 34 })) 3 { 4 Console.WriteLine(x); 5 } 6 Console.ReadKey();
即使這樣也很繁瑣,所以我們需要進行更高級的部分應用,這里我們需要另一個自動化函數來幫助我們實現:
1 public static Func<T3,R> Apply<T1, T2, T3, R>(Func<T1, Func<T2, Func<T3, R>>> func,T1 arg1,T2 arg2) 2 { 3 return x => func(arg1)(arg2)(x); 4 }
這個函數的作用就是將原本的部分應用的函數變成一個接收兩個參數並返回一個只接收一個參數的函數,因為算法部分不會變動,但是數據會經常的變動。下面我們通過一個實際的運用來展示:
1 var f = Apply(Currey(FilterDelegate<int, int>()), x => x, x => x <= 10); 2 3 foreach (int x in f(new int[] { 2, 3, 1, 4, 5, 3, 34 })) 4 { 5 Console.WriteLine(x); 6 } 7 foreach (int x in f(new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13 })) 8 { 9 Console.WriteLine(x); 10 } 11 Console.ReadKey();
通過這樣一番折騰后,我們就得到的我們真正需要的函數了,我們在一開始的時候確定算法。然后在后面的使用中我們就可以只傳遞數據即可。