haskell学习笔记——关于Monad
前言:haskell真是一门让人又爱又恨的语言,学习起来的难度也是相当之大,重点在于理解概念与每一步递归的进行,即使学了一月有余我依然被各种syntax error所折磨,而且对于一些深入的概念理解总是懂了又忘,半知半解的状态。于是本人打算开几篇随笔记录下对一些难点,我对其的认识逐步完善的过程。
一些概念:Functor、Applicative、Monad
Functor:大致解决了把一个函数作用于一个被context包裹的东西,返回一个新的被context包裹的东西的问题
需要实现函数
fmap :: (a -> b) -> f a -> f b
Applicative:大致解决了把一个被context包裹的函数作用于一个被context包裹的东西,返回一个新的被context包裹的东西的问题
*可以看做一种对高阶函数逐步传入参数的过程
需要实现函数
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b
Monad:大致解决了把一个被context包裹的东西解包,传入一个返回值是被context包裹的东西的函数,得到一个被context包裹的东西的问题
*可以看做把一个被context包裹的东西逐步传入多个函数进行分步处理的过程
需要实现函数
return :: a -> f a --seems to be unnecessary
(>>=) :: f a -> (a -> f b) -> f b
以上是定义部分
对于*部分的解释
g <*> a <*> b,可以看做g这个函数先传入a这个东西得到一个新函数,再传入b这个东西以得到一个新的东西,这全程都在context的包裹中,不会出现把包裹中的数据泄露的情况
a >>= f >>= g,可以看做把a这个东西先传入f这个函数得到一个新的东西,再传入g这个函数得到一个新的东西,这全程都在context的包裹中,不会出现把包裹中的数据泄露的情况
这可能听起来有点抽象,但是它处理side effect真的很自然,以Maybe类型为例,此处略去不表
但以上的部分只是开胃前菜,真正阴间的部分才只露出冰山一角
一些进阶的应用:
一、The State Monad
这部分真的是相当的抽象了
我们先定义ST:
newtype ST a = S (State -> (a, State))
前面的学习告诉我们,这句话的意思是ST a代表着一个S包裹着的一个把State转化成(a, State)的函数
我们再定义把这个函数作用到一个State上:
app :: ST a -> State -> (a, State)
app (S f) s = f s
这部分其实就是把S包裹的f解包出来,作用于s之上
接下来我们先把它定义成functor
fmap :: (a -> b) -> S a -> S b
这里要注意S a、S b是两个函数,在之后的定义中需要牢记这一点
fmap g st = S (\s -> let (x, s') = app st s in (g x, s'))
这个函数的意思不难理解,就是我们的新函数其实是先让S a作用于s,然后再对得到的元组的左边部分施加g函数,来让函数的返回值是(b, State)
这部分是最简单的,后面的Applicative和Monad可能会有点抽象了
接下来我们把它定义成Applicative
pure :: a -> ST a
pure a = S (\s -> (a, s))
就是对一个a,我们让pure a返回的函数不对s进行修改,只是在它旁边加上了a
(<*>) :: S (a -> b) -> S a -> S b
还记得 S (a -> b)其实是一个函数吗?
stf <*> stx = S (\s -> let (f, s' ) = app stf s
(x, s'') = app stx s'
in (f x, s''))
这句话的意思是,我们先把stf中包裹的函数作用于s,得到s',并把(a -> b)的函数f拿出来,然后我们再把stx中包裹的函数作用于s',得到s'',并把f作用于得到的元组的左边部分
还能接受吗
接下来我们把它定义成Monad
(>>=) :: S a -> (a -> S b) -> S b
stx >>= f = S (\s -> let (x, s') = app stx s
in app (f x) s')
这句话的意思是,我们先把stx中包裹的函数作用于s,得到s',并把x拿出来,但注意我们的f的返回值是一个被包裹的函数,我们的返回值就应该是把它作用于x之后的东西再作用于s'啦
以上的部分有点抽象,但后续的应用会更抽象,记得倒回来看定义以加深理解
一个简单的应用:树的重标注
这个东西真是要把我折磨疯了
先给出这里树的定义
data Tree a = Leaf a | Node (Tree a) (Tree a)
deriving Show
tree :: Tree Char
tree = Node (Node (Leaf 'a') (Leaf 'b')) (Leaf 'c')
然后我们用Applicative的ST来实现它
我们先来定义一个看起来没头没尾的函数
fresh :: ST Int
fresh = S (\n -> (n, n+1))
但我们需要弄懂它在干嘛:它是利用传入的n这个State,把元组左边的部分赋值为n,并把State更新为n+1,这很重要
注意到在这里我们的State对元组左边的部分是产生了影响的,这很重要
接下来是我们实现主要功能的函数了
alabel :: Tree a -> ST (Tree Int)
alabel (Leaf _) = Leaf <$> fresh
alabel (Node l r) = Node <$> alabel l <*> alabel r
这里的g <$> x其实等价于pure g <*> x
先看对一个Leaf节点如何处理,我们把Leaf函数复合到fresh函数上了
观察前面Applicative和Tree的定义,我们发现其实就是把元组左边的Int变成了Tree嘛,非常简单
再看看对Node节点的处理,我的妈这是啥,为啥直接复合就做完啦?
这个时候容我们回去看下Applicative的实现,它在实现中是对State一直在修改
等等,我们的State在修改?哦,是在fresh里,它每执行一步就会把State加一,State好像是个计数器,然后Leaf fresh就是新建了一个叶子,然后把count++
有点懂了!
那这个复合是怎么回事呢,我们发现对于State的修改,好像是顺着函数进行来修改的
而Applicative本身是在干什么呢?对,是在把高阶函数分别传参
这就比较能理解了,我们先对左边标号,然后得到Node函数左边的Tree,然后在把State这个计数器继续拿到右边用,对右边标号后传入Node函数右边的Tree
大功告成!
然后我们最后函数就呼之欲出啦,就是把ST(Tree Int)作用于一个State上,这个State是一个计数器,初值为0
relabel' :: Tree a -> Tree Int
relabel' t = fst (app (alabel t) 0)
然后是一个monad的写法,如果你理解了上面Applicative的部分,这部分是完全类似的
mlabel :: Tree a -> ST (Tree Int)
mlabel (Leaf _) = fresh >>= \n -> return (Leaf n)
mlabel (Node l r) = mlabel l >>= \l' ->
mlabel r >>= \r' -> return (Node l' r')
然后用do语法糖搞一下,就非常简洁了(对理解而言其实并不清晰)
mlabel (Leaf _ ) = do n <- fresh
return (Leaf n)
mlabel (Node l r) = do l' <- mlabel l
r' <- mlabel r
return (Node l' r')
感觉理解了这部分以后,下一部分理解起来就没有那么抽象了
二、Monadic Parser
就是实现一个parser(编译器?),把算式转化为表达式树
首先我们先定义一下Parser
newtype Parser a = P (String -> [(a, String)])
parse :: Parser a -> String -> [(a, String)]
parse (P f) program = f program
Parser内包含一个函数,把这个函数作用于一个String上,会返回一个元组的list,其中第一个元素是a类型,表示处理的结果,第二个元素是处理后剩下的string
一个简单而基础的Parser
item :: Parser Char
item = P (\program -> case program of
[] -> []
(x:xs) -> [(x, xs)])
表示取出String中的第一个元素,并把它作为处理结果,这是我们之后进行操作的基础
我们类似地把它定义成Functor,Applicative,Monad
instance Functor Parser where
fmap g p = P (\program -> case parse p program of
[] -> []
[(v, out)] -> [(g v, out)])
instance Applicative Parser where
pure a = P (\program -> [(a, program)])
pg <*> px = P (\program -> case parse p program of
[] -> []
[(g, out)] -> parse (fmap g px) out)
instance Monad Parser where
p >>= f = P (\program -> case parse p program of
[] -> []
[(v, out)] -> parse (f v) out)
类似前面的定义,这部分不难写出和理解
但是我们对未知字符串的解析可能会分多钟情况
所以还要引入一个新的类型,Alternative,用来处理几种操作的选择,它派生于Applicative里,需要实现empty和<|>函数
同时它还附带了两个基于<|>和<*>的函数some和many,在后面会用到
class Applicative f => Alternative f where
-- An associative binary operation
(<|>) :: f a -> f a -> f a
-- The identity of '<|>'
empty :: f a
-- | Zero or more.
many :: f a -> f [a]
many v = some v <|> pure []
-- | One or more.
some :: f a -> f [a]
some v = (:) <$> v <*> many v
然后我们把Parser定义为Alternative
instance Alternative Parser where
empty = P (\program -> [])
p <|> q = P (\program -> case parse p program of
[] -> parse q program
rst -> rst
注意这里与前面不同的是,我们两个parse传入的都是program,这保证了两个操作操作到的字符串是一样的
接下来是一个简单的函数,判断字符串的第一个字符是不是我们想要的
sat :: (Char -> Bool) -> Parser Char
sat p = do x <- item
if p x then return x else empty
至此,我们需要的基本函数就定义完成了
接下来给出一个优美的实例,把monad在异常处理中的优越性很好地展现了出来
char :: Char -> Parser Char
char x = sat (x ==)
string :: String -> Parser String
string [] = return []
string (x:xs) = do char x
string xs
return (x:xs)
char函数表示验证首字符是否为x,string函数表示验证字符串是否具有给定的前缀
可以看到,我们只需要一直char即可,一旦有一个char失败,那么整体的返回值都将为空,这是由我们前面monad的定义决定的
一个简单的练习:算术表达式的解析,只有"+"和"*"
我们的考虑是把表达式按运算优先级分类
--expr ::= term '+' expr | term
--term ::= factor '*' term | factor
--factor ::= digit | '(' expr ')'
--digit ::= '0' | '1' | ... | '9'
数字和带括号的表达式是优先级最高的,其次是"*"连接的,最后是"+"连接的
我们解析的思路也是先提出一个更高优先级的表达式,然后递归处理
代码是容易写出的
import Prelude hiding (Maybe (..))
import Control.Monad
import Control.Applicative
import Data.Char
newtype Parser a = P { parse :: String -> [(a,String)] }
eval :: String -> Int
eval = fst . head . parse expr
item :: Parser Char
item = P (\program -> case program of
[] -> []
(x : xs) -> [(x, xs)])
instance Functor Parser where
fmap g p = P (\program -> case parse p program of
[] -> []
[(v, out)] -> [(g v, out)])
instance Applicative Parser where
pure v = P (\program -> [(v, program)])
pg <*> px = P (\program -> case parse pg program of
[] -> []
[(g, out)] -> parse (fmap g px) out)
instance Monad Parser where
p >>= f = P (\program -> case parse p program of
[] -> []
[(v, out)] -> parse (f v) out)
--many : perform arbitrary times
--some : perform at least 1 time
instance Alternative Parser where
empty = P (\program -> [])
p <|> q = P (\program -> case parse p program of
[] -> parse q program
rst -> rst)
sat :: (Char -> Bool) -> Parser Char
sat p = do x <- item
if p x then return x else empty
digit :: Parser Char
digit = sat isDigit
char :: Char -> Parser Char
char x = sat (x ==)
string :: String -> Parser String
string [] = return []
string (x : xs) = do char x
string xs
return (x : xs)
nat :: Parser Int
nat = do x <- some digit
return (read x)
int :: Parser Int
int = do char '-'
n <- nat
return (-n)
<|> nat
space :: Parser ()
space = do many (sat isSpace)
return ()
token :: Parser a -> Parser a
token v = do space
x <- v
space
return x
natural :: Parser Int
natural = token nat
integer :: Parser Int
integer = token int
symbol :: String -> Parser String
symbol xs = token (string xs)
nats :: Parser [Int]
nats = do symbol "["
n <- natural
ns <- many (do {symbol ","; natural})
symbol "]"
return (n : ns)
expr :: Parser Int
expr = do a <- term
do symbol "+"
b <- expr
return (a + b)
<|> return a
term :: Parser Int
term = do a <- factor
do symbol "*"
b <- term
return (a * b)
<|> return a
factor :: Parser Int
factor = do symbol "("
a <- expr
symbol ")"
return a
<|> natural
但当我们想要加入"/"和"-"的时候,却发现问题没有这么简单
我们发现为啥"1-2-3=2"啊
原来是"/"和"-"是左结合的,而我们前面的定义事实上是利用结合律把"+"和"*"按右结合做的,但"/"和"-"可没有结合律啊
预知后事如何,且听下回分解