Haskell簡介[1]--Haskell簡介(

balbit說模考前應該耍廢,所以我就暫停做題然後熬夜打了這篇。(等等好像哪裡不太對

因為想要趕快進入有趣的地方,所以把入門的東西通通塞進來了,不過對初學者而言這樣應該就足夠玩很久了?
預設是下一篇把拉里拉雜的函式語法講完,以及在Prelude[1]裡面的各種神奇資料結構。
之後就可以開始玩有趣的東西了><
不過依照一年一篇的速度不知道那是什麼時候了Q

環境設定

想在本地編譯Haskell程式請安裝 ghc。內有編譯器(ghc)以及直譯器(ghci)。

型別

Haskell是個強型別語言,以下是幾種常見的型別:

  • Int: 有界的整數。一般是\(-2^{31}\) ~ \(2^{31}-1\)。
  • Integer: 大數
  • Char: 字元
  • Float Double: 浮點數
  • Bool: 布林值(TrueFalse
  • (a, b): tuple,其中a,b都是型別。當然也有更長的tuple。
  • (): 可以視為長度為\(0\)的tuple,也可以視為void
  • [a]: 串列(list)。只支援\(O(k)\) 存取第\(k\)個元素。
  • String: 字串。事實上是[Char]的語法糖。
  • a -> b:從型別a打到型別b的函數。

以上應該包含了大部份預設宣告的型別。而在下下一篇會提到,如何在haskell中定義型別。
若想在ghci中看到某個expression的型別
可以打:t Expression
直譯器會顯示Expression :: Type

型別類別(Typeclass)

當只打一個數字2時,Haskell並不知道它代表的是Int還是Integer或甚至是Float。所以Haskell引入型別類別的概念。透過型別類別可以在強型別語言中達到重載(overload)的效果。
以下舉例一些預設宣告的型別類別:

  • Num:可以做加法減法乘法的東西
  • Show:可以變成字串印出的東西
  • Fractional:可以做除法的東西
  • Integral:整數
  • Floating:浮點數
  • Ord:可以比大小的東西
  • Real:又是Num又是Ord的東西

讀者應該發現了,不同的型別類別有著從屬的關係。
詳細的從屬關係可以看這張haskell官網的圖片:

這些就是所有預設宣告的型別與型別類別。
尚未提及的型別與型別類別應該會在下幾篇提到。
以下來看幾個ghci的範例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Prelude> :t 2
2 :: Num t => t

Prelude> :t 4/2
4/2 :: Fractional t => t

Prelude> :t (2::Integer)
(2::Integer) :: Integer

Prelude> :t (2 :: Real a => a)
(2 :: Real a => a) :: Real a => a

Prelude> :t max
max :: Ord a => a -> a -> a

Prelude> :t max 1 2
max 1 2 :: (Num a, Ord a) => a

Num t => t表示2的型別是t,且t必須是Num的一種
4/2不但是一個Num,它還要是一個可以做除法的Fractional
當想要直接指定型別時,可以使用::接在表達式的後面。
max的型別是a -> a -> a,表示他是一個接收兩個a並回傳一個a的函數(請見 前篇 的Currying),並要求a要是可以比大小的Ord
而當一個型別被要求屬於不只一個型別類別時,會用逗點隔開並用小括號括住。

常見函式介紹

記得去ghci把每個都用用看喔,也可以看看他看他的型別,看這些函式是定義在哪些型別/型別類別上

運算相關

  • (+) (-) (*):加、減、乘法。
  • (/):小數除法。
  • div:整數除法。 div a b 表示 a/b。 另外haskell有提供語法糖讓a `div` b表示div a b
  • mod:整數除法取餘。 大部分寫作 a `mod` b
  • (^):次方運算。
  • (==) (/=):等於與不等於。注意不等於與其他語言的寫法不同
  • (>=) (>) (<=) (<):不等號們。
  • (&&) (||):和、或。
  • not:邏輯非。
  • min max:取較小或較大者。
  • succ pred:下一個或上一個(定義在Enum的型別類別。對大部分型別而言是+1-1)。
  • fromIntegral:把Integral的東西換成其他型別

串列相關

  • length:串列長度。
  • null:檢查串列是否為空。
  • (!!):取第k個元素(x !! k)。
  • elem:判斷a是否在x中(a `elem` x)。
  • (++):連接兩個串列
  • (:): 將元素接到串列的前端(1:[2,3] = [1,2,3])。
  • head:第一個元素。
  • tail:第一個以外的元素。
  • last:最後一個元素。
  • init:最後一個以外的元素。偷learn you a haskell的圖來說明就是
  • reverse:反轉串列。
  • take n x:取x的前n項。
  • drop n x:丟掉x的前n項並回傳剩下的。
  • map f x:將f作用在x的每個元素並回傳結果串列。
  • filter p x:將x中所有滿足p的留下
  • replicate n x:將x重複n次形成的串列

其他

  • fst snd:長度為\(2\)的tuple的第一與第二項。
  • curry uncurry:將(a, b) -> ca -> b -> c互相轉換。
  • show read:轉成字串與從字串轉回來。
  • lines words:對\n分割字串。
  • unlines unwords:用\n合併字串串列。
  • id:恆等函數(id x = x)。
  • const a:恆回傳a的常數函數(const a b = a)。
  • flip f:交換二元函數的兩個參數(flip f a b = f b a)
  • ($):函數使用。f $ x = f x[2]
  • (.):函數合成。(f.g) x = f (g x) = f $ g x

折疊

折疊(fold)是函式編成的一個重要特性,因此單獨抓出來講。
所謂折疊只是依序對串列中的每個元素做運算,得到的結果再與下一個元素做運算。
例如foldl (+) 0 [1,2,3,4]就會執行((((0+1)+2)+3)+4)得到10
其中foldl的l指的是從左到右。當然還有從右到左的foldr。若想直接從串列的前兩個或後兩個開始運算而不傳入初始值,可以使用foldl1foldr1
而若想要過程中的每個結果,可以使用scanlscanr
讀者可以看看以上每個函數的型別,是否跟想像一樣?
除此之外, Haskell也有預設定義一些fold,例如

  • sum = foldl1 (+)
  • product = foldl1 (*)
  • and = foldl1 (&&)
  • or = foldl1 (||)
  • concat = foldl1 (++)
  • maximum = foldl1 max
  • minimum = foldl1 min

更多語言特性

透過上一篇文章提到的語言特性,我們可以作到各式各樣的事情。

部份使用(Partial application)

所謂部份使用,就是將一個多參數函式的某幾個參數固定,類似JS的bind
例如如果f x = max 0 x,那f就是吃進一個數字,回傳它和0中較大的那個,也就是將max部份使用到0身上。
而由於Haskell的函式是Currified的,因此f = max 0即可,畢竟max本質上就是一個a -> (a -> a)的函式。
讀者可以檢查一下max 0的型別。它的確是一個Ord a => a -> a的函式。
而Haskell又提供對於運算子的語法糖,使(+3)(2*)(`mod` 3 + 2)可以直接代表部份使用後的運算子,稱為部份運算子(operator section),這些都會是一個Num a => a -> a的函式。

範圍表達式(Range expression)

haskell對屬於Enum這個型別類別的型別提供了兩種(包含後面的無限串列共四種)範圍表達式。
[1..10]代表[1,2,3,4,5,6,7,8,9,10],而[1,3..10]代表[1,3,5,7,9]
縱使Float屬於Enum,但因為浮點數誤差,還是不建議使用範圍表達式,否則會造成意想不到的結果,例如讀者可以在ghci試試看[0.1, 0.3 ..1.0]會是什麼

串列生成(List comprehension)

串列生成是一個能夠簡單生出一個串列的語法糖。
例如[i*j | i <- [1..10], j <- [1..10]]就是concat $ map (\i -> map (\j -> i*j) [1..10]) [1..10]的語法糖。
而在串列生成時也可以做篩選(filter),例如

1
2
Prelude> [(i, j)|i<-[1..10], j<-[1..10], i + j == 10]
[(1,9),(2,8),(3,7),(4,6),(5,5),(6,4),(7,3),(8,2),(9,1)]

問題:請寫出所有三邊長小於100的畢式直角三角形。

無限串列

由於Haskell的懶惰求值,我們可以定義無限串列。
當使用範圍表答式時,其實可以不定義範圍的結束在哪裡,也就是寫[1..]
但記得不要在ghci打上這個。當在ghci打上這串時,事實上就是對它呼叫show並把結果印出。對無限串列呼叫show的結果就是它會塞爆你的螢幕。
而無限串列能讓程式碼簡單許多。若要取得前\(10\)個平方數,就只要take 10 (map (^2) [1..])就好。是不是比結構式程式設計來的簡單許多呢><
另外,Haskell也有預設定義一些能夠生成無限串列的函式。例如

  • iterate f x[x, f x, f (f x), ...]
  • repeat x[x, x, x, ...]
  • cycle:將某個串列不斷重複。

後語

基本上這次就這樣啦。基礎介紹的很大一部份是參考learn you a haskell for great good,畢竟我的Haskell也是在上面學的。如果想深入看看Prelude有哪些東西,也可以上hackage看看。
讀者試一試會發現,上面的函式已經能夠弄出很複雜很炫泡的東西了,例如
問題:什麼是flip const const
問題:什麼又是curry fst
問題:什麼又是((not.).(null.).filter.(==))
從型別應該就可以略知一二了。


  1. Prelude就是haskell會預設載入的模組

  2. 你可能會覺得很莫名其妙,但是它是有存在的價值的。
    ($)的優先度很低,而且是右至左,因此可以將f (g (h x))改寫成f $ g $ h x
    而使用稍後會介紹的operator section的語法更可以做到許多對高級函數的操作。