一、簡介
Haskell 是一種純函數式語言(purely functional programming language),其函數式特性的純度沒有爭議
命令式語言要求你提供求解的步驟,Haskell 則傾向於讓你提供問題的描述
-
非函數式思維:通過命令告訴電腦要做什麼,比如求和是通過循環結構遍歷所有的數,相加並記錄其和
-
函數式思維:通過函數來描述出問題是什麼,比如求和是把第一個數與其餘樹的和相加
P.S. 關於思維模式的差異,請查看 一場函數式思維模式的洗禮
Haskell 的特點:
-
變量不可變:函數式裡的變量與常量概念一樣,源自數學思維,令
x=1,那麼x永遠都是1 -
引用透明:函數調用能被直接替換成相應的值,而不會影響函數的行為。即函數僅用來求值,沒有副作用(不會影響外部狀態),相同輸入總能得到相同的輸出
-
惰性求值:真正需要值的時候才現算,所以此時的一連串計算(函數調用)只是作用於輸入數據的一系列變換公式,具體來看就是
array.map().filter().reduce()只需要遍歷array一遍,而不是 3 遍 -
靜態類型:編譯器會做靜態類型檢查,這沒什麼奇怪的,但還支持強大的自動類型推斷,所以多數情況不必聲明類型,這樣既擁有了靜態類型檢查的好處,還保證了代碼簡潔程度
P.S. 引用透明(Referential transparency)的詳細解釋:
An expression is said to be referentially transparent if it can be replaced with its corresponding value without changing the program's behavior. As a result, evaluating a referentially transparent function gives the same value for same arguments. Such functions are called pure functions. An expression that is not referentially transparent is called referentially opaque.
二、基本運算
負數與一元減號
-3
表示對數字 3 使用一元運算符 -,求得其相反數 -3。也就是說,-3 的 -不是數值字面量的一部分,而是個運算符
2 + -3
會得到報錯:
cannot mix '+' [infixl 6] and prefix `-' [infixl 6] in the same infix expression
二元運算符和一元運算符不能混用在同一個中綴表達式裡,這會帶來解析時的不確定性(有歧義,編譯器該怎樣理解)。所以,經驗原則是給所有負數字面量都帶上括號,如 (-3)
P.S. Haskell 只有一個一元運算符,就是一元減號 -,具體見 Unary operator
邏輯運算
3 個運算符:與(&&),或(||),非(not)
兩個 Bool 字面量:True,False
相等性判斷
-
==:判斷等於,可以用於字符串 -
/=:判斷不等於(數學家的語言,所以不等號長這樣)
注意,類型必須嚴格一致才能比較,否則報錯認為沒有可比性(1 == True 會報錯),但認為整型與浮點型是可比的(1 == 1.0 是 True)
運算符優先級
在 GHCi 環境可以通過 :info 命令查看運算符優先級,例如:
> :i *
class Num a where
...
(*) :: a -> a -> a
...
-- Defined in 'GHC.Num'
infixl 7 *
> :i -
class Num a where
...
(-) :: a -> a -> a
...
-- Defined in 'GHC.Num'
infixl 6 -
乘法比減法優先級高(分別是 7 和 6),都是中綴函數(infixl 的 infix),都是左結合的(infixl 的 l 表示 left associative),函數簽名也相同(Num a => a -> a -> a)
優先級的範圍是 0-9,值越大越優先
P.S. infixl 7 * 稱為 fixity 聲明,用來指定運算符(函數)的結合性與優先級,例如:
infixr 5 <+
x <+ xs = x : xs
infixl 5 +>
x +> xs = xs ++ [x]
聲明了 2 個自定義中綴運算符 <+ 和 +>,分別為右結合與左結合,優先級都是 5(與 : 一樣)
三、函數調用
語法格式
Haskell 裡的函數調用默認是前綴語法,例如:
succ 2
min 1 (-2)
與 Bash 腳本的函數調用語法一樣,函數名 參數 1 參數 2
但運算符作為特殊的函數,默認要以中綴形式調用,例如:
1 + 2
實際上,任意一個函數(包括運算符),都可以以前綴或者中綴形式調用,規則如下:
-- 前綴轉中綴
prefixFunc a b
a `prefixFunc` b
-- 中綴轉前綴
a infixFunc b
(infixFunc) a b
例如:
1 `min` (-2)
(+) 1 2
dollar 函數
$ 也是個函數:
($)
forall (r :: GHC.Types.RuntimeRep) a (b :: TYPE r).
(a -> b) -> a -> b
-- Defined in 'GHC.Base'
infixr 0 $
優先級最低的中綴右結合函數,從簽名來看,只是個函數調用符,相當於在右邊加括號:
-- 求自然數的平方和加到第多少個時超過 1000
length (takeWhile (< 1000) (scanl (+) 0 (map sqrt [1..])))
-- 等價於
length $ takeWhile (< 1000) $ scanl (+) 0 $ map sqrt [1..]
優先級最低,不影響運算,只調整運算順序:
> max 5 3 * 2 + 1
11
> max 5 $ 3 * 2 + 1
7
簡單地把 $ 理解成做括號的替代品是不合適的,比如:
> 3 * $ 5 - 2 + 1
<interactive>:21:5: error:
parse error on input '$'
Perhaps you intended to use TemplateHaskell
換個姿勢:
> (3 *) $ 5 - 2 + 1
12
因為 $ 是個中綴函數,要求左邊是函數,右邊是其參數
P.S. 還有一個很有意思的東西:($ 2) sqrt,中綴函數柯里化的小把戲
柯里化
Haskell函數默認都是柯里化的,都只接受一個參數:
In Haskell, all functions are considered curried: That is, all functions in Haskell take just one argument.
例如:
> :t (/)
(/) :: Fractional a => a -> a -> a
> :t (/ 2)
(/ 2) :: Fractional a => a -> a
其中,(/ 2) 是對 (/) :: Fractional a => a -> a -> a 函數的不全調用(partially applied),所以沒有得到計算結果,而是返回了函數 (/ 2) :: Fractional a => a -> a
P.S. (-) 函數比較特殊,因為 (- 2) 表示負 2,而不返回一個新函數,非要的話,用 (subtract 2)
可以通過 curry 與 uncurry 函數進行柯里化或去柯里化:
-- uncurry 去柯里化
> :t uncurry (/)
uncurry (/) :: Fractional c => (c, c) -> c
> (uncurry (/)) (4, 2)
2.0
-- curry 柯里化
> :t curry (uncurry (/))
curry (uncurry (/)) :: Fractional c => c -> c -> c
> (curry (uncurry (/))) 4 2
2.0
注意:多參函數要以元組形式傳參,例如 f (4, 2)
利用柯里化特性時還需要注意參數順序,例如:
> (/ 2) 4
2.0
> (2 /) 4
0.5
偏函數應用
偏函數應用(partial application)與柯里化(currying)的最大區別是對參數數量的影響,從調用函數求值的角度來看,柯里化並不改變參數數量,而偏函數應用會減少參數數量,因為預填了幾個,例如:
fn (a, b) = a + b
curriedFn = curry fn
partialFn = curriedFn 2
// 調用函數求值
fn (2, 3)
-- 加上括號讓結合性更清楚一些
(curriedFn 2) 3
partialFn 3
所以,二者的聯繫是,可以通過柯里化函數來創建偏函數(partialFn = curriedFn 2)。區別是目的不同,偏函數應用是為了減少函數所需參數數量(通過固定一些參數值),柯里化是為了把一個多���函數轉換成單參函數,這個單參函數返回另一個單參函數(參數數量不足),或者求值(參數數量夠了)
四、函數聲明
doubledouble x = double (double x)
double x = x * 2
isEven x = x - x `div` 2 * 2 == 0
x `mod'` y = x - (x `div` y) * y
形式與函數調用差不多,函數名加空格分隔的參數列表,= 後面是函數體
2 個特點:
-
聲明順序無所謂
-
函數名首字母不能大寫,不能數字開頭
P.S. 數學裡把相似的東西用 x x' x'' 的命名習慣表示,在 Haskell 裡也可以這樣做:
y x = x ^ 2
y' x = x ^ 2 + 1
另外,中綴形式轉換在函數聲明中也可以用:
x `mod'` y = x - (x `div` y) * y
一些場景下能夠提升函數聲明的可讀性
無參函數
常量可以理解成無參函數,例如:
> :t 2
2 :: Num t => t
或者更生動的例子:
-- 無參函數,就是 const
two = 1 + 1
匿名函數
匿名函數即函數表達式,在 Haskell 中稱之為 lambda。語法格式如下:
反斜線 + 參數列表 -> 函數體
例如:
sum' = \x y -> x + y
P.S. 類似於 JS 的 const sum = (x, y) => x + y
從應用場景來看,lambda 的特點是:
-
用完即扔:不要求複用
-
足夠簡單:自解釋,單從函數體一眼就能看明白其功能
例如:
map (\x -> x + 1) [1, 2, 3]
map (\([x, y]) -> x + y) [[1, 1], [2, 2], [3, 3]]
但很多時候並不需要顯式地通過 lambda 語法來造函數,可以充分利用柯里化、List Comprehension 等特性:
map (+1) [1, 2, 3]
[ x + y | [x, y] <- [[1, 1], [2, 2], [3, 3]]]
另外,有個有趣的東西:
addThree = \x -> \y -> \z -> x + y + z
-- 因為 haskell 自帶 currying,所以等價於
-- addThree x y z = x + y + z
P.S. 匿名函數中的 -> 與類型聲明中的 -> 語義相同,都表示「映射到」(maps to)
函數組合
數學中的函數組合的表達方式是 f·g(x) = f(g(x)),Haskell 與之類似:
fg = f . g
用到的運算符是 .:
(.) :: (b -> c) -> (a -> b) -> a -> c -- Defined in 'GHC.Base'
infixr 9 .
右結合,所以 f . g . h x 等價於 f (g (h x)):
((/7) . (*2) . (+3)) 4
函數組合可以用來製造新函數,並且能夠把參數抽出來,例如:
-- f x = 2 * (sqrt x) + 1
-- 對應的 pointfree style
f = (+ 1) . (* 2) . sqrt
通過組合把內層參數抽離出來,並利用柯里化特性去掉。這種只通過函數組合得到的,不涉及實際參數的函數風格被稱為 pointfree style
P.S. 注意,巨長的函數鏈會降低可讀性,不鼓勵這樣做,應該通過 let/where 等聲明把函數鏈拆開並賦予語義
五、條件語句和模式匹配
條件語句
-- if then else
gt3 x = if x > 3
then True
else False
注意:if-then-else 完整結構,else 部分不可省略
有趣的是,if 語句也是表達式,所可以這樣做:
lt10 x = if x < 10 then True else False
gte10 x = not (if x < 10 then True else False)
模式匹配
模式匹配是基本的函數調用機制,例如:
sayOneTwoThree :: (Integral a) => a -> String
sayOneTwoThree x = "Not between 1 and 3"
sayOneTwoThree 1 = "One!"
sayOneTwoThree 2 = "Two!"
sayOneTwoThree 3 = "Three!"
調用函數時會按聲明順序匹配參數類型,所以上面的 sayOneTwoThree 2 只會返回 "Not between 1 and 3"
再比如利用模式匹配遞歸求階乘:
fact 0 = 1
fact n = n * fact (n - 1)
注意,如果模式匹配失敗,就會報錯:
mod10 0 = 0
mod10 1 = 1
-- 如果最後不用萬能匹配兜住,mod10 2 就會報錯
-- mod10 x = x `mod` 10
匹配失敗時:
> mod10 2
*** Exception: t.hs:(27,1)-(28,11): Non-exhaustive patterns in function mod10
提示 mod10 函數的模式定義有遺漏
除了用於函數參數定義,模式匹配還可以用於 List Comprehension 和 let-in 表達式、where 子句等場景,例如:
[ x + y | (x, y) <- [(1, 2), (3, 4)] ]
sumOneTwoThree = let (a, b, c) = (1, 2, 3) in a + b + c
常用的模式匹配技巧如下:
-- 拆開 list 首元與尾巴,要求 length >= 1,否則報錯
x:xs
-- 拆開 list 前兩個元素與尾巴
x:y:ys
-- 拆分同時保留原引用
xs @(x:y:ys)
P.S. @ 保留原引用稱為 as 模式
Guard
一個簡單的 guard 模式示例:
plus'' a b
| a > 0, b > 0 = "sum is a positive value"
| a < 0, b < 0 = "sum is a negative value"
| otherwise = "what?"
參數列表後面多了 | 條件 表示不同的函數體分支,被調用時滿足條件就執行對應函數體並返回,否則就按順序依次向下檢查
注意,最後的 otherwise 比較有意思,因為:
> :i otherwise
otherwise :: Bool -- Defined in 'GHC.Base'
> otherwise == True
True
所以 otherwise只是語義需要,直接用 True 作為默認分支的條件也可以
P.S. 單行形式也是合法的(但可讀性差,不建議用),例如:
isPositive n | n > 0 = True | otherwise = False
where 關鍵字
where 關鍵字跟在 guard 後面,用來定義函數聲明中需要用到的變量,及輔助函數
checkArea r
| area < little = addSpace "little circle" ++ wrap sArea
| area < normal = addSpace "normal circle" ++ wrap sArea
| otherwise = addSpace "big big circle" ++ wrap sArea
where area = pi * r ^ 2
-- ! 必須對齊,有點傻
--little = 10
--normal = 50
-- where 中可以用模式匹配
(little, normal) = (10, 50)
sArea = show area
-- 可以定義函數
addSpace s = ' ' : s
-- where 可以嵌套,在輔助函數中定義輔助函數
wrap s = wrapLeft (wrapRight s)
where wrapLeft s = " " ++ s
wrapRight s = s ++ " "
where 子句的幾個特點:
-
多行聲明必須對齊縮進,否則編譯器無法正確解析(不知道要定義的變量/函數列表結束了沒)
-
子句中聲明的變量和函數的作用域是當前函數及其 guard,且不包括同名函數的其它模式
-
子句中可以用模式匹配
-
允許嵌套使用,輔助函數也可以在自己的
where子句中聲明需要的變量和輔助函數
注意,where 是一種語法結構,用來在函數底部聲明變量/函數,作用域是包括 guard 在內的整個函數
P.S. 非要單行的話,可以用分號隔開多個聲明,例如:
sayHello = hello ++ " " ++ greeting
where hello = "Hi"; greeting = "girls"
let 關鍵字
語法格式:
let [bindings] in [expressions]
例如:
cylinder r h =
let sideArea = 2 * pi * r * h
bottomArea = pi * r ^ 2
in sideArea + 2 * bottomArea
-- 表達式形式一般用法類似於 js 解構賦值
oneTwoThree = let (a, b, c) = (1, 2, 3) in a + b + c
let-in 的作用與 where 類似,都用來定義局部變量/函數,區別如下:
-
形式上:
let xxx in...與...where xxx的聲明位置區別,let把定義放在前面了 -
語法上:
let-in是表達式,而where是語法結構,前者可以隨便放 -
作用域上:
let-in的作用域限制更嚴格,在let部分定義的變量/函��只對in部分可見
注意,同樣要求多行聲明要嚴格對齊,非要單行就用分號隔開
P.S. let-in 的 in 部分可以省略,作用域擴展到當前函數/List Comprehension,如果是在 GHCi 環境,在整個交互過程都可見
Case 表達式
最常見的 case 表達式就是函數定義時參數的模式匹配(case 表達式的語法糖):
tail' [] = "empty list"
tail' [x] = "no tail"
tail' (_:xs) = show xs
等價於
tail'' xs = case xs of [] -> "empty list"
[x] -> "no tial"
(_:xs) -> show xs
語法格式如下:
case expression of pattern -> result
pattern -> result
pattern -> result
...
用 expression 依次嘗試匹配 pattern,匹配成功就執行對應的代碼塊並返回結果,否則嘗試下一個,都不匹配就報錯
P.S. 同樣,作為表達式,case-of 可以用於任何地方,比模式匹配靈活得多(模式匹配只能用於函數聲明、where、let、List Comprehension 等特定場景)
六、數據結構
List
Haskell 中的 List 是單一類型數組,例如:
emptyArr = []
numbers = [1, 2, 3, 4]
chars = ['a', 'b', 'c']
實際上,字符串就是 Char 類型元素的 List,例如:
> str = "okay"
> :i str
str :: [Char] -- Defined at <interactive>:51:1
> charArr = ['o', 'k', 'a', 'y']
> :i charArr
charArr :: [Char] -- Defined at <interactive>:54:1
所以,字符串字面量只是個 List 語法糖
List 操作
++ 連接:
> "Is life always this hard" ++ " ,or is it just when you are a kid ?"
"Is life always this hard ,or is it just when you are a kid ?"
-- 或者
> ['A', 'l', 'w', 'a', 'y', 's'] ++ [' '] ++ "like this."
"Always like this."
注意,++ 操作會遍歷左邊的 List,所以性能不高
: 左端插入:
> 0 : [1, 2, 3]
[0,1,2,3]
另外,[1, 2, 3] 實際上是 1 : 2 : 3 : [] 的語法糖,: 右結合得到完整 List
!! 按索引取元素:
> "hello there" !! 4
'o'
> "hello there" !! 14
*** Exception: Prelude.!!: index too large
索引從 0 開始,越界會報錯
多維數組
> [[1], [2, 3]] !! 1 !! 1
3
允許鋸齒數組,同樣要求元素類型一致
List 比較
如果 List 中元素可比較,則 List 可比較(遍歷比較各元素):
> "hello" == ['h', 'e', 'l', 'l', 'o']
True
> "0110" > "0101"
True
常用函數
-- head 取首元
> head [1, 2, 3]
1
-- tail 取尾巴(去首元)
> tail [1, 2, 3]
[2,3]
-- last 取尾元
> last [1, 2, 3]
3
-- init 取身子(去尾元)
> init [1, 2, 3]
[1,2]
注意:如果 List 為空,這 4 個函數會報錯
-- length 求數組長度
> length [1, 2]
2
-- null 判空
> null []
True
-- reverse 首尾轉置
> reverse [1, 3, 2]
[2,3,1]
-- take 取前 n 個元素
> take 5 [1, 2]
[1,2]
-- drop 去掉前 n 個元素
> drop 5 [1, 2]
[]
take/drop 越界不會報錯
-- maximum 求最大值
> maximum [1, -2, 3 , 0]
3
-- sum 求和
> sum [1..10]
55
-- elem 判斷包含性
> 3 `elem` [1, 2]
False
其中 [1..10] 是 Range 語法,elem 以中綴形式調用更符合語義習慣
Range
一種用來生成可枚舉 List 的便捷方式,例如:
> [1..7]
[1,2,3,4,5,6,7]
> ['A'..'Z']
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
允許通過前兩項來指定間隔(step),可以是負間隔(遞減序列):
> [1, 3..7]
[1,3,5,7]
> [10, 9..1]
[10,9,8,7,6,5,4,3,2,1]
浮點數存在精度的問題,不建議在 Range 中使用:
> [0.1, 0.3..1]
[0.1,0.3,0.5,0.7,0.8999999999999999,1.0999999999999999]
另外,還允許無限序列,例如:
-- 不設上限的 Range
> take 3 [1..]
[1,2,3]
-- 或者 cycle 函數無限重複
> take 7 (cycle [1..3])
[1,2,3,1,2,3,1]
-- 或者 repeat 生成單元素無限重複的 List
> take 6 (repeat 3)
[3,3,3,3,3,3]
-- 上一句結果等價於
> replicate 6 3
[3,3,3,3,3,3]
List Comprehension
列表推導,是指從既有 List 按照一定規則產生另一個 List。所以需要 map, filter 等操作的場景都可以用 List Comprehension 來完成
語法形式與數學集合定義類似,比如用集合描述 10 以內的偶數為 S = {2 * x | x <- N, x <= 5},對應的 List Comprehension 語法如下:
> [ 2 * x | x <- [1..5] ]
[2,4,6,8,10]
P.S. 用 <- 表示屬於符號,非常形象
還可以添加更多限制條件(predicate):
> [ 2 * x | x <- [1..5], 2 * x `mod` 3 == 1, x `mod` 5 /= 0 ]
[4]
除了加限制條件過濾,還可以通過 let 聲明變量/函數:
> [ doubleX | x <- [1..5], let doubleX = 2 * x, doubleX `mod` 3 == 1, x `mod` 5 /= 0 ]
[4]
P.S. 省略 in 部分的話,聲明的變量/函數對其右側限制條件和變量,以及豎線左側部分可見。帶上的話,僅作用於當前條件
複雜一點的,比如求 1 到 100 的所有素數:
isPrime n = null [ x | x <- [2..n-1], n `mod` x == 0 ]
[ x | x <- [1..100], isPrime x ]
看起來與數學公式沒什麼區別,isPrime 的判定規則是 n 無法被 2..n-1 中的任何一個數整除,1 到 100 中所有滿足該判定規則的元素組成的集合即為所求
像集合定義一樣,元素可以來自多個既有集合,例如:
> [ x * y | x <- [1, 2, 3], y <- [4, 5] ]
[4,5,8,10,12,15]
可以利用 List Comprehension 自己實現個 length 函數:
length' arr = sum [ 1 | x <- arr ]
-- 或者更符合習慣的
length' xs = sum [ 1 | _ <- xs ]
P.S. 其中 _ 表示佔位符,不關心其值是什麼,算一種地道的編碼習慣
另外,List Comprehension 是可以嵌套的:
-- 濾掉二維數組中的奇數
[ [ x | x <- xs, even x ] | xs <- [[1,2], [3, 4]] ]
[[2],[4]]
Tuple
元組不要求單一元素類型,從類型約束來看,相當於結構體
例如:
> :t (1, "Leon")
(1, "Leon") :: Num t => (t, [Char])
-- List 要求類型單一,所以把二元組和三元組放到一個 List 裡會報錯
> [(1, "Leon"), (2, "Milk"), (3, "Little", "Girl")]
<interactive>:148:28: error:
? Couldn't match expected type '(t, [Char])'
with actual type '(Integer, [Char], [Char])'
與 List 一樣,如果元組中的元素可比較,那麼同類型元組也可以比較
複雜一點的例子,求所有三邊長度皆為整數且小於等於 10,周長為 24 的直角三角形:
[ (a, b, c) | c <- [1..10], b <- [1..c], a <- [1..b], a + b + c == 24, a^2 + b^2 == c^2 ]
注意其中隱含的邊長關係:c >= b >= a,算作去重規則
常用函數
fst/snd 取二元組的首元/尾元:
fst (1, 2)
snd (1, 2)
注意,這兩個函數只能用於二元組。一般元組沒有類似的工具函數,但可以通過模式匹配來自己實現:
-- 取三元組首元
first (x, _, _) = x
zip 從 List 組合出元組:
> zip [1, 2] ['A', 'B', 'C']
[(1,'A'),(2,'B')]
多餘的單身元素會被丟掉
暫無評論,快來發表你的看法吧