1. 程式人生 > >每個人都應該懂點函數式編程

每個人都應該懂點函數式編程

函數作為參數 span 編程風格 定義函數 了解 msd 出現 函數定義 繪制直線

目錄

  • 一個問題
  • 函數式編程中的函數
  • 數學與函數式編程
  • 混合式編程風格

一個問題

假設現在我們需要開發一個繪制數學函數平面圖像(一元)的工具庫,可以提供繪制各種函數圖形的功能,比如直線f(x)=ax+b、拋物線f(x)=ax2+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準則,所以引入委托幫助我們像函數式編程那樣去操作每個函數(方法)。

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

每個人都應該懂點函數式編程