仙境里的Haskell(之二)

仙境里的Haskell(之二)

Haskell是一種惰性求值(lazy evaluation)的、有自動類型推斷能力的、純(pure)函數式編程語言。

Haskell是一門很容易學的編程語言(相信我),就像數學一樣(`﹏′)。核心語法非常少,那些高深東西都可以從我今天要講的基本的東西里自行推導出來。 有沒有見過數學老師講課的時候忘了一個定理,然後在黑板上直接推導出來? 就這麼簡單 ╮(╯▽╰)╭。

上一篇里,我們準備了學習環境,其實也就是ghci repl和一個基本的編輯器就夠用了。

打開ghci

Prelude> 3 * 4n12n

多好的計算器啊~

在ghci里設置一下顯示類型信息:

Prelude> :set +tnPrelude> 3 * 4n12nit :: Num a => an

設置了 +t後,執行結果後面會跟一行顯示結果的類型,每次退出ghci後要重新設置。Haskell里聲明變數及其類型的語法就如這個例子里的 it :: Num a => a , it是變數名,=>箭頭前面的Num a是個類型約束,表示編譯器現在不確定it是個什麼具體類型,可能是整數,也可能是浮點數等等,只知道它是Num類型類(typeclass)的實例,因為它能做乘法運算。就目前的使用場景來說,知道它是Num也就夠了。類型約束是可選的。比如

Prelude> cncnit :: Charn

這裡編譯器明確的確定了c的類型是Char。 順便說一句,這個it變數是實際可用的

Prelude> itncnit :: Charn

字元串的類型實際上就是字元列表

Prelude> "damotou"n"damotou"nit :: [Char]n

你可以用字元構造一個列表,結果是一樣的

Prelude> [d,m,o,t,o,u]n"dmotou"nit :: [Char]n

列表的所有元素必須是相同類型的。

Prelude> [1,a]n n<interactive>:15:2:n No instance for (Num Char) arising from the literal 『1』n In the expression: 1n In the expression: [1, a]nn In an equation for 『it』: it = [1, a]n

編譯器根據第一個元素,1,推斷這個列表應該是(Num a)類型的,但是a是Char類型,而Char類型不是Num類型的實例,所以編譯不過。

我這麼啰嗦的解釋這個編譯錯誤,是因為用Haskell寫程序大部分時間都在跟編譯錯誤做鬥爭,當程序編譯過了的時候基本上功能就是好的了。所以需要細心的體味編譯器給出的貼心的編譯錯誤~~~

Haskell支持元組(tuple)類型

Prelude> (1,a)n(1,a)nit :: Num t => (t, Char)nnPrelude> ("Haskell",H,1)n("Haskell",H,1)nit :: Num t => ([Char], Char, t)n

元組的類型就是其每個成員的類型。當然你可以把元組放到列表裡

Prelude> [(1,a), (2,b), (3,c)]n[(1,a),(2,b),(3,c)]nit :: Num t => [(t, Char)]n

你就得到了這個元組類型的列表。

有以上這些基本類型基本上就夠我們在仙境里的使用了,哦忘了還有個布爾型:

Prelude> TruenTruenit :: BoolnPrelude> FalsenFalsenit :: BoolnnPrelude> not TruenFalsenit :: BoolnPrelude> True && FalsenFalsenit :: BoolnnPrelude> False || TruenTruenit :: Booln

和大多數語言都是一致的,就不多解釋了。 這下齊活了。

以上都是在程序員不主動寫明類型的情況下,編譯器自動推斷的結果。 實際上我們是可以主動寫明類型的:

ghci不支持這樣的語法,所以你需要建個文件,叫做haskellfariyland.hs,然後在編輯器里輸入這些內容。第一行的模塊聲明在仙境里不重要,所以不解釋了,和文件名一致即可,照抄一下吧。

如果你和我一樣用的是Haskellformac,那麼在playground里輸入x,就可以最右側窗口看到x的執行結果,和在ghci里執行效果是一樣的。你可以用你喜歡的編輯器,比如Atom寫,然後在ghci里:l 文件名, 載入完成後,輸入x回車,就能看到一樣的結果了。

函數式編程,當然是以函數為核心的,所以我們要來學習add函數的4種寫法

第一種:

add :: (Int,Int) -> Intnadd (x,y) = x + yn

雖然Haskell具有很強的類型推斷能力(也許是現有編程語言中最強的),但是習慣上每個模塊的頂層函數定義和值定義都會聲明類型,方便閱讀代碼和生成文檔。而且先寫函數的類型簽名再寫實現也是驅動我們思考程序設計的非常好的方法,這種開發方式被稱為類型驅動開發(Type Driven Development —— 簡稱TDD) (`﹏′)

函數式編程里的函數,就如同數學意義上的函數 y = f(x),函數就是把一個輸入值轉化成一個輸出值的運算。

Prelude> add (1,2)n3nit :: Num a => an

這種寫法的入參是一個二元組,(Int,Int)共同構成了一個值。返回值是Int,所以這個函數的類型就如寫下來的那樣:(Int,Int) -> Int

第二種寫法:

add :: Int -> (Int -> Int)nadd x y = x + yn

有了前面那個例子,現在這個函數簽名應該也很容易理解吧?入參是一個Int,返回值是一個Int -> Int的函數。 返回的這個函數可以再接受第二個參數,最後返回一個Int結果。 所以我們可以這麼調用它:

Prelude> add 1 2n3nit :: Num a => an

可能有聰明的同學已經會想到,如果我只傳一個參數會怎樣?當然是返回給你一個函數咯,你還可以給返回的函數一個名字,比如:

Prelude> let add1 = add 1nadd1 :: Num a => a -> anPrelude> add1 2n3nit :: Num a => an

(再提醒一下,在ghci里定義一個名字需要用let關鍵字,在編輯器里不用。) 這種行為稱為柯里化, 柯里化是非常有用的特性,我們以後會看到更多使用場景。返回的函數叫做部分應用函數(partially applied functions)

第三種寫法就是著名的lambda寫法

add :: Int -> (Int -> Int)nadd = x y -> x + yn

用法和add一樣,實際上他們應該是編譯成一樣的結果的。 lambda寫法可以用來寫匿名函數,比如說這個函數我就想用一次,不想給個名字,那麼可以直接用lambda定義和使用:

Prelude> (x y -> x + y) 1 2n3nit :: Num a => an

其實lambda更主要的作用是寫一個函數作為參數傳給另一個函數,後面講高階函數的時候大家就會知道。

關於第四種寫法。。。大家 現在再回看一下第二種寫法:

add :: Int -> (Int -> Int)nadd x y = x + yn

入參(形參x y)和一個Int對不上號~~~ 所以在學習了lambda寫法後,我們可以寫個對的上號的版本:

add :: Int -> (Int -> Int)nadd x = y -> x + yn

add函數的返回結果是個Int -> Int函數,所以我們在函數實現的=後面就用lambda來定義這個函數。 這個寫法在後面寫一個用來組合函數的combinator的時候很重要,請細心體會~

Prelude> add 1 2 --載入後在ghci里運行n3nit :: Intn

以上總結了add函數的各種寫法,有些很文藝,有些很二X,普通青年的寫法是:

------------參數1---參數2---返回值nnormalAdd :: Int -> Int -> IntnnormalAdd x y = x + yn

但是你要理解這是柯里化的效果。

我們的add函數有點挫,只能加整數,normalAdd 1.0 2.0就會出錯,我們修改一下函數簽名,讓它能加天下可加之物!

add :: Num a => a -> a -> anadd x y = x + yn

前面已經解釋過Num a是類型約束,小寫的字母a是類型參數。和函數參數一樣,類型參數的名字也是你任意起的,叫b、c、d,x、y都無所謂,只是必須小寫字母。大寫字母開頭的代表具體類型,像Int就是個具體類型。 不要把a理解為Any,也不要完全按照Java的介面來理解,以為a被變成了Num。 實際上a捕捉了你調用函數式時給出的真實類型,並且做出了強類型的嚴格限定。a -> a -> a這個簡單的簽名表明第二個參數的類型必須和第一個參數相同,結果類型也一樣。

Prelude> let x = 1 :: Intnx :: IntnPrelude> let y = 2 :: Intny :: IntnPrelude> add x yn3nnit :: Intn nPrelude> let x = 1.0 :: Floatnx :: FloatnPrelude> let y = 2.0 :: Floatny :: FloatnnPrelude> add x yn3.0nit :: Floatn nPrelude> let x = 1 :: Intnx :: IntnnPrelude> let y = 2.0 :: Floatny :: FloatnPrelude> add x yn n<interactive>:18:7:n Couldnt match expected type 『Int』 with actual type 『Float』nn In the second argument of 『add』, namely 『y』n In the expression: add x yn

以上代碼建議大家敲敲試試,並確保完全理解。

然後再介紹add的第5種寫法~~~ Haskell是函數式編程語言,Haskell的世界裡自然到處都是函數。比如 +

Prelude> :t (+)n(+) :: Num a => a -> a -> an

咦,這簽名和我們的add一模一樣啊? 其實這是當然的,你有沒有發現我們的add實際上不就是調用+嗎?只不過有一些規則讓+可以作為中置運算符(infix)來使用.

既然我們的add函數就是標準庫提供的+函數,所以最簡單的實現當然就是

Prelude> let add = (+)nadd :: Num a => a -> a -> annPrelude> add 1 2n3nit :: Num a => an

最後留一個彩蛋,我們的add函數也可以中置使用哦:

Prelude> 1 `add` 2n3nit :: Num a => an

推薦閱讀:

Equational Reasoning的含義是什麼?
Haskell 有什麼缺陷?

TAG:Haskell | 函数式编程 |