《Haskell 趣学指南》是一本非常简洁明了的入门书。

Introduction

Haskell 是一门纯函数式编程语言。

函数式,相对于命令式,解决问题的方式是定义问题,而不是定义如何做。

纯函数,说的是没有副作用。也就是函数的执行不会依赖和改变状态(变量的值等)。只依赖输入。两次相同输入对应同一个输出。

Haskell 是惰性的。

表达式的值只会在需要的时候才会求值。所以使用无限长度的列表也不会有问题。

Haskell 基于静态类型

也就是 Haskell 编译期能够在运行前推断出值的类型。

Haskell 是一帮博士设计的语言。第一版语言报告在 2003 年公布。

Haskell 的一个运行平台是 GHC (Glasgow Haskell Complier) 。文件后缀为 .hs

类型名称使用大驼峰。

变量使用小驼峰。

变量名称中可以使用 '_ 字符。

' 表示稍微修改的版本。

_ 表示不关注该变量。

GHC 的一些命令

命令 作用
:l 加载文件 (Load)
:r 重新加载文件
:t 查看类型
:set prompt 改变 prelude 提示字符
:m + 模块名称 加载模块的某一部分
it 上一个表达式结果
ghc –make xxx.hs 编译文件
runhaskell xxx.hs 跳过编译临时运行

Starting Out

支持大部分算术符号。使用负数需要用括号括起来。使用括号可以改变优先级

符号 操作
+ 加法
- 减法,负数
* 乘法
/ 除法,结果为小数
() 括号,改变优先级
True 布尔值真
False 布尔值假
&& 布尔运算且
|| 布尔运算或
not 布尔运算非
== 判断是否相等
/= 判断是否不等

Haskell 不会自动隐式类型转换。所以 5 + True 会报错。5 + 4.0 也是会报错的。因为一个是整数一个是浮点数。

以上操作符均为中缀操作符。

函数调用使用前缀表达式。函数名后跟参数,以空格区分。

1
2
ghci> succ 8  
9

函数调用优先级最高。

1
2
3
4
5
ghci> succ 9 + max 5 4 + 1  
16
-- 相当于下面的式子
ghci> (succ 9) + (max 5 4) + 1
16

函数使用反引号「`」包裹后可使用中缀的表达式

1
2
3
4
ghci> div 92 10
9
ghci> 92 `div` 10
9

函数嵌套需要括号括住

1
2
ghci> succ (succ 7)
9

函数定义格式为,函数名,参数列表,=,表达式。

1
doubleMe x = x + x

函数定义不需要手动提升。先定义的函数可以调用后定义的函数

条件判断操作符为 if ... then ... else ... 。没有大括号,必须有 else 分支以保证 if 表达式一定有值。

条件判断操作符可以换行,但要注意缩进。

1
2
3
4
5
doubleSmallNumber x = if x > 100  
then x
else x*2

doubleSmallNumber' x = (if x > 100 then x else x*2) + 1

通常在函数后面加 ' 表示稍微修改的函数。

字符串使用双引号,字符使用单引号。

变量绑定使用 let 关键字。不需要声明类型。

1
let a = 1

列表使用 [] ,元素用 , 分隔

1
['h','e','l','l','o']

列表使用 ++ 函数拼接。字符串是字符列表所以用 ++ 拼接

1
2
3
4
ghci> [1,2,3,4] ++ [9,10,11,12]  
[1,2,3,4,9,10,11,12]
ghci> "hello" ++ " " ++ "world"
"hello world"

: 用于向列表最前端插入一个元素。

1
2
3
4
ghci> 'A':" SMALL CAT"  
"A SMALL CAT"
ghci> 5:[1,2,3,4,5]
[5,1,2,3,4,5]

!! 用于取出元素,长度不够会报错。

1
2
ghci> "Steve Buscemi" !! 6  
'B'

head 获得列表头元素。 tail 获得尾列表

1
2
3
4
ghci> head [5,4,3,2,1]  
5
ghci> tail [5,4,3,2,1]
[4,3,2,1]

elem 用于判断元素是否在列表中

1
2
3
4
ghci> 4 `elem` [3,4,5,6]  
True
ghci> 10 `elem` [3,4,5,6]
False

.. 在列表中表示范围

1
2
3
4
5
6
ghci> [1..20]  
[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]
ghci> ['K'..'Z']
"KLMNOPQRSTUVWXYZ"
ghci> [2,4..20]
[2,4,6,8,10,12,14,16,18,20]

replicate 用于重复相同元素一定的次数。

1
2
ghci> replicate 3 10
[10,10,10]

其他函数参见常用函数

列表解析式(List comprehensions)是 Haskell 中一种特殊的列表构造方式,类似于数学中的集合。

比如集合 $S = \lbrace 2 \cdot x | x \in \Bbb{N} , x \le 10 \rbrace $ 对应的 Haskell 代码

1
2
ghci> [x*2 | x <- [1..10]]  
[2,4,6,8,10,12,14,16,18,20]

| 左边为集合元素表达式,右边为过滤条件。

1
2
3
4
5
6
ghci> [x*2 | x <- [1..10], x*2 >= 12]  
[12,14,16,18,20]
ghci> [ x | x <- [50..100], x `mod` 7 == 3]
[52,59,66,73,80,87,94]
ghci> [ if x < 10 then "BOOM!" else "BANG!" | x <- [1..10], odd x]
["BOOM!","BOOM!","BOOM!","BOOM!","BOOM!"]

支持多个元素(变量)

1
2
3
4
ghci> [ x*y | x <- [2,5,10], y <- [8,10,11]]  
[16,20,22,40,50,55,80,100,110]
ghci> [ (a,b,c) | c <- [1..10], b <- [1..c], a <- [1..b], a^2 + b^2 == c^2]
[(3,4,5),(6,8,10)]

元组用于保存不同类型变量。

1
("Christopher", "Walken", 55)

Types and Typeclasses

Haskell 使用 Hindley-Milner 类型系统。格式为

1
函数名字 :: (类型约束) => 参数1 -> 参数2 ... -> 参数n

比如加法的类型签名为

1
2
ghci> :t (+)
(+) :: Num a => a -> a -> a

其中 a 为类型变量。Num a 表示 a 为数字类型,可以是 intInteger 等。

Haskell 的常见类型

类型 描述
Int 整数,[$ -2^{31}$,$2^{31} - 1 $ ]
Integer 整数,无范围限制
Float 单精度浮点数
Double 双精度浮点数
Bool 布尔类型
Char 字符

类型类(Typeclass) 指的是类型的类型。是对类型的再一次归类。如 int 可以是 Eq 类,也可以是 Ord 类。

Haskell 的常见类型类

类型类 描述 支持的操作符
Eq 可比较是否相等 == /= elem
Ord 可比较顺序 < > <= >=
Show 任意类型转为可显示的字符串 show
Read 字符串解析为特定类型 read
可枚举类型类 .. succ pred
Bounded 有界类型类 minBound maxBound
Num 数字类型类 + - *
Integral
Floating 小数类型类
Functor 函子 fmap
Applicative Functor 可相互应用的函子 pure <*>
Monad 单子 return >>=

Syntax in Functions

模式匹配类似于命令式语言中的 Switch ... case 匹配,不过匹配的源表达式更复杂。

1
2
3
lucky :: (Integral a) => a -> String  
lucky 8 = "LUCKY NUMBER EIGHT!"
lucky x = "Sorry, you're out of luck, pal!"

匹配顺序从上到下,匹配失败会报错。

模式匹配经常于递归搭配使用,先写出递归的边界条件,然后匹配剩下的递归

1
2
3
factorial :: (Integral a) => a -> a  
factorial 0 = 1
factorial n = n * factorial (n - 1)

模式匹配还可用于元组和列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
addVectors :: (Num a) => (a, a) -> (a, a) -> (a, a)  
addVectors a b = (fst a + fst b, snd a + snd b)
-- 使用模式匹配后
addVectors :: (Num a) => (a, a) -> (a, a) -> (a, a)
addVectors (x1, y1) (x2, y2) = (x1 + x2, y1 + y2)
-- 列表的模式匹配
ghci> let xs = [(1,3), (4,3), (2,4), (5,3), (5,6), (3,1)]
ghci> [a+b | (a,b) <- xs]
[4,7,6,8,11,4]

head' :: [a] -> a
head' [] = error "Can't call head on an empty list, dummy!"
head' (x:_) = x

length' :: (Num b) => [a] -> b
length' [] = 0
length' (_:xs) = 1 + length' xs

模式匹配支持 all@pattern 的方式匹配。

1
2
3
capital :: String -> String  
capital "" = "Empty string, whoops!"
capital all@(x:xs) = "The first letter of " ++ all ++ " is " ++ [x]

如果说模式匹配类似于 Switch ... case ,那么条件匹配(Guards)类似于 if ... then ... else

条件匹配可以支持一个范围区间,而模式匹配不可以,除非手写范围区间内所有情况。

条件匹配用 | 分隔条件。

1
2
3
4
5
6
bmiTell :: (RealFloat a) => a -> String  
bmiTell bmi
| bmi <= 18.5 = "You're underweight, you emo, you!"
| bmi <= 25.0 = "You're supposedly normal. Pffft, I bet you're ugly!"
| bmi <= 30.0 = "You're fat! Lose some weight, fatty!"
| otherwise = "You're a whale, congratulations!"

if 类似,条件匹配必须有 otherwise 保证表达式一定有值。

定义函数时可以使用反引号「`」

1
2
3
4
5
6
7
myCompare :: (Ord a) => a -> a -> Ordering  
a `myCompare` b
| a > b = GT
| a == b = EQ
| otherwise = LT
ghci> 3 `myCompare` 2
GT

限定语句(Where) ,可用于声明变量。

1
2
3
4
5
6
7
8
bmiTell :: (RealFloat a) => a -> a -> String  
bmiTell weight height
| bmi <= skinny = "You're underweight, you emo, you!"
| bmi <= normal = "You're supposedly normal. Pffft, I bet you're ugly!"
| bmi <= fat = "You're fat! Lose some weight, fatty!"
| otherwise = "You're a whale, congratulations!"
where bmi = weight / height ^ 2
(skinny, normal, fat) = (18.5, 25.0, 30.0)

绑定语句(Let),与限定语句对应,用于在开头绑定变量。

形式为 Let <binding> in <expression>

1
2
3
4
5
cylinder :: (RealFloat a) => a -> a -> a  
cylinder r h =
let sideArea = 2 * pi * r * h
topArea = pi * r ^2
in sideArea + 2 * topArea

绑定语句很灵活,不仅仅在函数定义中使用,还可以在列表等多处地方使用。

1
2
ghci> [let square x = x * x in (square 5, square 3, square 2)]  
[(25,9,4)]

case 语句。类似于命令式语言的 case

1
2
3
head' :: [a] -> a  
head' xs = case xs of [] -> error "No head for empty lists!"
(x:_) -> x

Recursion

Haskell 的函数定义支持递归

1
2
3
quicksort :: (Ord a) => [a] -> [a]  
quicksort [] = []
quicksort (x:xs) = quicksort [a | a <- xs, a <= x] ++ [x] ++ quicksort [a | a <- xs, a > x]

有了递归之后常见函数都可以使用递归实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
replicate' :: (Num i, Ord i) => i -> a -> [a]  
replicate' n x
| n <= 0 = []
| otherwise = x:replicate' (n-1) x

take' :: (Num i, Ord i) => i -> [a] -> [a]
take' n _
| n <= 0 = []
take' _ [] = []
take' n (x:xs) = x : take' (n-1) xs

reverse' :: [a] -> [a]
reverse' [] = []
reverse' (x:xs) = reverse' xs ++ [x]

repeat' :: a -> [a]
repeat' x = x:repeat' x

Higher order functions

柯里化是一种将多参函数化为多个单参函数的奇数。Haskell 中函数只支持一个参数,通过柯里化就能实现支持多个参数的函数。多参函数的调用过程可以视为单参函数的逐步填充过程。调用函数却没有给够参数的过程称为部分应用(partially apply)

1
2
3
4
ghci> max 4 5  
5
ghci> (max 4) 5 -- (max 4) 是一个函数
5

接收或返回函数的一类函数称为高阶函数。

1
2
3
4
5
6
7
8
applyTwice :: (a -> a) -> a -> a  
applyTwice f x = f (f x)

ghci> applyTwice (+3) 10
16

ghci> applyTwice (++ " HAHA") "HEY"
"HEY HAHA HAHA"

Haskell 常见的高阶函数

函数 描述
map 对列表所有元素应用函数,返回所有元素应用函数后的列表
filter 对列表所有函数应用过滤函数,返回符合条件的列表
foldl 从最左边的元素开始,从左往右应用函数,返回累积值。
foldr 从最右边的元素开始,从右往左应用函数,返回累积值。
foldl1 与 foldl 类似,不用提供初始值
foldr1 与 foldr 类似,不用提供初始值
scanl 与 foldl 类似,不过保留每个步骤的结果
scanr 与 foldr 类似,不过保留每个步骤的结果

例子

1
2
ghci> sum (takeWhile (<10000) (filter odd (map (^2) [1..])))  
166650

列表操作大部分都可以使用 fold

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
maximum' :: (Ord a) => [a] -> a  
maximum' = foldr1 (\x acc -> if x > acc then x else acc)

reverse' :: [a] -> [a]
reverse' = foldl (\acc x -> x : acc) []

product' :: (Num a) => [a] -> a
product' = foldr1 (*)

filter' :: (a -> Bool) -> [a] -> [a]
filter' p = foldr (\x acc -> if p x then x : acc else acc) []

head' :: [a] -> a
head' = foldr1 (\x _ -> x)

last' :: [a] -> a
last' = foldl1 (\_ x -> x)

匿名函数(Lambda),没有名字的函数。

1
2
3
4
5
6
7
numLongChains :: Int  
numLongChains = length (filter (\xs -> length xs > 15) (map chain [1..100]))

ghci> map (+3) [1,6,3,2]
[4,9,6,5]
ghci> map (\x -> x + 3) [1,6,3,2]
[4,9,6,5]

$ 函数应用符。右结合,优先级很低,所以可以用来省略括号。

1
2
3
sum (filter (> 10) (map (*2) [2..10]))

sum $ filter (> 10) $ map (*2) [2..10]

还可以这么玩

1
2
ghci> map ($ 3) [(4+), (10*), (^2), sqrt]  
[7.0,30.0,9.0,1.7320508075688772]

(.) 函数组合。 (f . g) x = f (g x)

1
2
3
4
5
6
7
8
ghci> map (negate . abs) [5,-3,-6,7,-3,2,-19,24]  
[-5,-3,-6,-7,-3,-2,-19,-24]

oddSquareSum :: Integer
oddSquareSum = sum (takeWhile (<10000) (filter odd (map (^2) [1..])))
-- 改为函数组合
oddSquareSum :: Integer
oddSquareSum = sum . takeWhile (<10000) . filter odd . map (^2) $ [1..]

Modules

Haskell 的模块与其他语言的模块概念相似。

导入模块 import <module name>

1
2
3
4
import Data.List  
-- nub 是其中的一个函数
numUniques :: (Eq a) => [a] -> Int
numUniques = length . nub

只引用模块中部分函数

1
import Data.List (nub, sort)

不用模块中的某些函数

1
import Data.List hiding (nub)

取别名

1
import qualified Data.Map as M

编写模块与编写普通的 Haskell 代码一样,只不过需要在文件头部加上需要导出的函数模块声明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
module Geometry  
( sphereVolume
, sphereArea
, cubeVolume
, cubeArea
, cuboidArea
, cuboidVolume
) where

sphereVolume :: Float -> Float
sphereVolume radius = (4.0 / 3.0) * pi * (radius ^ 3)

sphereArea :: Float -> Float
sphereArea radius = 4 * pi * (radius ^ 2)

cubeVolume :: Float -> Float
cubeVolume side = cuboidVolume side side side

cubeArea :: Float -> Float
cubeArea side = cuboidArea side side side

cuboidVolume :: Float -> Float -> Float -> Float
cuboidVolume a b c = rectangleArea a b * c

cuboidArea :: Float -> Float -> Float -> Float
cuboidArea a b c = rectangleArea a b * 2 + rectangleArea a c * 2 + rectangleArea c b * 2

rectangleArea :: Float -> Float -> Float
rectangleArea a b = a * b

Making Our Own Types and Typeclasses

代数数据类型(Algebraic data types),是 Haskell 中的一种自定义数据类型。注意不要缩写成 ADT 。因为有好几个缩写都可以是 ADT 。

纯数据类型

1
data Bool = False | True

带构造器类型

1
data Shape = Circle Float Float Float | Rectangle Float Float Float Float

注意类型首字母大写,类型变量首字母小写的规定。

为了能显示需要继承 Show 类型类,使用 deriving 关键字,多个类型类用 , 分隔。

1
data Shape = Circle Float Float Float | Rectangle Float Float Float Float deriving (Show)

实例化

1
2
3
4
ghci> Circle 10 20 5  
Circle 10.0 20.0 5.0
ghci> Rectangle 50 230 60 90
Rectangle 50.0 230.0 60.0 90.0

定义了抽象数据类型后通常需要定义相应的 Getter 。Haskell 中有记录语法(Record Syntax)避免手写这些重复的代码。

1
2
3
4
5
6
7
data Person = Person { firstName :: String  
, lastName :: String
, age :: Int
, height :: Float
, phoneNumber :: String
, flavor :: String
} deriving (Show)

定义完后,可以直接取值

1
2
3
4
ghci> :t flavor  
flavor :: Person -> String
ghci> :t firstName
firstName :: Person -> String

代数数据类型中的类型构造函数可以接受类型参数,这点类似泛型中的参数变量

1
data Maybe a = Nothing | Just a  -- a 为类型参数

接受不同的类型

1
2
3
4
5
6
7
8
9
10
11
12
ghci> Just "Haha"  
Just "Haha"
ghci> Just 84
Just 84
ghci> :t Just "Haha"
Just "Haha" :: Maybe [Char]
ghci> :t Just 84
Just 84 :: (Num t) => Maybe t
ghci> :t Nothing
Nothing :: Maybe a
ghci> Just 10 :: Maybe Double
Just 10.0

既然有类型参数,那么类型参数也是可以有的

1
data (Ord k) => Map k v = ...

有时候新的类型跟已有类型内容一样只是名字不同,可以使用类型别名(Type Synoyms)。

1
2
3
4
5
type String = [Char] 

type PhoneNumber = String
type Name = String
type PhoneBook = [(Name,PhoneNumber)]

类型别名也可以带类型参数

1
2
3
type AssocList k v = [(k,v)]  
type IntMap v = Map Int v
type IntMap = Map Int

代数数据类型还支持递归

1
data List a = Empty | Cons a (List a) deriving (Show, Read, Eq, Ord)

实例化

1
2
3
4
5
6
7
8
ghci> Empty  
Empty
ghci> 5 `Cons` Empty
Cons 5 Empty
ghci> 4 `Cons` (5 `Cons` Empty)
Cons 4 (Cons 5 Empty)
ghci> 3 `Cons` (4 `Cons` (5 `Cons` Empty))
Cons 3 (Cons 4 (Cons 5 Empty))

递归最典型的数据结构是树

1
2
3
4
5
6
7
8
9
10
11
data Tree a = EmptyTree | Node a (Tree a) (Tree a) deriving (Show, Read, Eq)  

singleton :: a -> Tree a
singleton x = Node x EmptyTree EmptyTree

treeInsert :: (Ord a) => a -> Tree a -> Tree a
treeInsert x EmptyTree = singleton x
treeInsert x (Node a left right)
| x == a = Node x left right
| x < a = Node a (treeInsert x left) right
| x > a = Node a left (treeInsert x right)

如果想自定义类型类的行为,可以实现相应的实例

1
2
3
4
instance Show TrafficLight where  
show Red = "Red light"
show Yellow = "Yellow light"
show Green = "Green light"

实例中也可以有类型约束

1
2
3
4
instance (Eq m) => Eq (Maybe m) where  
Just x == Just y = x == y
Nothing == Nothing = True
_ == _ = False

关于 data ,type 和 new type 的区别

data 用于声明全新的类型

type 用于声明已有的类型的别名

new type 用于声明已有的类型的封装

Input and Output

Haskell 是纯函数语言,而输入输出是有副作用的,也就不是纯粹的函数。先来看最应该写在文章开头的 Hello World

1
main = putStrLn "hello, world"

直接拼接输入是不可以的,因为类型不一致

1
nameTag = "Hello, my name is " ++ getLine  -- getLine :: IO String

这样也是不行的,相当于给 getLine 取了一个 name 的别名

1
name = getLine

需要使用 do 语法。这是因为使用了 Monad

1
2
3
4
main = do  
putStrLn "Hello, what's your name?"
name <- getLine
putStrLn ("Hey " ++ name ++ ", you rock!")

do 语法中变量赋值需要使用 let

1
2
3
4
5
6
7
8
9
10
import Data.Char  

main = do
putStrLn "What's your first name?"
firstName <- getLine
putStrLn "What's your last name?"
lastName <- getLine
let bigFirstName = map toUpper firstName
bigLastName = map toUpper lastName
putStrLn $ "hey " ++ bigFirstName ++ " " ++ bigLastName ++ ", how are you?"

打开文件有相应的函数。但是如果只是读写可以使用 readFile writeFile

1
2
3
4
5
6
import System.IO     
import Data.Char

main = do
contents <- readFile "girlfriend.txt"
writeFile "girlfriendcaps.txt" (map toUpper contents)

Functors, Applicative Functors and Monoids

具体的定义参见之前写的文章

fmap 的作用是能让普通函数应用到被 functor 包裹的值

1
2
ghci> fmap (replicate 3) [1,2,3,4]  
[[1,1,1],[2,2,2],[3,3,3],[4,4,4]]

IO 也是一个 functor

1
2
3
4
instance Functor IO where  
fmap f action = do
result <- action
return (f result)

IO 的示例用法

1
2
3
4
main = do line <- getLine   
let line' = reverse line
putStrLn $ "You said " ++ line' ++ " backwards!"
putStrLn $ "Yes, you really said" ++ line' ++ " backwards!"

也可以使用 fmap

1
2
3
main = do line <- fmap reverse getLine  
putStrLn $ "You said " ++ line ++ " backwards!"
putStrLn $ "Yes, you really said" ++ line ++ " backwards!"

列表的 fmapmap

1
2
ghci> fmap (replicate 3) [1,2,3,4]  
[[1,1,1],[2,2,2],[3,3,3],[4,4,4]]

还有一个 functor 说出来你可能不信。就是 (->) 。对,就是函数签名里面的 ->

1
2
instance Functor ((->) r) where  
fmap f g = (\x -> f (g x))

functor 能使正常的函数应用到 functor 里面的值。applicative functor 能使内部的值相互应用

1
2
3
4
ghci> Just (+3) <*> Just 9  
Just 12
ghci> Just (++"hahah") <*> Nothing
Nothing

遇到多个参数可以使用 (<$>) 应用函数提升为 functor 再使用 <*>

1
2
3
4
5
6
7
ghci> ((+) $ 2) 3 -- 普通版
5
ghci> (+) <$> (Just 2) <*> (Just 3) -- applicative functor 版
Just 5

ghci> (++) <$> ["ha","heh","hmm"] <*> ["?","!","."]
["ha?","ha!","ha.","heh?","heh!","heh.","hmm?","hmm!","hmm."]

如果觉得麻烦,还有专门用于提升的函数。提升两个参数的函数为 Control.Applicative.liftA2

1
2
ghci> liftA2 (+) [3] [4]
[7]

IO 使用 <*>

1
2
3
4
5
6
7
8
myAction :: IO String  
myAction = do
a <- getLine
b <- getLine
return $ a ++ b

myAction :: IO String
myAction = (++) <$> getLine <*> getLine

(->)<*> 比较奇怪,看起来像 S 组合子

1
2
3
instance Applicative ((->) r) where  
pure x = (\_ -> x)
f <*> g = \x -> f x (g x)

应用

1
2
ghci> (+) <$> (+3) <*> (*100) $ 5  
508

sequenceA 是一个很有趣的函数,把可遍历的函子转为函子包裹的可遍历结构。

1
2
3
4
5
ghci> :t sequenceA
sequenceA :: (Traversable t, Applicative f) => t (f a) -> f (t a)
-- 或者更具体一点
sequenceA :: (Applicative f) => [f a] -> f [a]
sequenceA = foldr (liftA2 (:)) (pure [])

应用

1
2
3
4
ghci> sequenceA [Just 3, Just 2, Just 1]  
Just [3,2,1]
ghci> sequenceA [(+3),(+2),(+1)] 3
[6,5,4]

A Fistful of Monads

终于到了这个该死的单子了。

monad 的定义有好几种。比如有用 return>>= (bind)定义的

1
2
3
class Monad m where  
return :: a -> m a
(>>=) :: m a -> (a -> m b) -> m b

我更偏向于用 returnjoin 定义

1
2
3
class Monad m where  
return :: a -> m a
join :: m (m a) -> m a

Haskell 中有很多类型都是 Monad 。例如列表,元组,MaybeIO 等。

Monad 可以说是一种设计模式,用层次表达或者记录内容。

Maybe 表达了值可能不存在的情况

[] 表达了有多种可能的情况

IO 表达了与外界交互的情况

应用

1
2
3
4
5
6
ghci> return "WHAT" :: Maybe String  
Just "WHAT"
ghci> Just 9 >>= \x -> return (x*10)
Just 90
ghci> Nothing >>= \x -> return (x*10)
Nothing

因为 Monad 的 >>= 太常用了。所以引入了 do 语法(糖)

1
2
3
4
5
main = do
putStr "Please enter something dirty:"
line <- getLine
let line' = reverse line
putStrLn line'

等价于

1
putStr "Please enter something dirty:" >> getLine >>= \line -> let line' = reverse line in putStrLn line'

所以 <- 其实就是 -> 绑定。

For a Few Monads More

Writer 用于附着日志的 monad

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import Control.Monad.Writer  

gcd' :: Int -> Int -> Writer [String] Int
gcd' a b
| b == 0 = do
tell ["Finished with " ++ show a]
return a
| otherwise = do
tell [show a ++ " mod " ++ show b ++ " = " ++ show (a `mod` b)]
gcd' b (a `mod` b)

ghci> mapM_ putStrLn $ snd $ runWriter (gcd' 8 3)
8 mod 3 = 2
3 mod 2 = 1
2 mod 1 = 0
Finished with 1

Reader 用于处理函数的 monad 。也就是说函数也是一种 monad

1
2
3
instance Monad ((->) r) where  
return x = \_ -> x
h >>= f = \w -> f (h w) w

>>= 的实现看起来很奇怪。先看例子

1
2
ghci> ((+2) >>= (+)) 1
4

>>= 的签名 (>>=) :: Monad m => m a -> (a -> m b) -> m b 看出

h >>= f 也就是 hm a(-> a) ,是一个 monad 值,而这种 monad 是函数。

f 也就是 (a -> mb) ,说明 f 是一个能够返回 monad 值的函数,这个返回的 monad 是一个函数。

(+2) >>= (+) 中,h(+2)f(+)

h >>= f = \w -> f (h w) w

1
2
3
4
5
6
(+2) >>= (+)
\w -> f (h w) w
\w -> f ((+2) w) w
\w -> f (w+2) w
\w -> (+) (w+2) w
\w -> ((w+2) + ) w

最后的结果是一个 monad ,这种 monad 是函数 \w -> ((w+2) + ) w

有些常用的辅助函数

1
2
3
4
5
6
7
8
9
10
11
12
ghci> (+1) $ 2
3
ghci> (+1) <$> [1..10]
[2,3,4,5,6,7,8,9,10,11]
ghci> fmap (+1) [1..10]
[2,3,4,5,6,7,8,9,10,11]
ghci> liftM (+1) [1..10]
[2,3,4,5,6,7,8,9,10,11]
ghci> (+) 1 2
3
ghci> liftM2 (+) [1] [1..10]
[2,3,4,5,6,7,8,9,10,11]

最后

有些章节没好好看就跳过了所以没有笔记。