Haskell簡明教程(三):Haskell語法
這一系列是我學習Learn You a Haskell For Great Good
之後,總結,編寫的學習筆記。
這個系列主要分為五個部分:
- ofollow,noindex" target="_blank">從遞迴說起
- 從命令式語言進行抽象
- Haskell進階:Monoid, Applicative, Monad
- 實戰:Haskell和JSON
簡介
但凡談到Haskell,都會有這麼一句話,Haskell is an advanced, purely functional programming language.
,這是其官網
所標榜的。
至於什麼是純(pure),什麼是函式式,什麼是高等/高階。我們將會在之後的文章裡一一看到。
安裝GHC
如同Go需要編譯器,Python需要直譯器一樣,Haskell也許要一個程式讓它能跑起來,以前很多教學都用hugs ,不過我們用GHC 。
那麼如何安裝呢?我假設本文的讀者都是 Linux 使用者。
sudo pacman -S ghc sudo apt-get install ghc
找準自己的發行版執行對應的命令,就可以了。安裝好之後,在命令列中輸入ghci
會進入類似以下的介面:
ghci GHCi, version 8.0.2: http://www.haskell.org/ghc/:? for help Prelude>
我們常用的幾個命令就是:
- ghci: 互動式的Haskell直譯器,類似於 python 命令
- runhaskell: 解釋性的執行Haskell程式
- ghc: 編譯Haskell程式
第一個Haskell程式
我們先來看一個Haskell程式,蹭蹭臉熟。
sayHello :: String -> String sayHello = (++) "Hello! " main = do fmap sayHello getLine >>= putStrLn main
儲存成Hello.hs
,然後執行一下:
$ runhaskell Hello.hs # 或者ghc -dynamic Hello.hs 然後 ./Hello world Hello! world Jhon Hello! Jhon Hello.hs: <stdin>: hGetLine: end of file
嗯,看樣子我們可以看到:
>>=
對於第二點,不用擔心,以後還會看到更多的 :)
型別系統
普通的型別
看到了上面示例裡的第一行嗎?sayHello :: String -> String
,Haskell中有很多類似的程式碼,叫做型別簽名
。
等我們對Haskell的型別系統稍微熟悉之後,我們便可以獲得這樣一種能力:根據型別簽名就可以推斷出這個函式大概是做什麼用的,再加上比較好的函式命名,我們就可以不看實現便知道函式的作用。
不過在此之前我們需要先熟悉一些常見的型別:
-
Char 字元,例如
'a'
-
[] 列表,例如
[1, 2, 3]
-
String 字串,也是[Char]的別名,例如
Hello
其實是H:e:l:l:o:[]
- Int 整數,通常是32位,所以取值範圍是 -2147483648 ~ 2147483647
-
Bool 布林值,例如
True
和False
- Integer 整數,沒有取值範圍,其範圍只取決於記憶體大小
- Float 浮點數
- Double 雙精度浮點數
如果我們不確定一個東西是什麼型別怎麼辦呢?開啟ghci
,然後輸入:t
,ghci便會告訴我們:
Prelude> :t "Hello" "Hello" :: [Char] Prelude> :t 1 1 :: Num t => t Prelude> :t 1.3 1.3 :: Fractional t => t Prelude> :t 1.3 :: Double 1.3 :: Double :: Double Prelude> :t True True :: Bool
通常ghci都會給我們一個比較寬泛的型別,但是我們可以通過加入:: <型別>
來指定一個更加具體的型別。
函式的型別
函式也有型別?對啊,沒毛病,例如 Golang 裡我們也要把函式的簽名寫出來,Haskell也是如此,但是我們可以省略,因為Haskell有一個 強大的功能,叫做型別推斷 。
我們來看看最開始我們的函式型別sayHello :: String -> String
,其中->
意思是給一個 String,得到一個 String。就算是sayHelloTo :: String -> String -> String
我們也可以這樣讀,儘管Haskell中所有的函式都只有一個引數,但是為了簡便,我們知道就好。
型別的共同特徵-型別的型別?TypeClass
在第二篇 中我們知道了什麼是抽象。那麼我們想想,Haskell的型別中是否也能找出共同點呢?是否也能抽象出一系列的介面呢?是否也有類似介面的概念呢?
有。就是我們要講的TypeClass。例如,Char
和Int
都是可以比較是否相等的,那麼我們可以抽象一個介面叫做Equal
,例如Int
和Bool
都是有取值範圍限制的,我們可以抽象一個介面叫做Bounded
。
不過Haskell中的介面不叫interface
,而是叫做class
。沒錯,叫做class
,在此需要重申一遍,以防止大家和麵向物件程式設計中的class
關鍵字搞混。
我們看看Equal
在Haskell中是怎麼定義的。
Prelude> :i Eq class Eq a where (==) :: a -> a -> Bool (/=) :: a -> a -> Bool {-# MINIMAL (==) | (/=) #-} -- Defined in ‘GHC.Classes’ instance Eq a => Eq [a] -- Defined in ‘GHC.Classes’ instance Eq Word -- Defined in ‘GHC.Classes’ instance Eq Ordering -- Defined in ‘GHC.Classes’ instance Eq Int -- Defined in ‘GHC.Classes’ instance Eq Float -- Defined in ‘GHC.Classes’ instance Eq Double -- Defined in ‘GHC.Classes’ instance Eq Char -- Defined in ‘GHC.Classes’ instance Eq Bool -- Defined in ‘GHC.Classes’ instance (Eq a, Eq b, Eq c, Eq d, Eq e, Eq f, Eq g, Eq h, Eq i, Eq j, Eq k, Eq l, Eq m, Eq n, Eq o) => Eq (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o) -- Defined in ‘GHC.Classes’ instance (Eq a, Eq b, Eq c, Eq d, Eq e, Eq f, Eq g, Eq h, Eq i, Eq j, Eq k, Eq l, Eq m, Eq n) => Eq (a, b, c, d, e, f, g, h, i, j, k, l, m, n) -- Defined in ‘GHC.Classes’ instance (Eq a, Eq b, Eq c, Eq d, Eq e, Eq f, Eq g, Eq h, Eq i, Eq j, Eq k, Eq l, Eq m) => Eq (a, b, c, d, e, f, g, h, i, j, k, l, m) -- Defined in ‘GHC.Classes’ instance (Eq a, Eq b, Eq c, Eq d, Eq e, Eq f, Eq g, Eq h, Eq i, Eq j, Eq k, Eq l) => Eq (a, b, c, d, e, f, g, h, i, j, k, l) -- Defined in ‘GHC.Classes’ instance (Eq a, Eq b, Eq c, Eq d, Eq e, Eq f, Eq g, Eq h, Eq i, Eq j, Eq k) => Eq (a, b, c, d, e, f, g, h, i, j, k) -- Defined in ‘GHC.Classes’ instance (Eq a, Eq b, Eq c, Eq d, Eq e, Eq f, Eq g, Eq h, Eq i, Eq j) => Eq (a, b, c, d, e, f, g, h, i, j) -- Defined in ‘GHC.Classes’ instance (Eq a, Eq b, Eq c, Eq d, Eq e, Eq f, Eq g, Eq h, Eq i) => Eq (a, b, c, d, e, f, g, h, i) -- Defined in ‘GHC.Classes’ instance (Eq a, Eq b, Eq c, Eq d, Eq e, Eq f, Eq g, Eq h) => Eq (a, b, c, d, e, f, g, h) -- Defined in ‘GHC.Classes’ instance (Eq a, Eq b, Eq c, Eq d, Eq e, Eq f, Eq g) => Eq (a, b, c, d, e, f, g) -- Defined in ‘GHC.Classes’ instance (Eq a, Eq b, Eq c, Eq d, Eq e, Eq f) => Eq (a, b, c, d, e, f) -- Defined in ‘GHC.Classes’ instance (Eq a, Eq b, Eq c, Eq d, Eq e) => Eq (a, b, c, d, e) -- Defined in ‘GHC.Classes’ instance (Eq a, Eq b, Eq c, Eq d) => Eq (a, b, c, d) -- Defined in ‘GHC.Classes’ instance (Eq a, Eq b, Eq c) => Eq (a, b, c) -- Defined in ‘GHC.Classes’ instance (Eq a, Eq b) => Eq (a, b) -- Defined in ‘GHC.Classes’ instance Eq () -- Defined in ‘GHC.Classes’ instance (Eq b, Eq a) => Eq (Either a b) -- Defined in ‘Data.Either’ instance Eq Integer -- Defined in ‘integer-gmp-1.0.0.1:GHC.Integer.Type’ instance Eq a => Eq (Maybe a) -- Defined in ‘GHC.Base’
wow,原來在ghci
中輸入:i
就可以知道了,應該還有很多其他功能,我們輸入:help
,然後仔細看看,絕對會受益匪淺。
我們把Eq
的定義抽出來看:
class Eq a where (==) :: a -> a -> Bool (/=) :: a -> a -> Bool
a?a是什麼?在Haskell的定義中,a常常用來代表不管是什麼型別,也可以通過加型別限制的方式指定具體某種型別,為什麼用a呢?其實用b,用c都可以,如果a不夠表達,就可以用abcdefg等等等等了,只不過預設的約定俗成的就是a罷了。
我們來看看這裡(==) :: a -> a -> Bool
,(==)
取任意型別,再取任意型別,然後返回一個Bool。沒錯,這就是是否相等的定義。
那我們怎麼知道某個具體型別是否實現了這個介面呢?事實上,為了實現這個介面,我們需要在具體型別上這樣寫:instance Eq Int where...
。這優點類似Java中的implemented
關鍵字。但是對於這一點,我們暫時先按下不表。
我們先繼續以瀏覽更多東西,增加廣度為先。接下來我們看看長間的資料結構。
List, Tuple
啊,學過Python的朋友們都知道List和Tuple的區別,在Haskell中也是一樣, 只不過List中只能放同一種資料型別的資料,說起來有點像Golang中的slice。而Tuple則更像是Python中的Tuple了,長度固定,可以放多種型別在其中。
List用[]
表示而 Tuple用()
表示。
Prelude> :t [1, 2, 3] [1, 2, 3] :: Num t => [t] Prelude> :t (1, 2, 3) (1, 2, 3) :: (Num t, Num t1, Num t2) => (t2, t1, t)
Haskell中的List有語法糖叫做List Comprehension,其實Python中也有,看看Python的:
>>> [i for i in range(10)] [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] >>> [i + 1 for i in range(10)] [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] >>> [i + j for i in range(10) for j in range(2)] [0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10] >>> [i + j for i in range(10) for j in range(2) if i % 2 == 0] [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
而Haskell版本的:
Prelude> [i | i <- [0..9]] [0,1,2,3,4,5,6,7,8,9] Prelude> [i + 1 | i <- [0..9]] [1,2,3,4,5,6,7,8,9,10] Prelude> [i + j | i <- [0..9], j <- [0..1]] [0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10] Prelude> [i + j | i <- [0..9], j <- [0..1], i `mod` 2 == 0] [0,1,2,3,4,5,6,7,8,9]
是不是感覺很像?
模式匹配
我們在命令式語言中接觸的最多的表示分支的方式,估計就是if ... else ...
了。
只不過在不同的語言中,表述也不一樣,表現形式也略有不同,例如if...then...
,
例如switch...case...
。其實就是決策樹的分支表現形式。在Haskell中其實也有
這樣的東西,我們先來看第一種,模式匹配,為了好好的和模式匹配玩耍,我們要先和
List玩耍。我們來看看List的幾個常見的操作:head
,tail
。
先看看型別:
Prelude> :t head head :: [a] -> a Prelude> :t tail tail :: [a] -> [a]
我們就這樣猜測吧:head
是取出List中的第一個元素,而tail
是取出除了第一個元素
的其他所有剩餘元素。
我們新建MyList.hs
然後把型別填進去:
head :: [a] -> a tail :: [a] -> [a]
然後填上實現:
head :: [a] -> a head [] = head (x:_) = x tail :: [a] -> [a] tail [] = [] tail (_:xs) = xs
然後我們去ghci裡執行一下:
Prelude> :l MyList.hs [1 of 1] Compiling Main( MyList.hs, interpreted ) Ok, modules loaded: Main. *Main> head [] <interactive>:2:1: error: Ambiguous occurrence ‘head’ It could refer to either ‘Prelude.head’, imported from ‘Prelude’ at MyList.hs:1:1 (and originally defined in ‘GHC.List’) or ‘Main.head’, defined at MyList.hs:2:1 *Main> head [1, 2, 3] <interactive>:3:1: error: Ambiguous occurrence ‘head’ It could refer to either ‘Prelude.head’, imported from ‘Prelude’ at MyList.hs:1:1 (and originally defined in ‘GHC.List’) or ‘Main.head’, defined at MyList.hs:2:1 *Main>
嗯。。。看報錯是說我們定義的head函式和標準庫Prelude預先載入定義的名字衝突了,
如果寫過Python我們可以參考看有沒有類似import as
的功能,所以我們搜一下:
http://lmgtfy.com/?q=haskell+import+as
不過我試了一下,在ghci中這麼幹並不行。所以只能用一種比較老土的方法,就是把函式 名字給改了。
Prelude> :l MyList.hs [1 of 1] Compiling Main( MyList.hs, interpreted ) Ok, modules loaded: Main. *Main> myHead [] *** Exception: bad operation on empty list CallStack (from HasCallStack): error, called at MyList.hs:2:13 in main:Main *Main> myHead [1, 2, 3, 4] 1 *Main> myTail [1, 2, 3, 4] [2,3,4] *Main>
要注意Haskell和Golang中有一點比較相像,那就是首字元大小寫有不同的意義。
模式匹配就是把某個引數,強行拆解,看能不能匹配上該行的拆解形式,如果能,那麼就執行 後面的程式碼,要不然就跳到下一個模式裡去。
if, else, let, where, guard
在平時的程式設計中我們經常要乾的事情就是做判斷,如果那麼,要不然就怎樣。那麼為什麼 Haskell作為一門函數語言程式設計不能脫離這些呢?說好的函數語言程式設計和指令式程式設計不一樣呢? 是啊,函數語言程式設計應該是描述這是什麼,而不是具體怎麼做。其實這兩者並不衝突,例如 模式匹配其實也是一種分支,不過模式匹配要更加強大。接下來我們看看Haskell中的分支 是怎麼表達的。
biggerThan100 :: Int -> Bool biggerThan100 x = if x > 100 then True else False
和我們往日所寫的其實差不多,所以就不贅述了。
接下來我們介紹一種新的寫法,guard。有中文翻譯成守衛表示式,不過我還是更喜歡直接 用英文。把上面的改成guard會是這樣:
biggerThan100' :: Int -> Bool biggerThan100' x | x > 100 = True | otherwise = False
有兩點需要注意,第一,最後一個引數後面是|
而不是=
,第二,otherwise相當於
switch語句裡的default
。
平時我們定義函式和變數都是在最外層定義,然後函式裡引用,那麼有沒有更小的作用域呢?
有,我們看看let
和where
的示例。
printHello :: String -> String printHello x = let finalPrint = "Hello! " ++ x in finalPrint printHello' :: String -> String printHello' x = finalPrint where finalPrint = "Hello! " ++ x
執行一下:
Prelude> :l LetWhere.hs [1 of 1] Compiling Main( LetWhere.hs, interpreted ) Ok, modules loaded: Main. *Main> printHello "World" "Hello! World" *Main> printHello' "World" "Hello! World" *Main>
Haskell的pure所在和如何遞迴的寫程式
有沒有發現到目前為止我們都沒有真正的簡單的原始的寫一個HelloWorld
出來呢?而是
一直在寫一些String -> String
啊Char -> String -> Bool
的函式呢?為什麼Haskell中
這麼多這樣的函式(最少到目前我們接觸的為止)?因為Haskell有一個很大的特點是pure。
什麼是純函式?就是無論在什麼情況下,只要給定輸入,那麼輸出一定是同樣的。那什麼是
不純的函式?舉個例子,網路IO,硬碟IO,標準輸出也是。我們暫時可以這樣想像:凡是
和現實世界接觸的東西,都是不純的,凡是可以抽象成數學理論可以解釋的,都是純的。
Haskell把不純的東西也做了抽象,叫做Monad,你可以把它理解成一個盒子,就是我們以前 所說的黑盒子,它把不純的東西包在裡面,並且提供一些介面來操作它。這些我們會在下一篇 看到。
接下來我們將要看看如何遞迴的寫程式,和第一篇一樣,我們簡單地來看看如何遞迴的遍歷
列表。現在我們有一個列表[1, 3, 2, 5, 6]
,我們想要將偶數過濾掉,只留下奇數。
但是我們不能用for迴圈。列表,如果用遞迴的方式去看,就是一個表頭+一個列表,拿[1, 3, 2, 5, 6]
來看,就是1
和[3, 2, 5, 6]
。在Haskell中我們可以這樣寫:1:[3, 2, 5, 6]
,其中:
是列表連線符,讀作Cons
。我們可以看看它的型別:
Prelude> :t (:) (:) :: a -> [a] -> [a]
很顯然,要過濾偶數,我們可以這樣寫:
filterEven :: [Int] -> [Int] filterEven [] = [] filterEven (x:xs) = if x `mod` 2 == 0 then filterEven xs else x:filterEven xs filterEven' :: [Int] -> [Int] filterEven' [] = [] filterEven' (x:xs) | x `mod` 2 == 0 = filterEven' xs | otherwise = x:filterEven' xs -- 把判斷是否是偶數抽離出來 isEven :: Int -> Bool isEven x = x `mod` 2 == 0 filterEven'' :: [Int] -> [Int] filterEven'' [] = [] filterEven'' (x:xs) | isEven x = filterEven'' xs | otherwise = x:filterEven'' xs
執行一下:
Prelude> :l FilterEven.hs [1 of 1] Compiling Main( FilterEven.hs, interpreted ) Ok, modules loaded: Main. *Main> filterEven [1, 3, 2, 5, 6] [1,3,5] *Main> filterEven' [1, 3, 2, 5, 6] [1,3,5] *Main> filterEven'' [1, 3, 2, 5, 6] [1,3,5] *Main>
我們其實就是順著遞迴的思路去寫程式碼,如何過濾偶數呢?就是我們把列表看成是一個 元素+一個列表的結構,我們每次都看當前元素是否是偶數,如果是,那麼就忽略,直接 考慮下一個列表,要不然的話,我們要把現在這個元素追加到最前面,然後才開始考慮下一個 列表。
此外,在寫示例的時候我犯了一個錯誤,就是忘記了寫邊界條件:
filterEven :: [Int] -> [Int] filterEven (x:xs) = if x `mod` 2 == 0 then filterEven xs else x:filterEven xs
結果執行就會報錯:
Prelude> :l FilterEven.hs [1 of 1] Compiling Main( FilterEven.hs, interpreted ) Ok, modules loaded: Main. *Main> filterEven [1, 3, 2, 5, 6] [1,3,5*** Exception: FilterEven.hs:2:1-77: Non-exhaustive patterns in function filterEven *Main>
為什麼呢?初看你可能覺得這是因為ghci就像python直譯器一樣,執行到了對應的報錯然後才
處理。這麼說的話,似乎也對,其實真正的原因是因為Haskell是一門惰性語言,英文的說法
叫做lazy
。lazy
?
def iter_array(array): for i in array: yield i
什麼叫做lazy呢?就是延遲計算,在Python中可以是迭代器,也可以是重寫__call__
來
造成延遲計算,或者類似的手法。其核心思想就是,並不是一開始就計算好,而是等到真正
要用的時候才去計算。
高階函式
Haskell functions can take functions as parameters and return functions as return values. A function that does either of those is called a higher order function.
所謂的高階函式其實就是能接受函式作為引數,或者返回一個函式作為結果的函式。舉個例子,map函式,map的型別為:
Prelude> :t map map :: (a -> b) -> [a] -> [b]
map接受一個函式a->b
,然後接受一個列表[a]
,返回一個列表[b]
。我們可以看看示例:
Prelude> map (+1) [1..5] [2,3,4,5,6]
模組
什麼是模組呢?模組就是一組函式或者資料型別。是一種集合,Haskell中的模組用module
關鍵字宣告。我們常用的模組例如預先匯入的Prelude
,例如Data.List
,Data.Map
等。我們可以把上面的 MyList.hs 包裝成一個模組。
module MyList ( myInit , myTail ) where myInit :: [a] -> a myInit [] = error "cannot operate on empty list" myInit (x:xs) = x myTail :: [a] -> [a] myTail [] = error "cannot operate on empty list" myTail (x:xs) = xs
然後在匯入的時候,可以有如下寫法:
import XMonad.Util.Run (spawnPipe) import qualified Data.Map as M
第一個是類似於Python中from xxx import yyy
的寫法,只引入模組中的部分。
第二個是類似於Python中import xxx as yyy
的寫法,引入模組,但是建立別名。
自己建立型別和TypeClass
Haskell中還有一個關鍵字是data
。是用來建立我們的型別的。
data Bool = False | True
左邊的Bool
是型別(type),右邊的用|
分隔的叫做value constructors
。有的需要帶值,有的不需要。例如上面的Bool就不需要,而Maybe則需要:
data Maybe a = Nothing | Just a
此處左邊的a叫做type parameter
。而Maybe叫做type constructor
。為什麼又有一個新名字而不是也叫type呢?因為Maybe
不是一個具體的型別,而像Maybe Int
才是。
Prelude> :t Nothing Nothing :: Maybe a Prelude> :t Just 1 Just 1 :: Num a => Maybe a Prelude> :t Just "haha" Just "haha" :: Maybe [Char]
可能看到這裡還比較迷糊,但是在之後我們看到Monad的時候,就知道為什麼要把這些分得這麼清楚了。把有共同特徵的一類事物提取出來就是抽象。
對於具體型別例如Bool
,是一種型別。而像Maybe
這樣需要接受一個具體型別才能產生出一個具體型別的型別,就是另一種型別了。
我們可以把Maybe看作是一個盒子,這個盒子需要填充一個東西,才算是一個有意義的盒子,否則 知識一個空蕩蕩的i空盒子。對於這樣的想象成盒子的想法,我們在後面學習Monad的時候還會用到。
總結
在這一篇中我們簡略的過了一下Haskell的基本語法,學習一門新語言首先要把基本的語法搞懂,然後更重要的是拿基本語法開始開發, 然後不斷的搜尋,寫,看資料,查文件,寫。這樣才能更快的掌握一門語言。
參考資料: