Haskell 期中复习
Haskell 复习
Functional Thinking
讲了加法乘法 exp 阶乘 fib 用 succ 定义;指出这些函数都可以用 foldn(\(n\) 次作用一个函数)定义,不过阶乘和 fib 需要返回一个二元组。
fold 可以拓展到 list 上,这就引出了许多其它函数。这里有一个 rev 函数,需要借助 revm:
revm (xs) (y:ys) = revm (y:xs) (ys)
revm (xs) [] = xs
rev = revm []
接下来就是 foldlr,foldll:
foldlr :: (A -> B -> B) -> B -> [A] -> B
foldlr h c [] = c
foldlr h c (x:xs) = h x (foldlr h c xs)
foldll :: (A -> B -> B) -> B -> [A] -> B
foldll h c [] = c
foldll h c (x:xs) = foldll h (h x c) xs
这里还很容易搞错,其实是看你希望实现什么功能。比如 foldll 就是 c,x,x,x,...。
这几个函数结合一些简单函数已经可以实现 rev、filter、快排等了。
Meeting Haskell
not'' :: Bool -> Bool
not'' x = if x == True then False else True
-- conditional expression
not''' :: Bool -> Bool
not''' x | x == True = False
| x == False = True
-- guarded equation
not'''' :: Bool -> Bool
not'''' x | x = False
| otherwise = True
-- guarded equation
and'' :: Bool -> Bool
and'' True True = True
and'' _ _ = False
-- pattern matching
(x +), (+ y) 都是合法表达式。div 是函数,而 div 可以是运算符。/ 是小数除法,div 是整除。
Numeric.Natural 中,Natural:任意精度自然数。
foldn :: (a -> a) -> a -> Natural -> a
foldn h c 0 = c
foldn h c n = h (foldn h c (n - 1))
首字母小写 (a) 表示类型变量,首字母大写表示类型。
函数调用优先级最高;函数调用是左结合的。
$ 运算符可以调整优先级,原理如下:首先,($) :: (a -> b) -> a -> b,而且其为运算符,这就意味着 f $ x = f x。而它的优先级最低,所以 f $ g x = (f) $ (g x) = f (g x)(两边都在其之前计算)。而且其右结合,故 f $ g $ h x = (f) $ (g) $ (h x) = f (g (h x))。
. 运算符:f $ g x = (f.g) x。注意函数调用优先级高,所以 f.g x = f.(g x)!
在实际定义函数时,if then else 和模式匹配都用得很多,这里再展示一例 guarded equations:
filter' :: (a -> Bool) -> [a] -> [a]
filter' p [] = []
filter' p (n:ns) | p(n) = n : filter' p ns
| otherwise = filter' p ns
注意条件方程组可以在任何时候跳出来,可以嵌套,还要注意 otherwise 咋用。
let A in B:A 是一些等于组成的语句,A 的量只能在 B 中访问。B where A 通常和上述等价。但注意条件方程组:
f x y | ... = g z
| ... = h z
| otherwise = k z
where z = p x y
这时不能轻易改写为 let in。
交互式程序里有一些常用系统函数,如
show :: a -> String:把一个值变成字符串。read :: String -> a:与 show 相反,但是为了让程序知道 a 的类型,常常需要写成read str :: Integer。getLine, putStrLn:输入输出一行。
Identifiers:首字母为字母,后面数字、下划线、单引号。函数、变量、类型变量必须小写开头,类型必须大写开头。
下面的 IO 内容先看完后面再说,
关于缩进:layout-sensitive(缩进表示层次)和 layout-insensitive(括号表示层次)。layout-sensitive => 相同缩进,开始新语句;更多缩进,继续上一条语句;更少缩进,代码块结束。
还有一种奇怪的写法,注释直接写,但是程序内容要用 >。
Type and Type Class
e :: T 表示 e 是 T 类型的,Haskell 可以 type inference。
常见 Type:
type String = [Char]
Int -- [-2^63, 2^63-1]
Integer -- arbitary precision
Word -- [0, 2^64-1]
Natural -- 见上
Float -- single precision
Double -- double precision
[T] -- list T
接下来还有 function type。curried function 就是 ((a, b) -> c) -> (a -> b -> c)。
Polymorphic function 就是存在类型变量的函数,例如 length :: [a] -> Int。Overloaded function 就是有 type class constraints 的 function。例如
(+) :: Num a => a -> a -> a
-- => 表示“推出”
Eq Ord 和 Num 是常见 type class。
- Eq 需要定义
(==)或(/=)。 - Ord 需要定义
compare或者(<=)。这里,compare返回LT, GT, EQ中的一个。 - Num 需要定义加,乘,abs,signum(sign of a number),fromInteger,和 negate(或者减)。
Function Definition
还是先讲 conditional expressions; guarded equations; pattern matching。注意 patterns are matched in order.
list pattern 用 (:) 来描述,head (x:_) = x。tuple pattern 也可以任意换为 _,例如 snd (_, y) = y。注意名字分别是 fst, snd。
Lambda functions:不用给出名称,例如 \x -> x + x。同时,还支持 currying:add = \x -> (\y -> x + y)。
List Comprehension
格式为 [x^2 | x <- [1..5]],例如这等价于 [1, 4, 9, 16, 25]。若有 multiple generator,可以看成先遍历前面的。原因在后面的 list monad 会提。
还可以 dependent generator:[(x, y) | x <- [1..3], y <- [x..3]];concat :: [[a]] -> [a],concat xss = [x | xs <- xss, x <- xs]。
还可以 guards。[x | x <- [1..10], even x]。
zip 很有用,例如可以获取下标:
positions :: Eq a => a -> [a] -> [Int]
positions x xs = [i | (x', i) <- zip xs [0..], x == x']
String comprehension 和 list 没有区别,注意三个函数(定义于 Data.Char):ord, chr, isLower。ord :: Char -> Int 为编码值,chr 为其反函数。
Recursive Function
讲到 Haskell 支持多种多样的递归:多参数的,list 上的,别的结构上的,互相递归(even 和 odd)等。没有新东西。
Higher-order Function
Higher-order Function 就是接受函数作为参数或者返回函数的 function。例如 (.) :: (b -> c) -> (a -> b) -> (a -> c)。这里就讲到 map 和 filter,又重提 foldl 和 foldr。再比如,all 表示是不是全都满足:all p xs = and [p x | x <- xs]。还有 takeWhile 和 dropWhile。
Type and Type Class
type 可以给已有的类型重命名,例如 type String = [Char] 或者 type Nat = Int。type 还可以有 parameter,丽日 type Pair a = (a, a)。
注意,type 定义的类型终究是已有的;因此,type declaration can be nested but not recursive。
data 可以定义全新的类型,例如 data Bool = False | True(Bool 叫做 type constructor,False, True 叫做 data constructor,都必须大写字母开头)。它与 context free grammar 有深刻联系,在 parser 一节可以看到这一点。data 也能有 parameter,data Shape = Circle Float | Rect Float Float。注意这里的 type constructor 可以看成函数:Circle :: Float -> Shape。
一个例子:
data Maybe a = Nothing | Just a
data Nat = Zero | Succ Nat
nat2int :: Nat -> Int
nat2int Zero = 0
nat2int (Succ n) = 1 + nat2int n
data Expr = Val Int | Add Expr Expr | Mul Expr Expr
-- 例如,Mul :: Expr -> Expr -> Expr
注意优先级:函数调用 (i.e Just a, Succ Nat) 最先。
newtype 定义:若一个 type 只有一个 constructor 和一个 argument,就可以写 newtype。例如
newtype Nat = M Int -- correct
newtype Nat' a = N a -- correct
(N "a") :: Nat' String -- correct
newtype Na' a = N Int a -- wrong, has 2 arguments
type class 的实现方式如下:
class Eq a where
(==), (/=) :: a -> a -> Bool
-- Eq 具有的符号
x /= y = not (x == y)
x == y = not (x /= y)
-- Eq 符号的性质及如果未定义时补全方法
{-# MINIMAL (==) | (/=) #-}
-- 至少要哪些
instance Eq Bool where
False == False = True
True == True = True
_ == _ = False
-- instance 的声明:类簇 + 类型 + where
Type class 之间也有 (=>) 关系,例如 class (Eq a) => Ord a where ..。许多内置类簇可以 derive,如 data Bool = False | True deriving (Eq, Ord, Show, Read)。
An Example: The Countdown Problem
用暴力穷举找到 countdown problem 的解。最有趣的是其中的 combinatorial functions,例如
subs :: [a] -> [[a]] -- 所有子序列
subs [] = [[]]
subs (x:xs) = let yss = subs xs in yss ++ map (x:) yss
还有 interleave(所有插入方法),perms(全排列),choices(部分排列) 等。
用这些实现穷举表达式:先 choices 得到顺序,然后穷举表达式树:找一个地方分开,再枚举操作符拼起来。还讲了常数优化。
Interactive Programming
Haskell 可以容易地实现 Batch Program:给你 input(函数的)然后得到 output。但是,我们希望再从 keyboard 得到 input,然后一部分 output 输出到 screen。大区别在于,这强制 side effects 出现。
所以,为了把这部分也归到 pure function 上来,我们认为:
type IO = World -> World
type IO a = World -> (a, World)
这里,IO a 是 type of actions,返回值是 a。没有返回值,就是 IO (),() 就是零元组,也即一种没有值的类型。
由此可以知道有的函数的类型:
getChar :: IO Char
-- 从和 World 的交互中,返回一个 Char
putChar :: Char -> IO ()
-- 没有返回值
return :: a -> IO a
-- 交互就是什么都不做,但返回一个 a
do 的使用例子:
act :: IO (Char, Char)
act = do x <- getChar
getChar
y <- getChar
return (x, y)
getLine :: IO String
getLine = do x <- getChar
if x == '\n' then
return []
else
do xs <- getLine
return (x:xs)
注意这里 (<-) 的作用,可以看成把 IO a 的 a 取出来。
Monads and More
首先回顾一下两种提升代码抽象层次的方式:之前已经提到了 polymorphic functions,其是 over types。现在,我们在 type constructor 层面更进一级。
Functor 是一类(class)type constructor。其定义如下:
class Functor f where
fmap :: (a -> b) -> (f a) -> (f b)
-- <$> 就是 fmap 的运算符
(<$) :: a -> f b -> f a
(<$) = fmap . const
functor laws 为
fmap id = idfmap (f.g) = fmap f . fmap g
fmap 所做的可以简单地看成“替换包装的内容”,也没有特别深刻的理解。
Applicative 是更进一步的 functor,课件上用如下方式引入(其实已经挺自然了):
fmap0 :: a -> f a
fmap1 :: (a -> b) -> f a -> f b
fmap2 :: (a -> b -> c) -> f a -> f b -> f c
...
我们希望能把这些全看成一种东西,自然就会想,因为 fmap 一层就会把 (curried) 函数包进 f,那能不能直接 f (a -> b) -> f a -> f b 呢?
这就引出了
class Functor f => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b
这样,fmap3 g x y z = g <$> x <*> y <*> z,非常简洁。
例如 maybe applicative 就是直接把里面的结合起来,list applicative 可以定义为笛卡尔积,IO applicative 可以看成顺序做两个操作再结合返回值:
instance Applicative Maybe where
pure = Just
Nothing <*> _ = Nothing
(Just g) <*> mx = g <$> mx
instance Applicative [] where
pure x = [x]
gs <*> xs = [g x | g <- gs, x <- xs]
instance Applicative IO where
pure = return
mg <*> mx = do {g <- mg; x <- mx; return (g x)}
为了提取一个 list 的信息,如下定义 sequenceA:
sequenceA :: Applicative f => [f a] -> f [a]
sequenceA [] = pure []
sequenceA (x:xs) = (:) <$> x <*> sequenceA xs
-- sequenceA [Just 1, Just 2, Just 3] = Just [1, 2, 3]
-- sequenceA [Just 1, Nothing, Just 3] = Nothing
-- sequenceA [一堆序列] = 它们的笛卡尔积
那如果序列的 <*> 不是笛卡尔积(虽然细想肯定是满足要求的,但我觉得不符合直觉)行不行呢?这就引出 applicative laws:
pure id <*> x = x其实就是fmap id = id。pure (g x) = pure g <*> pure x。前两条其实都是对较简单的情况做出的规定。x <*> pure y = pure (\g -> g y) <*> x。这一条很有趣,很多不对的 applicative definition 都栽在这里,因为其要求交换函数和值(其中有一个是 pure)的顺序,不改变最终得到的东西的结构,换句话说(其中有一个是 pure 时)结构的交换律(例如后面的 Expr applicative),类似于这样:pure 返回的是“结构单位元”。x <*> (y <*> z) = (.) <$> x <*> y <*> z。这其实就是结合律。
可以验证,很多“非笛卡尔积”的 <*> 都满足不了第三条。第三条也可以这么理解:打包的函数和打包的值地位是对等的。
接下来,monad 又是什么呢?使用运算符的异常处理这个例子来引入。
一个比较简单的想法是
safediv :: Int -> Int -> Maybe Int
safediv _ 0 = Nothing
safediv n m = Just (div n m)
但如果要对它 evaluate 呢?看看这个程序:
eval :: Expr -> Maybe Int
eval (Div x y) = safediv <$> eval x <*> eval y
这个程序有问题,因为其最终结果的类型变成 Maybe Maybe Int 了!safediv 套了一层 Maybe,最终结果又套了一层 Maybe。当然可以写一个函数 f :: Maybe a -> a,但是不够简洁。
这时候就引入了 monad 的 bind 操作符:
(>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b
mx >>= f = case mx of
Nothing -> Nothing
Just x -> f x
eval :: Expr -> Maybe Int
eval (Div x y) = eval x >>= (\n -> (eval y >>= (\m -> safediv n m)))
-- 所有 lambda 函数的参数全是 int,增加了可读性和简洁性!
注意这里 case of 语句的用法。
接下来来点语法糖,定义 a <- b 表示 b >>= \a ->,则上面的东西可以写成
eval :: Expr -> Maybe Int
eval (Div x y) = do n <- eval x
m <- eval y
safediv n m
这个定义的理解是很容易的:b >>= \a -> 就是对 b 里面的东西做一个操作,在后面的操作里令这东西叫 a,相当于把 b 里面的东西提取出来叫做 a。形式化地说:
a >>= f等价于do v <- a;f v。a >> b等价于do a;b。
这里,(>>) :: m a -> m b -> m b m >> k = m >>= const k
例如,list monad:
instance Monad [] where
xs >>= f = [y | x <- xs, y <- f x]
Haskell 中不能把 type 定义成 monad,只能把 data 或者 newtype 定义成 monad。考虑下面的 ST(状态变换器),返回 a 类型信息和一个新状态:
newtype ST a = S (State -> (a, State))
app :: ST a -> State -> (a, State)
app (S f) s = f s
现在我们希望把它变成 monad,以支持“顺序操作”。直观感受一下以下几个函数的意义:
fmap :: (a -> b) -> ST a -> ST b:新的状态变换器就是对原来的结果再施加一个函数。pure :: a -> ST a:不改变状态,返回这个a。(<*>) :: ST (a -> b) -> ST a -> ST b:先把状态过第一个 ST,再过第二个,结果就是第一个的结果($)上第二个。(>>=) :: ST a -> (a -> ST b) -> ST b:先把状态过第一个 ST,把这个的状态过这个 ST 的结果得到的 ST。
问题:(<*>) 为什么一定要左结合,能否右结合?其实也可以,Applicative 本来就不唯一。
这个 State Monad 有啥用?下面给出例子:树的标注。考虑这样定义的树:
data Tree a = Leaf a | Node (Tree a) (Tree a) deriving Show
希望将其每个叶子标注一个从 0 开始不重复的编号,也即实现
relabel :: Tree a -> Tree Int
注意我们递归过程中要维护当前标到了几号,“几号”就是 State,“标好号的树”就是返回值,故可以用 Applicative 如下实现:
type State = Int
fresh :: ST Int
fresh = S $ \n -> (n, n + 1)
-- 返回状态并将状态加一
alabel :: Tree a -> (ST Tree Int)
alabel (Leaf _) = Leaf <$> fresh
-- 在叶子处调用上述变换,并将结果 fmap 成树
alabel (Node l r) = Node <$> alabel l <*> alabel r
-- 这个写法上面 safediv 已经见过了
relabel t = fst $ app (alabel t) 0
当然也可以写成 Monad:
mlabel :: Tree a -> (ST Tree Int)
mlabel (Leaf _) = do n <- fresh
return (Leaf n)
-- 在叶子把上述变换里面的状态取出来变成树
mlabel (Node l r) = do l' <- mlabel l
r' <- mlabel r
return $ (Node l' r')
-- 这个写法上面 safediv 已经见过了
relabel t = fst $ app (mlabel t) 0
这样至少让代码清晰很多。
下面是 Monad laws:
- Left identity:
return a >>= h = h a。 - Right identity:
mx >>= return = mx。 - Associativity:
(mx >>= g) >>= h = mx >>= (\x -> g x >>= h)。
下面引入一个新的、让事情更简洁的操作符,(>=>):the monad-composition operator。
(>=>) :: Monad m => (a -> m b) -> (b -> m c) -> (a -> m c)
f >=> g = \x -> f x >>= g
那此时 Monad laws 可以写成:
return >=> h = hf >=> return = f(f >=> g) >=> h = f >=> (g >=> h)
非常简洁。这其实是很自然的,因为控制流程本来就应该有这些性质。换句话说,f 构成幺半群,return 是单位元!
根据这些 law,我们能推出
do {x' <- return x; f x'}等价于do {f x},也就是<- return是没意义的。do {x' <- x; return x'}等价于do {x},也就是u <- ...; return u是没意义的。do {y <- do {x <- mx; f x}; g y}等价于do {x <- mx; y <- f x; g y}。
就算不变换也很好理解。但注意,其实程序某种意义上是“从下往上”展开的!
结合上 let,我们有 do {let A; B} 等价于 let A in do B。
上面我们用 Applicative 实现了 sequenceA,用 Monad 来写就是
sequence :: (Monad m) => [m a] -> m [a]
-- 把一系列计算一起做了,返回结果的 list
sequence [] = return []
sequence (x:xs) = do v <- x
vs <- sequence xs
return (v:vs)
这里头脑中想着 State Monad,就会很清晰(不需要把自己绕到具体实现上)。IO Monad 也就是一种特殊的 State Monad 罢了。
到这里内容就完了。作业中有一个语法问题:(->) a 是个 Type constructor,它接受一个 b,返回 a -> b 这个 type。
Monadic Parser
希望写一个解析器,给一个字符串,从前往后解析表达式。本题中,希望 Parser 类型是 newtype Parser a = P (String -> [(a, String)]),a 是返回值,String 是剩下的字符串。之所以是数组是因为若计算失败了返回空数组就行了(注意数组在 Applicative, Monad 意义上类似于 Maybe 的严格超集)。
由此就能实现很多简单的函数了,例如
parse :: Parser a -> String -> [(a, String)]
parse (P f) program = f program
item :: Parser Char
item = P (\program -> case program of
[] -> []
(x:xs) -> [(x, xs)])
-- parse item "abc" = [('a', "bc")]
我们发现 parser 的结构天然就很适合 do:
three = do {x <- item; item; z <- item; return (x,z)}
-- parse three "abcdef" = [(('a', 'c'), "def")]
sat :: (Char -> Bool) -> Parser Char
sat p = do x <- item
if p x then return x else empty
但为了处理不定长的整数等,我们需要 Alternative 这种 making choices 的 type class。其实就是一个幺半群,定义如下:
class Applicative f => Alternative f where
(<|>) :: f a -> f a -> f a
-- 有结合律
empty :: f a
-- 幺元
many :: f a -> f [a]
many v = some v <|> pure []
-- 0 or more:先把这里面的计算尝试 1 or more 次,不行就返回空
some :: f a -> f [a]
some v = (:) <$> v <*> many v
-- 1 or more:无论如何都要先尝试一次计算,再接上 0 or more
注意这里 many 和 sum 定义相互递归,但在之后用的时候就能看出它总会结束。
那在 parser 里面这有什么用呢?可以用 empty 表示不成功,而 (<|>) 运算符就用来判断表达式的这一项是数字、乘积、子表达式中的哪一种。所以作出如下定义:
instance Alternative Parser where
empty = P (const [])
p <|> q = P $ \program -> case parse p program of
[] -> parse q program
res -> res
-- 取两者中先有结果的一个
结合 Alternative 里的 some, many 和 sat(item)就可以写出很多 parser 了:读整数,读字母,读 token,读空格……例如读自然数:
digit :: Parser Char
digit = sat isDigit
nat :: Parser Int
nat = do xs <- some digit -- xs 是 Parser [Char],返回的就是 String
return (read xs) -- 库函数 read 把 String 里的东西读出来
下面我们看表达式的语法,例如其中一项是
expr ::= term '+' expr | term
这个语法直接就可以翻译成 Haskell!
expr :: Parser Int
expr = do t <- term
do symbol "+"
e <- expr
return (t + e)
<|> return t
-- 如果前者可行(还能读到 + 和 expr)就读,否则就读一 term
最后的 eval 函数也相当简单:
eval :: String -> Int
eval xs = fst $ head $ parse expr xs
Foldables and Friends
Haskell 中定义了半群和幺半群:
class Semigroup a where
(<>) :: a -> a -> a
mappend = <> -- 别名
-- 需要有结合律
class Semigroup a => Monoid a where
mempty :: a
-- 幺元
例如 Maybe Monoid:
instance Semigroup a => Semigroup (Maybe a) where
Nothing <> b = b
a <> Nothing = a
Just a <> Just b = Just (a <> b)
instance Semigroup a => Monoid (Maybe a) where
mempty = Nothing
如果一个东西有结合律,那就可以对他在序列上 fold 了!例如如下函数:
fold :: Monoid a => [a] -> a
fold [] = mempty
fold (x:xs) = x <> fold xs
将其拓展,例如树上也可以按照先序遍历 fold,我们如下定义 Foldable:
class Foldable t where
fold :: Monoid a => t a -> a
foldMap :: Monoid b => (a -> b) -> t a -> b
foldr :: (a -> b -> b) -> b -> t a -> b
foldl :: (b -> a -> b) -> b -> t a -> b
那这样对于一棵树,也可以把它变得 foldable:
data Tree a = Leaf a | Node (Tree a) (Tree a) deriving Show
instance Foldable Tree where
fold (Leaf x) = x
fold (Node l r) = fold l <> fold r
foldMap f (Leaf x) = f x
foldMap (Node l r) = foldMap l <> foldMap r
foldr f v (Leaf x) = f x v
foldr f v (Node l r) = foldr f (foldr f v r) l
-- foldr 是从右往左
foldl f v (Leaf x) = f v x
foldl f v (Node l r) = foldl f (foldl f v l) r
-- foldl 是从左往右
-- foldl foldr 要小心,两个函数稍不注意就会定义成一样的
foldable 至少需要定义 foldMap 或者 foldr,例如
fold = foldMap idfoldMap f = foldr (mappend . f) mempty(mappend就是<>)
如果我们想把一个 Foldable 东西里面的信息提出来,例如
traverse :: (a -> Maybe b) -> [a] -> Maybe [b]
traverse g [] = pure []
traverse g (x:xs) = (:) <$> g x <*> traverse g xs
-- 只要有一个变成 Nothing 就是 Nothing
为了把这个定义泛化(例如 Tree 里的信息也能提取),定义 Traversable 为
class (Functor t, Foldable t) => Traversable t where
traverse :: Applicative f => (a -> f b) -> t a -> f (t b)
这里可以想着 list 和 Maybe 的例子理解。
traverse 也可以用之前的 sequenceA 定义,两者是互推的:
sequenceA = traverse id
-- 式子是显然的,但注意这要求传进去的东西本身就是 t (f a),回忆 list 和 Maybe
traverse g = sequenceA . fmap g
-- 先把 g fmap 上去,再 sequenceA
Lazy Evaluation
计算策略有两种:call-by-value(自内而外)和 call-by-name(自外而内)。Haskell 的惰性求值是自外而内加上 sharing(记忆化),如
square (1 + 2)
= (1 + 2) * (1 + 2)
= 3 * 3 -- 这里只算了一次 1+2
= 9
但用 $!,可以强制自内而外,有时可以减少空间:
square $! (1 + 2)
= square 3
= 3 * 3
= 9
$! 也可以嵌套,如
(f $! x) y先算 x(f x) $! y先算 y(f $! x) $! y会把 x 和 y 都算出来

浙公网安备 33010602011771号