每個人都應(yīng)該懂點函數(shù)式編程
一個問題
假設(shè)現(xiàn)在我們需要開發(fā)一個繪制數(shù)學(xué)函數(shù)平面圖像(一元)的工具庫,可以提供繪制各種函數(shù)圖形的功能,比如直線f(x)=ax+b、拋物線 f(x)=ax²+bx+c或者三角函數(shù)f(x)=asinx+b等等。那么怎么設(shè)計公開接口呢?由于每種行數(shù)的系數(shù)(a、b、c等)不同,并且函數(shù)構(gòu)造 也不同。正常情況下我們很難提供一個統(tǒng)一的接口。所以會出現(xiàn)類似下面這樣的公開方法:
- //繪制直線函數(shù)圖像
- public void DrawLine(double a, double b)
- {
- List<PointF> points = new List<PointF>();
- for(double x=-10;x<=10;x=x+0.1)
- {
- PointF p =new PointF(x,a*x+b);
- points.Add(p);
- }
- //將points點連接起來
- }
- //繪制拋物線圖像
- public void DrawParabola(double a, double b, double c)
- {
- List<PointF> points = new List<PointF>();
- for(double x=-10;x<=10;x=x+0.1)
- {
- PointF p =new PointF(x,a*Math.Pow(x,2) + b*x + c);
- points.Add(p);
- }
- //將points點連接起來
- }
- ...
- DrawLine(3, 4); //繪制直線
- DrawParabola(1, 2, 3); //繪制拋物線
如果像上面這種方式著手的話,繪制N種不同函數(shù)就需要定義N個接口。很明顯不可能這樣去做。
(注,如果采用虛方法的方式,要繪制N種不同函數(shù)圖像就需要定義N個類,每個類中都需要重寫生成points的算法)
如果我們換一種方式去思考,既然是給函數(shù)繪制圖像,為什么要將它們的系數(shù)作為參數(shù)傳遞而不直接將函數(shù)作為參數(shù)傳給接口呢?是的,沒錯,要繪制什么函 數(shù)圖像,那么我們直接將該函數(shù)作為參數(shù)傳遞給接口。由于C#中委托就是對方法(函數(shù),這里姑且不討論兩者的區(qū)別)的一個封裝,那么C#中使用委托實現(xiàn)如 下:
- public delegate double Function2BeDrawed(double x);
- //繪制函數(shù)圖像
- public void DrawFunction(Function2BeDrawed func)
- {
- List<PointF> points = new List<PointF>();
- for(double x=-10;x<=10;x=x+0.1)
- {
- PointF p =new PointF(x,func(x));
- points.Add(p);
- }
- //將points點連接起來
- }
- ...
- Function2BeDrawed func =
- (Function2BeDrawed)((x) => { return 3*x + 4;}); //創(chuàng)建直線函數(shù)
- DrawFunction(func); //繪制系數(shù)為3、4的直線
- Function2BeDrawed func2 =
- (Function2BeDrawed)((x) => {return 1*Math.Pow(x,2) + 2*x + 3;}); //創(chuàng)建拋物線函數(shù)
- DrawFunction(func2); //繪制系數(shù)為1、2、3的拋物線
- Function2BeDrawed func3 =
- (Function2BeDrawed)((x) => {return 3*Math.Sin(x) + 4;}); //創(chuàng)建正弦函數(shù)
- DrawFunction(func3); //繪制系數(shù)為3、4的正弦函數(shù)圖像
如上。將函數(shù)(委托封裝)作為參數(shù)直接傳遞給接口,那么接口就可以統(tǒng)一。至于到底繪制的是什么函數(shù),完全由我們在接口外部自己確定。
將函數(shù)看作和普通類型一樣,可以對它賦值、存儲、作為參數(shù)傳遞甚至作為返回值返回,這種思想是函數(shù)式編程中最重要的宗旨之一。
注:上面代碼中,如果覺得創(chuàng)建委托對象的代碼比較繁雜,我們可以自己再定義一個函數(shù)接收a、b兩個參數(shù),返回一個直線函數(shù),這樣一來,創(chuàng)建委托的代碼就不用重復(fù)編寫。
函數(shù)式編程中的函數(shù)
在函數(shù)式編程中,我們將函數(shù)也當(dāng)作一種類型,和其他普通類型(int,string)一樣,函數(shù)類型可以賦值、存儲、作為參數(shù)傳遞甚至可以作為另外一個函數(shù)的返回值。下面分別以C#和F#為例簡要說明:
注:F#是.NET平臺中的一種以函數(shù)式編程范式為側(cè)重點的編程語言。舉例中的代碼非常簡單,沒學(xué)過F#的人也能輕松看懂。F#入門看這里:MSDN
定義:
在C#中,我們定義一個整型變量如下:
int x = 1;
在F#中,我們定義一個函數(shù)如下:
let func x y = x + y
賦值:
在C#中,我們將一個整型變量賦值給另外一個變量:
- int x = 1;
- int y = x;
在F#中,我們照樣可以將函數(shù)賦值給一個變量:
- let func = fun x y -> x + y //lambda表達(dá)式
- let func2 = func
存儲:
在C#中,我們可以將整型變量存儲在數(shù)組中:
int[] ints = new int[]{1, 2, 3, 4, 5};
在F#中,我們照樣可以類似的存儲函數(shù):
- let func x = x + 1
- let func2 x = x * x
- let func3 = fun x -> x – 1 //lambda表達(dá)式
- let funcs = [func; func2; func3] //存入列表,注意存入列表的函數(shù)簽名要一致
傳參:
在C#中將整型數(shù)值作為參數(shù)傳遞給函數(shù):
- void func(int a, int b)
- {
- //
- }
- func(1, 2);
在F#中將函數(shù)作為參數(shù)傳遞給另外一個函數(shù):
- let func x = x * x //定義函數(shù)func
- let func2 f x = //定義函數(shù)func2 ***個參數(shù)是一個函數(shù)
- f x
- func2 func 100 //將func和100作為參數(shù) 調(diào)用func2
作為返回值:
在C#中,一個函數(shù)返回一個整型:
- int func(int x)
- {
- return x + 100;
- }
- int result = func(1); //result為101
在F#中,一個函數(shù)返回另外一個函數(shù):
let func x =
let func2 = fun y -> x + y
func2 //將函數(shù)func2作為返回值
let result = (func 100) 1 //result為101,括號可以去掉
數(shù)學(xué)和函數(shù)式編程
函數(shù)式編程由Lambda演算得來,因此它與我們學(xué)過的數(shù)學(xué)非常類似。在學(xué)習(xí)函數(shù)式編程之前,我們***忘記之前頭腦中的一些編程思想(如學(xué)習(xí)C C++的時候),因為前后兩個編程思維完全不同。下面分別舉例來說明函數(shù)式編程中的一些概念和數(shù)學(xué)中對應(yīng)概念關(guān)系:
注:關(guān)于函數(shù)式編程的特性(features)網(wǎng)上總結(jié)有很多,可以在這篇博客中看到。
1.函數(shù)定義
數(shù)學(xué)中要求函數(shù)必須有自變量和因變量,所以在函數(shù)式編程中,每個函數(shù)必須有輸入?yún)?shù)和返回值。你可以看到F#中的函數(shù)不需要顯示地使用關(guān)鍵字 return去返回某個值。所以,那些只有輸入?yún)?shù)沒有返回值、只有返回值沒有輸入?yún)?shù)或者兩者都沒有的函數(shù)在純函數(shù)式編程中是不存在的。
2.無副作用
數(shù)學(xué)中對函數(shù)的定義有:對于確定的自變量,有且僅有一個因變量與之對應(yīng)。言外之意就是,只要輸入不變,那么輸出一定固定不變。函數(shù)式編程中的函數(shù)也符合該規(guī)律,函數(shù)的執(zhí)行既不影響外界也不會被外界影響,只要參數(shù)不變,返回值一定不變。
3.柯里化
函數(shù)式編程中,可以將包含了多個參數(shù)的函數(shù)轉(zhuǎn)換成多個包含一個參數(shù)的函數(shù)。比如對于下面的函數(shù):
- let func x y = x + y
- let result = func 1 2 //result為3
- 可以轉(zhuǎn)換成
- let func x =
- let func2 = fun y -> x + y
- func2
- let result = (func 1) 2 //result結(jié)果也為3,可以去掉括號
可以看到,一個包含兩個參數(shù)的函數(shù)經(jīng)過轉(zhuǎn)換,變成了只包含一個參數(shù)的函數(shù),并且該函數(shù)返回另外一個接收一個參數(shù)的函數(shù)。***調(diào)用結(jié)果不變。這樣做的好處便是:講一個復(fù)雜的函數(shù)可以分解成多個簡單函數(shù),并且函數(shù)調(diào)用時可以逐步進(jìn)行。
其實同理,在數(shù)學(xué)中也有類似“柯里化”的東西。當(dāng)我們計算f(x,y) = x + y這個函數(shù)時,我們可以先將x=1帶入函數(shù),得到的結(jié)果為f(1,y) = 1 + y。這個結(jié)果顯然是一個關(guān)于y的函數(shù),之后我們再將y=2帶入得到的函數(shù)中,結(jié)果為f(1,2) = 1 + 2。這個分步計算的過程其實就是類似于函數(shù)式編程中的“柯里化”。
4.不可變性
數(shù)學(xué)中我們用符號去表示一個值或者表達(dá)式,比如“令x=1”,那么x就代表1,之后不能再改變。同理,在純函數(shù)式編程中,不存在“變量”的概念,也沒有“賦值”這一說,所有我們之前稱之為“變量”的東西都是標(biāo)識符,它僅僅是一個符號,讓它表示一個東西之后不能再改變了。
5.高階函數(shù)
在函數(shù)式編程中,將參數(shù)為函數(shù)、或者返回值為函數(shù)的這類函數(shù)統(tǒng)稱之為“高階函數(shù)”,前面已經(jīng)舉過這樣的例子。在數(shù)學(xué)中,對一個函數(shù)求導(dǎo)函數(shù)的過程,其實就是高階函數(shù),原函數(shù)經(jīng)過求導(dǎo)變換后,得到導(dǎo)函數(shù),那么原函數(shù)便是輸入?yún)?shù),導(dǎo)函數(shù)便是返回值。
混合式編程風(fēng)格
過程式、面向?qū)ο笤俚竭@篇文章講到的函數(shù)式等,這些都是不同地編程范式。每種范式都有自己的主導(dǎo)編程思想,也就是對待同一個問題思考方式都會不同。很明顯,學(xué)會多種范式的編程語言對我們思維方式有非常大的好處。
無論是本文中舉例使用到的F#還是Java平臺中的Scala,大多數(shù)冠名“函數(shù)式編程語言”的計算機(jī)語言都并不是純函數(shù)式語言,而是以“函數(shù)式” 為側(cè)重點,同時兼顧其他編程范式。就連曾經(jīng)主打“面向?qū)ο?rdquo;的C#和Java,現(xiàn)如今也慢慢引入了“函數(shù)式編程風(fēng)格”。C#中的委托、匿名方法以及 lambda表達(dá)式等等這些,都讓我們在C#中進(jìn)行函數(shù)式編程成為可能。如果需要遍歷集合找出符合條件的對象,我們以前這樣去做:
- foreach(Person p in list)
- {
- if(p.Age > 25)
- {
- //...
- }
- }
現(xiàn)在可以這樣:
list.Where(p => p.Age>25).Select(p => p.Name).toArray();
本篇文章開頭提出的問題,采用C#委托的方式去解決,其實本質(zhì)上也是函數(shù)式思想。由于C#必須遵循OO準(zhǔn)則,所以引入委托幫助我們像函數(shù)式編程那樣去操作每個函數(shù)(方法)。
本篇文章介紹有限,并沒有充分說明函數(shù)式編程的優(yōu)點,比如它的不可變特性無副作用等有利于并行運算、表達(dá)方式更利于人的思維等等。實質(zhì)上博主本人并沒有參與過實際的采用函數(shù)式語言開發(fā)的項目,但是博主認(rèn)為函數(shù)式思想值得我們每個人去了解、掌握。