天天看點

函數式程式設計

假設現在我們需要開發一個繪制數學函數平面圖像(一進制)的工具庫,可以提供繪制各種函數圖形的功能,比如直線f(x)=ax+b、抛物線 f(x)=ax+bx+c或者三角函數f(x)=asinx+b等等。那麼怎麼設計公開接口呢?由于每種行數的系數(a、b、c等)不同,并且函數構造 也不同。正常情況下我們很難提供一個統一的接口。是以會出現類似下面這樣的公開方法:

函數式程式設計

//繪制直線函數圖像

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) {

       PointF p =new PointF(x,a*Math.Pow(x,2) + b*x + c);

...

DrawLine(3, 4);   //繪制直線

DrawParabola(1, 2, 3);    //繪制抛物線

如果像上面這種方式着手的話,繪制N種不同函數就需要定義N個接口。很明顯不可能這樣去做。

(注,如果采用虛方法的方式,要繪制N種不同函數圖像就需要定義N個類,每個類中都需要重寫生成points的算法)

如果我們換一種方式去思考,既然是給函數繪制圖像,為什麼要将它們的系數作為參數傳遞而不直接将函數作為參數傳給接口呢?是的,沒錯,要繪制什麼函 數圖像,那麼我們直接将該函數作為參數傳遞給接口。由于C#中委托就是對方法(函數,這裡姑且不讨論兩者的差別)的一個封裝,那麼C#中使用委托實作如 下:

public delegate double Function2BeDrawed(double x);

//繪制函數圖像

public void DrawFunction(Function2BeDrawed func){

   for(double x=-10;x<=10;x=x+0.1){

       PointF p =new PointF(x,func(x));

Function2BeDrawed func =

   (Function2BeDrawed)((x) => { return 3*x + 4;}); //建立直線函數

DrawFunction(func);  //繪制系數為3、4的直線

Function2BeDrawed func2 =

   (Function2BeDrawed)((x) => {return 1*Math.Pow(x,2) + 2*x + 3;}); //建立抛物線函數

DrawFunction(func2);  //繪制系數為1、2、3的抛物線

Function2BeDrawed func3 =

   (Function2BeDrawed)((x) => {return 3*Math.Sin(x) + 4;}); //建立正弦函數

DrawFunction(func3);  //繪制系數為3、4的正弦函數圖像

如上。将函數(委托封裝)作為參數直接傳遞給接口,那麼接口就可以統一。至于到底繪制的是什麼函數,完全由我們在接口外部自己确定。

将函數看作和普通類型一樣,可以對它指派、存儲、作為參數傳遞甚至作為傳回值傳回,這種思想是函數式程式設計中最重要的宗旨之一。

注:上面代碼中,如果覺得建立委托對象的代碼比較繁雜,我們可以自己再定義一個函數接收a、b兩個參數,傳回一個直線函數,這樣一來,建立委托的代碼就不用重複編寫。

函數式程式設計中的函數

在函數式程式設計中,我們将函數也當作一種類型,和其他普通類型(int,string)一樣,函數類型可以指派、存儲、作為參數傳遞甚至可以作為另外一個函數的傳回值。下面分别以C#和F#為例簡要說明:

注:F#是.NET平台中的一種以函數式程式設計範式為側重點的程式設計語言。舉例中的代碼非常簡單,沒學過F#的人也能輕松看懂。F#入門看這裡:MSDN

定義:

在C#中,我們定義一個整型變量如下:

int x = 1;

在F#中,我們定義一個函數如下:

let func x y = x + y

指派:

在C#中,我們将一個整型變量指派給另外一個變量:

int y = x;

在F#中,我們照樣可以将函數指派給一個變量:

let func = fun x y -> x + y //lambda表達式

let func2 = func

存儲:

在C#中,我們可以将整型變量存儲在數組中:

int[] ints = new int[]{1, 2, 3, 4, 5};

在F#中,我們照樣可以類似的存儲函數:

let func x = x + 1

let func2 x = x * x

let func3 = fun x -> x – 1 //lambda表達式

let funcs = [func; func2; func3] //存入清單,注意存入清單的函數簽名要一緻

傳參:

在C#中将整型數值作為參數傳遞給函數:

void func(int a, int b)

{

   //

func(1, 2);

在F#中将函數作為參數傳遞給另外一個函數:

let func x = x * x //定義函數func

let func2 f x = //定義函數func2 第一個參數是一個函數

f x

func2 func 100 //将func和100作為參數 調用func2

作為傳回值:

在C#中,一個函數傳回一個整型:

int func(int x)

    return x + 100;

int result = func(1); //result為101

在F#中,一個函數傳回另外一個函數:

let func x =

let func2 = fun y -> x + y

func2 //将函數func2作為傳回值

let result = (func 100) 1 //result為101,括号可以去掉

數學和函數式程式設計

函數式程式設計由Lambda演算得來,是以它與我們學過的數學非常類似。在學習函數式程式設計之前,我們最好忘記之前頭腦中的一些程式設計思想(如學習C C++的時候),因為前後兩個程式設計思維完全不同。下面分别舉例來說明函數式程式設計中的一些概念和數學中對應概念關系:

注:關于函數式程式設計的特性(features)網上總結有很多,可以在這篇部落格中看到。

1.函數定義

數學中要求函數必須有自變量和因變量,是以在函數式程式設計中,每個函數必須有輸入參數和傳回值。你可以看到F#中的函數不需要顯示地使用關鍵字 return去傳回某個值。是以,那些隻有輸入參數沒有傳回值、隻有傳回值沒有輸入參數或者兩者都沒有的函數在純函數式程式設計中是不存在的。

2.無副作用

數學中對函數的定義有:對于确定的自變量,有且僅有一個因變量與之對應。言外之意就是,隻要輸入不變,那麼輸出一定固定不變。函數式程式設計中的函數也符合該規律,函數的執行既不影響外界也不會被外界影響,隻要參數不變,傳回值一定不變。

3.柯裡化

函數式程式設計中,可以将包含了多個參數的函數轉換成多個包含一個參數的函數。比如對于下面的函數:

let result = func 1 2 //result為3

可以轉換成

func2

let result = (func 1) 2 //result結果也為3,可以去掉括号

可以看到,一個包含兩個參數的函數經過轉換,變成了隻包含一個參數的函數,并且該函數傳回另外一個接收一個參數的函數。最後調用結果不變。這樣做的好處便是:講一個複雜的函數可以分解成多個簡單函數,并且函數調用時可以逐漸進行。

其實同理,在數學中也有類似“柯裡化”的東西。當我們計算f(x,y) = x + y這個函數時,我們可以先将x=1帶入函數,得到的結果為f(1,y) = 1 + y。這個結果顯然是一個關于y的函數,之後我們再将y=2帶入得到的函數中,結果為f(1,2) = 1 + 2。這個分步計算的過程其實就是類似于函數式程式設計中的“柯裡化”。

4.不可變性

數學中我們用符号去表示一個值或者表達式,比如“令x=1”,那麼x就代表1,之後不能再改變。同理,在純函數式程式設計中,不存在“變量”的概念,也沒有“指派”這一說,所有我們之前稱之為“變量”的東西都是辨別符,它僅僅是一個符号,讓它表示一個東西之後不能再改變了。

5.高階函數

在函數式程式設計中,将參數為函數、或者傳回值為函數的這類函數統稱之為“高階函數”,前面已經舉過這樣的例子。在數學中,對一個函數求導函數的過程,其實就是高階函數,原函數經過求導變換後,得到導函數,那麼原函數便是輸入參數,導函數便是傳回值。

混合式程式設計風格

過程式、面向對象再到這篇文章講到的函數式等,這些都是不同地程式設計範式。每種範式都有自己的主導程式設計思想,也就是對待同一個問題思考方式都會不同。很明顯,學會多種範式的程式設計語言對我們思維方式有非常大的好處。

無論是本文中舉例使用到的F#還是Java平台中的Scala,大多數冠名“函數式程式設計語言”的計算機語言都并不是純函數式語言,而是以“函數式” 為側重點,同時兼顧其他程式設計範式。就連曾經主打“面向對象”的C#和Java,現如今也慢慢引入了“函數式程式設計風格”。C#中的委托、匿名方法以及 lambda表達式等等這些,都讓我們在C#中進行函數式程式設計成為可能。如果需要周遊集合找出符合條件的對象,我們以前這樣去做:

foreach(Person p in list)

   if(p.Age > 25)

   {

       //...

現在可以這樣:

list.Where(p => p.Age>25).Select(p => p.Name).toArray();

本篇文章開頭提出的問題,采用C#委托的方式去解決,其實本質上也是函數式思想。由于C#必須遵循OO準則,是以引入委托幫助我們像函數式程式設計那樣去操作每個函數(方法)。

本篇文章介紹有限,并沒有充分說明函數式程式設計的優點,比如它的不可變特性無副作用等有利于并行運算、表達方式更利于人的思維等等。實質上部落客本人并沒有參與過實際的采用函數式語言開發的項目,但是部落客認為函數式思想值得我們每個人去了解、掌握。

繼續閱讀