每個人都應該懂點函數語言程式設計

周見智發表於2015-08-27

目錄

一個問題

假設現在我們需要開發一個繪製數學函式平面影像(一元)的工具庫,可以提供繪製各種函式圖形的功能,比如直線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)
{
    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種不同函式就需要定義N個介面。很明顯不可能這樣去做。

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

如果我們換一種方式去思考,既然是給函式繪製影像,為什麼要將它們的係數作為引數傳遞而不直接將函式作為引數傳給介面呢?是的,沒錯,要繪製什麼函式影像,那麼我們直接將該函式作為引數傳遞給介面。由於C#中委託就是對方法(函式,這裡姑且不討論兩者的區別)的一個封裝,那麼C#中使用委託實現如下:

public delegate double Function2BeDrawed(double x);
//繪製函式影像
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;}); //建立直線函式
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 x = 1;
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 func x y = x + y
let result = func 1 2  //result為3

可以轉換成

let func x =
   let func2 = fun y -> x + y
   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準則,所以引入委託幫助我們像函數語言程式設計那樣去操作每個函式(方法)。

本篇文章介紹有限,並沒有充分說明函數語言程式設計的優點,比如它的不可變特性無副作用等有利於並行運算、表達方式更利於人的思維等等。實質上博主本人並沒有參與過實際的採用函式式語言開發的專案,但是博主認為函式式思想值得我們每個人去了解、掌握。(本文程式碼手敲未驗證,如有拼寫錯誤見諒)

 

相關文章