拿 C# 搞函數語言程式設計 - 1
最近閒下來了,準備出一個 C# 搞 FP 的合集。本合集所有程式碼均以 C# 8 為示例。
可能你說,為什麼要這麼做呢?回答:為了好玩。另外,意義黨們請 gun cu ke!
C# 有委託,而且有 Func<> 和 Action<>,可以說函式被視為一等功名,跟 int、bool 等型別並沒有什麼區別。那麼很多事情就簡單了。
純函式
什麼是純函式呢?純函式就是 f(x),它們接收引數,得到結果,並且相同的引數得到的結果一定是相同的,用對映來說,它是滿射的。另外這個函式不會改變任何的狀態值,它是無副作用的。
柯里化
首先,有一個東西讓我覺得不爽,那就是一般來說 C# 裡的函式呼叫不是柯里化的,這也就意味著我沒法一個一個傳引數進去,也沒法把傳了一部分引數的呼叫作為一個新函式拿去給別的地方用,那要怎麼辦呢?
自己動手,豐衣足食!
一個標準的加法函式可以這麼寫:
var function = new Func<int, int, int> ((x, y) => x + y); function(1, 2); // returns 3
如果我們想以柯里化形式呼叫的話,理想狀態是這麼個樣子的:
function 1 2
但是這個括號我們是省不了的,所以這樣也是可以接受的:
function(1)(2);
我們看一下這個呼叫形式,不就是 Func<int, Func<int, int>> 嘛!so easy~
我們只需要把 Func<int, int, int> 轉化為 Func<int, Func<int, int>>:
Func<int, Func<int, int>> Currying(Func<int, int, int> f) => x => y => f(x, y);
這樣寫就 ok 啦。進一步改造成擴充套件方法:
public static class CurryingExtensions { public static Func<int, Func<int, int>> Currying(this Func<int, int, int> f) => x => y => f(x, y); }
於是我們只需要:
var function = new Func<int, int, int> ((x, y) => x + y) .Currying(); function(1)(2); // returns 3
就可以採用柯里化形式呼叫該函式啦。
進一步我們用泛型改造,讓柯里化適用於任何型別:
public static class CurryingExtensions { public static Func<T1, Func<T2, TOutput>> Currying<T1, T2, TOutput>(this Func<T1, T2, TOuput> f) => x => y => f(x, y); }
如果遇到更多引數,我們只需要給這個靜態類裡面再加一個擴充套件方法即可。
那 Action<> 呢?這個東西在我看來完全就是副作用,具體下方有講,我們不用他(逃
Unit
什麼是 Unit 呢?Unit 就是任何函式呼叫後如果沒有結果,就會返回的一個東西。
可能你說,void 不就可以了?
但是如果一個純函式,它沒有返回值(即 Action<>),意味著這個函式它有輸入沒輸出,那這個函式除了能用來產生副作用之外,就什麼都幹不了了。這不清真!
因此我們需要一個 Unit 來代替 void,偷個懶,這個 Unit 就用 ulong 來代替吧。
高階函式
什麼叫做高階函式,把函式當作引數傳給另一個函式,接收這個函式引數的函式就叫做高階函式。
舉個例子:f(g(x)),f 即高階函式。
假設我們現在要開一個超市,超市有很多的產品,每種產品價格不同,不同產品可能還有各自的折扣。我們有很多種快樂水,每種快樂水價格不一樣,可口快樂水 3.5 塊,百事快樂水 3 塊,麥當勞快樂水 9 塊,快樂水價格計算函式:
var happyWater = new Func<float, int, float> ((float price, int number) => number * price) .Currying(); // 呼叫:happyWater(快樂水單價)(快樂水件數); var cocaHappyWater = happyWater(3.5f); var pepsiHappyWater = happyWater(3); var mcdHappyWater = happyWater(9);
超市可能有折扣,A 超市不打折,B 超市打八折,計算價格函式:
var calcPrice = new Func<Func<int, float>, float, int, float> ((calc, discount, number) => discount * calc(number)) .Currying(); // 呼叫:calcPrice(快樂水價格計算函式)(超市折扣)(快樂水件數);
現在我們分別在 A 超市買百事快樂水、B 超市買可口快樂水,麥當勞的太貴了我們不買,價格計算函式為:
var pepsiPriceCalc = calcPrice(pepsiHappyWater); var cocaPriceCalc = calcPrice(cocaHappyWater); var priceCalcA = pepsiPriceCalc(1); // A 超市 var priceCalcB = cocaPriceCalc(0.8f); // B 超市
最後我們在 A 超市買了 3 瓶百事快樂水,B 超市買了 5 瓶可口快樂水,計算總價:
var priceA = priceCalcA(3); var priceB = priceCalcB(5); var total = priceA + priceB;
最後得到 total = 23 元。
可以看到這些函式都是可拆卸並且可以隨意組合的,而且滿足 f(g(x)) = g(f(x))。
貼上完整程式碼示例:
using System; namespace ColaMarket { static class CurryingExtensions { public static Func<T1, Func<T2, TOutput>> Currying<T1, T2, TOutput>(this Func<T1, T2, TOutput> f) => x => y => f(x, y); public static Func<T1, Func<T2, Func<T3, TOutput>>> Currying<T1, T2, T3, TOutput>(this Func<T1, T2, T3, TOutput> f) => x => y => z => f(x, y, z); } class Program { static void Main(string[] args) { var happyWater = new Func<float, int, float> ((float price, int number) => number * price) .Currying(); var cocaHappyWater = happyWater(3.5f); var pepsiHappyWater = happyWater(3); var mcdHappyWater = happyWater(9); var calcPrice = new Func<Func<int, float>, float, int, float> ((calc, discount, number) => discount * calc(number)) .Currying(); var pepsiPriceCalc = calcPrice(pepsiHappyWater); var cocaPriceCalc = calcPrice(cocaHappyWater); var priceCalcA = pepsiPriceCalc(1); var priceCalcB = cocaPriceCalc(0.8f); var priceA = priceCalcA(3); var priceB = priceCalcB(5); var total = priceA + priceB; Console.WriteLine(total); } } }
下一篇將會講更多的東西,如 Functor、Applicative 和 Monad 等等。