來試試Haskell:可以秘密的

還早我們先熬鍋咖喱

為什麼是做咖喱?呃……因為我餓了,還因為Haskell的Curried Functions,現在要說的就是它。說起為什麼要說這個Curried Functions,完全是因為我們在前面初次接觸Haskell函數,并說函數聲明的方式的時候,我曾一言帶過,只是說了在聲明中變量使用“->”進行連接。我們現在,要說的Curried Functions,就是對Haskell變量連接的解釋了。

說起來,其實也不難理解。Haskell是純函數式的編程語言,函數本身對於Haskell也是一種變量,對於多變量的函數,我們其實就可以這麼理解,舉個例子來說:“f :: a -> a -> a”這個函數。用Haskell的角度來理解就是一個這樣“a -> a”的函數傳給了一個類似“f :: g -> a”的函數中來做變量g。換句話說,Haskell的函數都是單變量的,所以,函數本身也可以作為參數,返回也可以是一個函數,這就使得多變量的函數,在Haskell中,也可以理解為一個函數套一個函數的樣子。比如,下面這兩個語句:

Prelude> max 45 55
55
Prelude> ( max 45 ) 55
55

很明顯的,這兩個語句是一樣的結果,而且沒有報錯,第一個語句我們很容易理解。第二個其實就是說:“max 45”這個部分先返回一個45和誰更大的函數,再然後再傳入55這個變量,就能得到函數的結果了。更極端的,我們有這麼一個例子:

Prelude> let g = max 254
Prelude> g 456
456

這也是完全正確的語句,並且由此我們定義了一個和254比較大小的函數g,不過用Haskell的視角來說,g是一種函數變量,表達和254比較大小的結果。而且我們也可以把g使用”let“定義為別的數組或者某一個數字。所以,為了更清晰的解釋這個Haskell中的變量的含義,我們換用數學來理解,就是:態射(Morphism)(看不懂態射的含義也無所謂,反正只需要知道,Haskell的變量既可以是普通的的數值、數組,也可以是函數,就可以了)。

於是,我們再來重新理解一下Haskell的函數,就是:表達兩個域之間的關係,可能定義域是由另一組態射來定義,同理,到達域(Codomain)也可能是一個態射。所以,很容易的我們就可以知道,復合函數在Haskell裡面,是由定義變量的特點基礎上直接延伸出來的必然產物。

那什麼是復合函數呢?顯然,最簡單的就是類似:“f + f”類型的函數(f為一個函數),在數學上可能的一種表示就是:“ \( f \circ f \)”這樣的。我們用Haskell實現這樣一個函數:把F(x)F(2*x)的值加起來的函數:

compoFunc :: (Num a) => (a -> a) -> a -> a
compoFunc f x = (f x) + (f (x*2))

很容易就可以理解,上面定義的這個compoFunc函數是需要F(x)是一個Num->Num類型的單一自變量和單一因變量的函數,這個函數就是需要傳入compoFunc的變量之一。也就是,compoFunc是一個高階的函數,它不能單獨完成計算,但它定義了一個更抽象的映射,且把函數作為一種變量結構來處理。這就是Haskell的高階函數了。復合函數則使用“.”運算符來表示和計算,我們前面說了,Haskell中的變量是就是態射,數值是態射,函數自然也是,函數的復合自然作為基本運算的一部分,Haskell也做得很好了。我們簡單看幾個例子:

Prelude> (sin . cos) 23
-0.5079756592046831
Prelude> (sin .max) 23 45
 
<interactive>:3:1:
	No instance for (Floating (a0 -> a0)) arising from a use of 'it'
	In a stmt of an interactive GHCi command: print it

這裡出現了一個錯誤,就是在語句“(sin .max) 23 45”這裡。下面的錯誤說明得很清楚,Haskell不知道應該怎麼分配後面的變量,或者說,復合函數運算符“.”只能處理只有一個變量的函數之間的復合運算。我們想把max和sin復合在一起,就需要我們自己來寫函數處理了,我們簡單寫一個處理的函數:

comenTwoFunc :: (a -> a) -> (a -> a-> a) -> a -> a -> a
comenTwoFunc f g x y = f( g x y )

把這個函數保存為“comenTwoFunc.hs”,然後我們在GHCi中調用測試一下:

Prelude> :l comenTwoFunc.hs
[1 of 1] Compiling Main			( comenTwoFunc.hs, interpreted )
Ok, modules loaded: Main.
*Main> comenTwoFunc sin max 23 45
0.8509035245341184

這樣計算成功了,但是,也是存在問題的,就是第二個函數只能是有兩個自變量的函數這個限制。

一切都可以是函數嘛

高階函數、復合函數,在Haskell里都可以相對容易的處理,那麼我們就要考慮這樣一個事情了,就是在命令式語言中的控制語句在函數式語言中要怎麼處理。我們前面說到過,在構建函數的時候,if-else結構可以使用模式匹配來處理。那麼,以for為代表的循環結構呢?

一般的,對於數組,我們要遍歷數組的每一個元素,在命令語言中,使用forwhile來遍歷,而在函數式語言中,因為沒有循環這個概念,我認為Haskell使用了遞歸來替代循環的概念和處理方法,但這也不是絕對的,因為還有幾個神奇的函數,能很有效的解決一些問題,而不必我們手動來寫出遞歸函數再處理。現在我們就來逐一看看這些慢讚的Haskell自帶函數的使用好了~

map:如果你用過其他函數語言,那麼對於這個經典的map函數一定不會陌生。map函數的最重要的作用就是在一個集合上遍歷使用某一個函數。乍聽起來,似乎也蠻高級的樣子,其實我覺得就是函數式語言處理循環的方法。比如,對一個數組中的每一個數取自然對數:

Prelude> map log [2,3,4,5,6,7,8,9,10]

filter:這個函數的作用就是把符合條件的元素從集合中取出來。同樣是要遍歷整個集合,只是處理的方式上略有不同。舉個例子:

Prelude> filter (>17) [21,3,4,15,16,17,18,29,10]

上述語句把集合{21,3,4,15,16,17,18,29,10}中大於17的元素都選了出來。似乎這是一個分揀集合的好方法,尤其是在集合十分大的時候。
fold:說fold,其實是代表了兩個函數:foldrfoldl。他們的作用特點與map或者filter也是有相似之處的,因為也是要遍歷集合的每一個元素。但不一樣的地方是,它把計算的結果累計直到遍歷完成整個集合。最簡單的例子,就是,把集合所有元素加起來:

Prelude> foldr (+) 9 [1..8]
45
Prelude> foldl (+) 1 [2..9]
45
Prelude> foldr (+) 0 [1..9]
45
Prelude> foldl (+) 0 [1..9]
45

從上面四個語句,我們可以看到,foldl是從左向右去遍歷,foldr是從右向左去遍歷集合。而且也能看到fold類型函數的第二個變量就是記錄積累計算結果的初始值。
scan:這也是一類函數,和fold類型函數類似,也有scanlscanr兩個。scan類函數是把計算的所有中間值都記錄下來,而fold只給出結果。對比上面fold的例子:

Prelude> scanr (+) 9 [1..8]
[45,44,42,39,35,30,24,17,9]
Prelude> scanl (+) 1 [2..9]
[1,3,6,10,15,21,28,36,45]
Prelude> scanr (+) 0 [1..9]
[45,44,42,39,35,30,24,17,9,0]
Prelude> scanl (+) 0 [1..9]
[0,1,3,6,10,15,21,28,36,45]

以上就是這幾個神奇函數的介紹了,通過這幾個函數再加上模式匹配,我們幾乎不再需要循環這種控制語句了。但是,這還不算完結,因為還有一種特殊的東西。

不悄悄的但秘密的

不知道還記不記得,我們之前在設計素數計算函數的時候,在其中使用了這樣一段語句:“filter (\y -> mod x y == 0) [2..x]”。現在我們應該已經大概可以看懂這句話的意義了:就是滿足“\y -> mod x y == 0”條件的包含在集合{2..x}的元素被挑選出來。這個條件“\y -> mod x y == 0”,其實也是一個函數,或者準確的說是一個態射,它定義了一個x被y整除的映射關係。這個奇怪樣子的函數,在Haskell裡面,其實就是一個匿名函數。

Haskell裡面的匿名函數由“\”作為開始,緊跟著的就是匿名函數的自變量,變量和計算公式之間使用“->”連接。說起來,是很容易理解的啦~使用上,也是很容易的。不過,我們從這一刻開始就要很仔細的考慮我們面對的每一個函數或者我們要很仔細的面對每一個變量。

我們使用前面的“comenTwoFunc”這個函數,來進一步理解一下匿名函數好了~我們在GHCi中加載函數“comenTwoFunc”之後,運行這樣的語句:

*Main> comenTwoFunc (\x -> 3*x - x**2) (\x y -> x**2 - y**2) 33 44
-719950.0
*Main> comenTwoFunc (\x -> 3*x - x**2) (\x y -> x + y) 33 44
-5698.0
*Main> comenTwoFunc (\x -> 3*x - x**2) (+) 33 44
-5698.0

最後兩個語句是需要好好注意的,因為我們可以看到,我們定義匿名函數“\x y -> x + y”與直接使用Curried Function是一樣的效果。所以,從代碼的簡潔性考慮,代碼越簡單直觀,越好,所以不是所有時候都需要使用匿名函數。因為匿名函數雖然只在這裡起作用,但它卻並非默默無聞。他的作用結果是會直接反應出來的。

PS:

最近真的好忙啊……各種無力……寫這個的進程一再拖延……好吧!還是要用更多時間來寫,來總結~時間真的不是想擠就能擠得出來的……_(:зゝ∠)_

sidneyzhang

#iPhone #Mathematiker #male #Skorpion #Wordpress #Chineser #WOWer #MacBookPro #CSI #TBBT #LaTeX #Linux #Trading #lover@jossitixzhao

You may also like...

发表评论

电子邮件地址不会被公开。 必填项已用*标注