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"啊

原来是"/"和"-"是左结合的,而我们前面的定义事实上是利用结合律把"+"和"*"按右结合做的,但"/"和"-"可没有结合律啊

预知后事如何,且听下回分解

Next Part

posted @ 2022-10-19 17:08  deaf  阅读(129)  评论(0编辑  收藏  举报