# Monad专题
在[函子专题](/haskell/专题/函子专题)中已经简要介绍了`Monad`类型类的定义与用法。本章将针对`Monad`展开,讲解有关`Monad`更多有关特性与用法。
## `Monad`介绍
在Haskell中,monad经常被认为是计算的抽象描述符,本质上这些描述符可以进行组合,使用较小的表达式构建更大的表达式[[1]](#ref1)。Monad可以作为各种数据或控制结构的标准编程接口[[2]](#ref2)。
> 补充: 这里的表达式有时也被称为“动作”(actions)
monad具有三个属性,这些属性使其非常高效有用:
- 模块化: monad允许计算由更简单的计算组合而合成,并将组合策略与正在执行的实际计算分开。
- 灵活性: monad使得其函数式程序相比等价的不适用单子书写的程序更具有适应性。monad将计算策略集中到一个位置,而无需分散在整个程序中。
- 隔离性:monad可以用于创建命令式计算结构,这些结构与程序主体保持安全的隔离。[[2]](#ref2)
`Monad`类型类定义如下:
```hs
class Applicative m => Monad m where
(>>=) :: m a -> (a -> m b) -> m b
(>>) :: m a -> m b -> m b
return :: a -> m a
fail :: String -> m a
{-# MINIMAL (>>=) #-}
```
> 注意: 在新版本中,`fail`函数已经被移出Monad类型类中,而是移植到单独创建的MonadFail类型类;因此对于使用新版本的用户来说,应当注意这一点
通过定义,我们知道一个monad前提必须是一个应用函子,然而仅仅实现了某个类型的单子类型类的实例并不能保证其为真正monad。在此基础上,还应当满足以下单子律:
- 左单位元: `return a >>= k = k a`
- 右单位元: `m >>= return = m`
- 结合律:`m >>= (\x -> k x >>= h) = (m >>= k) >>= h`
## 常见的 monad
下面依次介绍几个Haskell内置的 monad,并验证其确实满足单子律。
### `Identity` monad
`Identity` monad 作为最简单的具有单子性质的类型,它仅仅定义了一个容器,以容纳某个类型的值。其定义如下:
```hs
newtype Identity a = Idnetity {runIdentity :: a}
instance Monad Identity where
Identity m >>= k = k m
```
> 提示:`Identity`可从`Control.Monad.Identity`导入,或者从定义处`Data.Functor.Identity`导入
下面证明其满足单子律。
对于左单位元,有:
```hs
左边 = return a >>= k
= Identity a >>= k
= k a = 右边
```
对于右单位元,令`m = Identity a`:
```hs
左边 = m >>= return
= Identity a >>= return
= return a
= Identity a = m = 右边
```
对于结合律,仍然令`m = Identity a`:
```hs
左边 = m >>= (\x -> k x >>= h)
= Identity a >>= (\x -> k x >>= h)
= k a >>= h
= (Identity a >>= k) >>= h
= (m >>= k) >>= h = 右边
```
因此,`Identity`满足单子律。
### `Maybe` monad
`Maybe` monad定义如下:
```hs
instance Monad Maybe where
Nothing >>= _ = Nothing
(Just a) >>= f = f a
```
下面我们来验证其满足单子律。
对于左单位元,要证`return a >>= k = k a`,其中`k`的类型为`a -> Maybe b`。
```hs
左边 = return a >>= k
= Just a >>= k
= k a = 右边
```
对于右单位元,要证`m >>= return = m`,其中`m`的类型为`Maybe a`。
```hs
当 m = Nothing 时:
左边 = m >>= return
= Nothing >>= return
= Nothing = m = 右边
当 m = Just a 时:
左边 = m >>= return
= Just a >>= return
= return a
= Just a = m = 右边
```
对于结合律,要证`m >>= (\x -> k x >>= h) = (m >>= k) >>= h`,其中`m`的类型为`Maybe a`,`k`的类型为`a -> Maybe b`,`h`的类型为`b -> Maybe c`。
```hs
当 m = Nothing 时:
左边 = m >>= (\x -> k x >>= h)
= Nothing >>= (\x -> k x >>= h)
= Nothing
= Nothing >>= h
= (Nothing >>= k) >>= h
= (m >>= k) >>= h
= 右边
当 m = Just a 时:
左边 = m >>= (\x -> k x >>= h)
= Just a >>= (\x -> k x >>= h)
= k a >>= h
= (Just a >>= k) >>= h
= (m >>= k) >>= h
= 右边
```
综上,`Maybe`满足成为monad的条件。
最后,我们给出一个示例:假设我们拥有若干字符串,我们首先需要将其合并后,并计算其中非字母字符的数量。
```hs
-- code'2.hs
import Data.Char(isAlpha)
string1 = Just "My name is "
string2 = Just "L fried."
-- just joking (Alfred)
string3 = Just "The textbook seems a mess 2 readers !"
maybeMonadDemo :: Maybe Int
maybeMonadDemo =
string1 >>= \x ->
(x ++) <$> string2 >>=
\y ->
(y ++) <$> string3 >>=
\z ->
Just ( foldr (+) 0 (((\x -> if x then 0 else 1) . isAlpha) <$> z))
-- 等效于
maybeMonadDemo' :: Maybe Int
maybeMonadDemo' =
string1 >>= \x ->
string2 >>= (
\y ->
string3 >>= (
\z ->
Just ( foldr (+) 0 (((\x -> if x then 0 else 1) . isAlpha) <$> z))) . (y ++)) . (x ++)
```
尝试在GHCi中运行查看结果:
```bash
Prelude> :load code'2.hs
[1 of 1] Compiling Main ( code'2.hs, interpreted )
Ok, one module loaded.
Prelude> maybeMonadDemo
Just 14
```
### `List` monad
`List` monad 定义如下:
```hs
instance Monad [] where
xs >>= f = concatMap f xs
```
其中`concatMap`函数类型为`Foldable t => (a -> [b]) -> t a -> [b]`,当`xs`有多于一个元素时,该函数会将`f`对每个元素映射的结果进行拼接。
下面我们来验证其满足单子律。
对于左单位元,要证`return a >>= k = k a`,其中`k`的类型为`a -> [b]`。
```hs
左边 = return a >>= k
= [a] >>= k
= k a = 右边
```
对于右单位元,要证`m >>= return = m`,其中`m`的类型为`[a]`。
```hs
当 m = [] 时
左边 = m >>= return
= concatMap return m
= concatMap return []
= [] = m = 右边
当 m = (x:xs) 时,假设对于xs,右单位元成立,即`m1 >>= return = concatMap return xs = m1`成立
左边 = m >>= return
= concatMap return m
= concatMap return (x:xs)
= return x ++ concatMap return xs
= [x] ++ concatMap return xs
= [x] ++ (m1 >>= return)
= [x] ++ xs -- 根据归纳假设
= x:xs = m = 右边
```
对于结合律,要证`m >>= (\x -> k x >>= h) = (m >>= k) >>= h`,其中`m`的类型为`[a]`,`k`的类型为`a -> [b]`,`h`的类型为`b -> [c]`。
```hs
当 m = [] 时
左边 = m >>= (\x -> k x >>= h)
= concatMap (\x -> k x >>= h) m
= concatMap (\x -> k x >>= h) []
= []
= [] >>= h
= ([] >>= k) >>= h
= (m >>= k) >>= h = 右边
当 m = (x:xs) 时,假设结合律对xs成立,即`xs >>= (\x -> k x >>= h) = (m1 >>= k) >>= h`成立
左边 = m >>= (\x -> k x >>= h)
= concatMap (\x -> k x >>= h) m
= concatMap (\x -> k x >>= h) (x:xs)
= (k x >>= h) ++ (concatMap (\x -> k x >>= h) xs)
= (k x >>= h) ++ (xs >>= (\x -> k x >>= h))
= (k x >>= h) ++ ((m1 >>= k) >>= h)
= (([x] >>= k) >>= h) ++ ((m1 >>= k) >>= h)
= concatMap h ([x] >>= k) ++ concatMap h (m1 >>= k)
= concatMap h (([x] >>= k) ++ (m1 >>= k)) -- 分配律
= concatMap h ((concatMap k [x]) ++ (concatMap k xs))
= concatMap h (concatMap k ([x] ++ xs)) -- 分配律
= concatMap h (concatMap k (x:xs))
= concatMap h (m >>= k)
= (m >>= k) >>= h = 右边
```
这里需要证明的一点是`concatMap _`和`++`是满足分配律的,即`concatMap f l1 ++ concatMap f l2 = concatMap f (l1 ++ l2)`成立。
```hs
当 l1 = [] 时
concatMap f l1 ++ concatMap f l2
= concatMap f [] ++ concatMap f l2
= [] ++ concatMap f l2
= concatMap f l2
= concatMap f ([] ++ l2)
= concatMap f (l1 ++ l2)
当 l1 == (x:xs) 时,假设分配律对长度不大于xs的列表均成立
concatMap f l1 ++ concatMap f l2
= concatMap f (x:xs) ++ concatMap f l2
= concatMap f (x:xs) ++ concatMap f l2
= concatMap f [x] ++ concatMap f xs ++ concatMap f l2 -- 归纳假设
= concatMap f [x] ++ (concatMap f xs ++ conacatMap f l2) -- 结合律 (列表满足半群结构,其中二元运算为拼接操作)
= concatMap f [x] ++ concatMap f (xs ++ l2)
= concatMap f ([x] ++ xs ++ l2) -- 归纳假设
= concatMap f ((x:xs) ++ l2) -- 结合律
= concatMap f (l1 ++ l2)
```
综上,列表满足成为monad条件。
## 更多 monad
接下来我们了解更多的monad,我们将侧重点放在这些monad的功能和使用上。
### do-标记
do-标记是用于构建单子运算的快速记法,任何单子的实例都可以使用do-标记。通过使用do-标记可以使得程序模拟带有命名变量的命令式程序进行单子计算[[3]](#ref3)。
do-标记使用`do`关键字开头,并允许在过程中使用`<-`符号将monad“赋值”(或使用`let ... = ...`进行一般的赋值),最终以一个单子表达式结束。
回顾前面的`Maybe` monad中的示例,`maybeMonadDemo`和等效的`maybeMonadDemo'`看起来都比较抽象而难以理解,使用do-标记可以使其更简洁和方便。
```hs
-- code'2.hs
maybeMonadDoDemo :: Maybe Int
maybeMonadDoDemo =
do
x <- string1
y <- string2
z <- string3
return (foldr (+) 0 (((\x -> if x then 0 else 1) . isAlpha) <$> x ++ y ++ z))
-- 等效于
maybeMonadDoDemo' :: Maybe Int
maybeMonadDoDemo' =
do { x <- string1;
y <- string2;
z <- string3;
return (foldr (+) 0 (((\x -> if x then 0 else 1) . isAlpha) <$> x ++ y ++ z))
}
```
实际上,do-标记只是一种语法糖,在使用`maybeMonadDoDemo`与`maybeMonadDemo`和`maybeMonadDemo'`是完全等效的。
> 注意:应当区分Haskell中的`return`函数与其在其他语言中的区别,在Haskell中`return`是一个函数,其本质上是将值封装为monad;而一般语言中的`return`则直接用于表示函数的返回值
一般地,对于do-标记中的每一行,都有如下大致的转换过程[[3]](#ref3):
```hs
x <- monad 表达式
...
-- 转换后
monad 表达式 >>= (\x -> ...)
```
```hs
monad 表达式
...
-- 转换后
monad 表达式 >>= (\_ -> ...)
```
```hs
let x = 表达式
...
-- 转换后
let x = 表达式 in ...
```
### `IO` monad
`IO` monad 用于控制程序的输入输出,对于Haskell这种纯函数式语言来说,IO操作与其 ***引用透明(referntial transparency)*** 和 ***无副作用(side-effect free)*** 的特性相违背。通过使用`IO` monad 可以将有副作用的IO操作限制在`IO` monad中,从而保证了Haskell的纯度不会被破坏。
> 补充:对于纯函数而言,一旦参数确定,那么结果就唯一确定,因此在任何时候对于纯函数的调用都可以被替换为计算结果,这种特性就是引用透明。
> 提示:本节主要对`IO` monad 作为单子的用法进行一些简要的介绍,侧重点在 monad 而非对IO操作的处理,更多内容读者可以移步 [IO 专题](/haskell/专题/IO%20专题)
在此之前,我们已经了解了`putStrLn`函数用于输出字符串,下面我们将结合`getLine`这一输入函数讲解`IO` monad的使用方法。
`getLine`函数的类型为`IO String`,即进行输入IO操作后返回一个`String`类型的值;对比之下`putStrLn`函数类型为`String -> IO ()`,接受字符串后进行输出操作,但不返回任何值。

> 注意:这里的“返回”指`IO` monad中封装的类型或者说传递给其他monad的数据的类型。因为对于`a >>= b`这种表达式,`a`计算后的值会传递给`b`,这有点类似“返回”这一行为;另外,`IO ()`并非不传递任何值,`()`类型本身拥有唯一的值`()`,因此当我们尝试对`IO ()`进行传值时,我们会得到一个`()`。
一个最简单的应用场景就是先从键盘获取输入后,将输入值进行输出。
```bash
Prelude> getLine >>= putStrLn
hello
hello
Prelude> :{
Prelude| do
Prelude| input <- getLine
Prelude| putStrLn input
Prelude| :}
hello
hello
```
整个过程示意图如下:

`getLine`先通过键盘读取一行字符串,并返回一个`String`类型,这个返回的数据被传入`putStrLn`函数作为参数,之后`putStrLn`将这个字符串输出[[4]](#ref4)。
### `State` monad
在纯函数式语言中,我们无法进行有状态的计算,一种常见的模拟有状态计算的方法是使用`State` monad,将一个状态参数“贯穿”一系列函数[[3]](#ref3)。
`State` monad定义如下:
```hs
-- code'2.hs
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE TupleSections #-}
newtype State s a = State {runState :: s -> (a, s)} deriving (Functor)
instance Applicative (State s) where
pure x = State (x,)
State f <*> State x = State $ \s -> (fst (f s) (fst (x s)),snd (x s))
instance Monad (State s) where
State x >>= f = State $ \s -> let (v,s') = x s in runState (f v) s'
```
> 注意: 这里的`State` monad定义为简化版的定义,实际的`State`类型是`StateT`类型部分参数实例化的别名,类似地,也没有对于`State`的单子实例声明,而是针对`StateT`的实例声明。类型`StateT`位于`Control.Monad.State.Lazy`中[[3]](#ref4),读者可以自行参考。这里使用简化版本并不影响读者对该monad的理解和使用。
我们有`evalState`和`execState`分别用于获取最终结果和最终状态(可以在库中找到,下同),定义如下:
```hs
-- code'2.hs
evalState :: State s a -> s -> a
evalState act = fst . runState act
execState :: State s a -> s -> s
execState act = snd . runState act
```
我们还可以为`State`单子提供一个标准而简单的接口--`MonadState`类型类,其定义如下:
```hs
-- code'2.hs
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE FlexibleInstances #-}
class MonadState s m | m -> s where
get :: m s
get = state (\s -> (s,s))
put :: s -> m ()
put s = state (\_ -> ((),s))
state :: (s -> (a, s)) -> m a
state f = do
s <- get
let (a, s') = f s
put s'
return a
{-# MINIMAL state | get, put #-}
instance MonadState s (State s) where
get = State $ \s -> (s,s)
put s = State $ \_ -> ((),s)
```
其中`get`函数检索状态并将其作为值复制一份;`put`函数只设定状态但不生成值,`state`函数则是对原来的状态和结果进行更新。
下面我们尝试应用`State` monad实现一个栈结构,一个栈结构需要压入栈(push)、弹出栈(pop)以及查看栈顶(peek)三种操作。
首先实现栈数据结构,我们使用列表进行存储。
```hs
-- code'2.hs
type Stack = [Int]
```
对于入栈,应当接受一个`Int`参数,并改变栈的状态,如下:
```hs
-- code'2.hs
push :: Int -> State Stack ()
push x = state $ \xs -> ((),x : xs)
```
对于出栈,应当改变栈的状态,将栈顶元素弹出并作为最终结果,如下:
```hs
-- code'2.hs
pop :: State Stack Int
pop = state $ \(x:xs) -> (x, xs)
```
最后,观察函数不改变状态,但将栈顶元素作为最终结果,如下:
```hs
-- code'2.hs
peek :: State Stack Int
peek = state $ \(x:xs) -> (x, x:xs)
```
下面尝试测试这些栈活动,例如可以使用栈进行加法运算`1 + 2`:
```hs
-- code'2.hs
makestack :: State Stack ()
makestack = do
push 1
push 2
add :: State Stack ()
add = do
a <- pop
b <- pop
let c = a + b
push c
```
`makestack`将两个数字压入栈中,`add`将两个数字弹出栈,并将计算结果重新压入栈中。
```bash
Prelude> evalState (makestack >> add >> peek) []
3
```
### `Reader` monad
当我们需要从一个共享环境中读取信息并进行计算时,但不需要改变环境的状态时,就可以使用`Reader` monad。`Reader` monad可以在这种特定的场景下替代`State` monad,以便表达更清晰容易[[3]](#ref3)。
`Reader` monad 定义如下(同样,这也是简化后的版本):
```hs
-- code'2.hs
newtype Reader e a = Reader {runReader :: e -> a} deriving (Functor)
instance Applicative (Reader e) where
pure a = Reader $ \e -> a
Reader f <*> Reader x = Reader $ \e -> f e (x e)
instance Monad (Reader e) where
(Reader r) >>= f = Reader $ \e -> runReader (f (r e)) e
```
类似`MonadState`,我们也可以写一个`MonadReader`提供一些基础的函数。
```hs
-- code'2.hs
class Monad m => MonadReader e m | m -> e where
ask :: m e
ask = reader id
local :: (e -> e) -> m a -> m a
reader :: (e -> a) -> m a
reader f = do
r <- ask
return (f r)
instance MonadReader e (Reader e) where
local f c = Reader $ \e -> runReader c (f e)
```
其中`ask`函数用于读取当前环境,`local`函数进行环境的局部修改(即不会影响到全局环境)。
由于`Reader`中并没有保存有关环境的信息(相比之下`State`中将`s`存储在元组中),因此`local`只是局部有效。
### `Writer` monad
`Writer` monad 在计算的同时还生成输出,例如日志记录和跟踪。这些输出不是计算的主要结果,但是必要保留的信息,通过使用`Writer` monad可以更简洁地管理输出,不会使主要地计算变得混乱[[3]](#ref3)。
`Writer` monad 的(简化后的)定义如下:
```hs
-- code'2.hs
newtype Writer w a = Writer {runWriter :: (a,w) } deriving (Functor)
instance Monoid w => Applicative (Writer w) where
pure x = Writer (x,mempty)
Writer (fa,w') <*> Writer (a,w) = Writer (fa a,w' `mappend` w)
instance Monoid w => Monad (Writer w) where
Writer (a,w) >>= f = let (a',w') = runWriter (f a) in
Writer (a', w `mappend` w')
```
类似地,我们可以写一个`MonadWriter`类型类,提供一些基础函数。
```hs
-- code'2.hs
class (Monoid w, Monad m) => MonadWriter w m | m -> w where
pass :: m (a, w -> w) -> m a
listen :: m a -> m (a,w)
tell :: w -> m ()
tell w = writer ((),w)
writer :: (a,w) -> m a
writer (a, w) = do
tell w
return a
{-# MINIMAL (writer | tell), listen, pass #-}
instance Monoid w => MonadWriter w (Writer w) where
pass (Writer ((a,f),w)) = Writer (a,f w)
listen (Writer (a,w)) = Writer ((a,w),w)
tell s = Writer ((),s)
```
`tell`函数单纯产生一个输出;`listen`函数将`Writer`中的输出`w`作为值的一部分,连同原来的值一起形成新的值(元组)`(a,w)`,这允许计算能够“听到”输出的内容;`pass`函数将值中函数`f`移除,并作用在输出中。
一般情况下,`pass`会有些麻烦,因为`f`位于值中,通常我们使用一个类似的函数`censor`,该函数将`f`作为一个参数吸收进来,并生成为`pass`函数可以处理的结构。
```hs
-- code'2.hs
censor :: (MonadWriter w m) => (w -> w) -> m a -> m a
censor f m = pass $ do
a <- m
return (a, f)
```
另一个常用的函数是`listens`,它可以看作`listen`函数的升级版, 提供对输出进行处理的功能。
```hs
-- code'2.hs
listens :: (MonadWriter w m) => (w -> b) -> m a -> m (a,b)
listens f m = do
(a,w) <- listen m
return (a,f w)
```
## 增强Monad `MonadPlus`
`MonadPlus`可以认为是`Monad`的增强版,其定义的是满足幺半群的单子类型类,定义如下:
```hs
class (Monad m,Alternative m) => MonadPlus (m :: * -> *) where
mzero :: m a
mplus :: m a -> m a -> m a
```
例如前面的`MonadWriter` 实例就可以使用`MonadPlus`约束替换。
## monad 转换器
***monad 转换器(Monad Transformer)*** 将不同的monad组合起来,使其同时具备多种monad的行为。monad 转换器通过将原始monad构造函数中添加monad参数,以生成新的构造函数,从而得到组合的单子类型[[3]](#ref3)。
### IdentityT monad 转换器
我们首先介绍最简单的转换器以便读者能够有一个简单的认识,`IdentityT` 是 `Identity` 的转换器,其原理为添加了容纳单子参数的`m`,如下:
```hs
-- code'2.hs
newtype IdentityT m a = IdentityT { runIdentityT :: m a } deriving (Functor)
```
我们为其声明单子实例:
```hs
-- code'2.hs
instance Applicative m => Applicative (IdentityT m) where
pure a = IdentityT $ pure a
IdentityT mf <*> IdentityT ma = IdentityT (mf <*> ma)
instance Monad m => Monad (IdentityT m) where
m >>= k = IdentityT $ do
a <- runIdentityT m
runIdentityT (k a)
```
至此我们可以将`IdnetityT`与其他monad组合,例如`Maybe` monad。
```hs
-- code'2.hs
type IdentityMaybe a = IdentityT Maybe a
```
我们沿用`Maybe` monad中的示例,演示如何使用`IdentityT` monad转换器。
```hs
-- code'2.hs
string1' = IdentityT string1
string2' = IdentityT string2
string3' = IdentityT string3
identityMaybeMonadTDemo :: IdentityMaybe Int
identityMaybeMonadTDemo =
do
x <- string1'
y <- string2'
z <- string3'
return (foldr (+) 0 (((\x -> if x then 0 else 1) . isAlpha) <$> x ++ y ++ z))
```
### monad 提升 `MonadTrans`
Haskell中有很多monad 转换器,它们可以归纳出共同的性质,这些性质被封装到了`MonadTrans`类型类中,该类型类位于`Control.Monad.Trans`。
`MonadTrans`定义如下:
```hs
-- code'2.hs
class MonadTrans t where
lift :: Monad m => m a -> t m a
```
其中`lift`意为“提升函数”,它可以将某个monad提升为与某个转换器的组合。
例如前面的`IdentityT`转换器:
```hs
-- code'2.hs
instance MonadTrans IdentityT where
lift m = IdentityT m
```
因此,前面的示例可以写成:
```hs
-- code'2.hs
identityMaybeMoandTDemo' :: IdentityMaybe Int
identityMaybeMoandTDemo' =
do
x <- lift string1
y <- lift string2
z <- lift string3
return (foldr (+) 0 (((\x -> if x then 0 else 1) . isAlpha) <$> x ++ y ++ z))
```
`lift`函数允许我们仅仅定义原始monad值,并自动转换为带有转换器的组合monad类型。
#### `lift` 定律
`lift`函数应当人为保证满足以下定律:
- `lift . return = return`
- `lift (m >>= f) = lift m >>= (lift . f)`
### IO monad 提升 `MonadIO`
由于我们并没有IO monad的转换器,因此当组合的monad行为中需要IO操作时,IO monad往往作为其他转换器的参数,当然也就无法使用`MonadTrans`类型类。
Haskell中提供了一个`MonadIO`用于将IO操作提升的函数`liftIO`,具体定义如下:
```hs
class Monad m => MonadIO m where
liftIO :: IO a -> m a
{-# MINIMAL liftIO #-}
```
该定义位于`Control.Monad.IO.Class`,一旦我们拥有`MonadIO`实现的转换器(或者说组合monad),就可以将其IO操作提升至该monad。
例如最简单的转换器`IdentityT`。
```hs
-- code'2.hs
import Control.Monad.IO.Class
instance MonadIO m => MonadIO (IdentityT m) where
liftIO = IdentityT . liftIO
```
> 提示:实际上,该实例已经在`Control.Monad.Trans.Identity`中定义
模仿前面的示例,但改为读取三行字符串,并计算其中非字母的字符数。
```hs
-- code'2.hs
identityIOMonadTDemo :: IdentityT IO Int
identityIOMonadTDemo =
do
x <- liftIO getLine
y <- liftIO getLine
z <- liftIO getLine
return (foldr (+) 0 (((\x -> if x then 0 else 1) . isAlpha) <$> x ++ y ++ z))
```
### 多次 monad 提升
类似`IO`这类无法实现转换器的monad有很多,因此一种比较好的方法是将这类monad的性质抽象出来。`MonadBase`位于`transformers-base`库的`Control.Monad.Base`,其内含有函数`liftBase`,能够一次性将monad提升到顶层,而无需多次使用lift函数[[5]](#ref5)。
> 可以使用`cabal install package --lib`全局安装额外的包,或者在项目中导入包(该方式详见IO专题)
```hs
class (Applicative b,Applicative m, Monad b,Monad m) => MonadBase b m | m -> b where
liftBase :: b α -> m α
{-# MINIMAL liftBase #-}
```
Haskell提供了提升的默认递归实现`liftBaseDefault`,即
```hs
-- code'2.hs
import Control.Monad.Base(MonadBase,liftBase)
liftBaseDefault :: (MonadTrans t, MonadBase b m) => b α -> t m α
liftBaseDefault = lift . liftBase
instance (MonadTrans t,MonadBase b m,Monad (t m)) => MonadBase b (t m) where
liftBase = liftBaseDefault
```
> 注意:使用这种递归必须保证递归的基本情况的实例已经被声明,Haskell已经内置了一些实例,如`MonadBase IO IO`等,读者可以自行查阅
使用`IdentityT`嵌套转换器,并使用`liftBase`提升至顶层:
```hs
-- code'2.hs
liftBaseDemo :: IdentityT (IdentityT IO) Int
liftBaseDemo =
do
x <- liftBase getLine
y <- liftBase getLine
z <- liftBase getLine
return (foldr (+) 0 (((\x -> if x then 0 else 1) . isAlpha) <$> x ++ y ++ z))
```
`getLine`类型为`IO String`,根据前面声明的实例,`liftBase`函数将`IO String`先提升为`IdentityT IO String`,然后又提升为 `IdentityT (IdentityT IO) String`,使其能够在`do`标记的语法块中进行运算。
### 更多 monad 转换器
下面列出一些内置的标准monad转换器版本:
| 原始单子 | 原始类型 | 转换器版本 | 组合类型 | 位置 |
|--|--|--|--|--|
| Maybe | Maybe a | MaybeT | MaybeT (m (Maybe a)) | transformers `Control.Monad.Trans.Maybe` |
| Either | Either e a | EitherT | m (Either e a) | transformers-either `Control.Monad.Trans.Either` |
| State | State (s -> (a,s)) | StateT | StateT (s -> m (a, s)) | transformers `Control.Monad.Trans.State.Lazy` |
| Reader | Reader (e -> a) | ReaderT | ReaderT (r -> m a) | transformers `Control.Monad.Trans.State.Lazy` |
| Writer | Writer (a, w) | WriterT | WriterT (m (a, w))| transformers `Control.Monad.Trans.Writer.Lazy` |
|
## `Control.Monad` 常用函数
`import Control.Monad`
读者可以按需查看`Control.Monad`中的函数。
- `mapM`
`mapM`函数的类型为`(Traversable t, Monad m) => (a -> m b) -> t a -> m (t b)`,将一个容器结构`t a`中每个元素`a`映射到一个monad`m b`中,并将这些monad内的元素合并`m (t b)`。
```bash
Prelude> mapM (\x -> Just x) [1,2,3,4]
Just [1,2,3,4]
```
- `mapM_`
类型为`(Traversable t, Monad m) => (a -> m b) -> t a -> m ()`,该函数为`mapM`忽略映射结果的版本。
```bash
Prelude> mapM_ (\x -> Just x) [1,2,3,4]
Just ()
```
- `forM`
`forM`的类型为`(Traversable t, Monad m) => t a -> (a -> m b) -> m (t b)`。`forM`函数是`mapM`的翻转版本,即接受参数的顺序发生调换。
```bash
Prelude> forM [1,2,3,4] (\x -> Just x)
Just [1,2,3,4]
Prelude> (flip mapM) [1,2,3,4] (\x -> Just x)
Just [1,2,3,4]
```
- `forM_`
类似的,`forM_`为`forM`忽略结果的版本。
```bash
Prelude> forM_ [1,2,3,4] (\x -> Just x)
Just ()
```
- `sequence`
`sequence`函数的类型为`(Traversable t, Monad m) => t (m a) -> m (t a)`。 对容器`t (m a)`中的每个monad`m a`中的元素进行合并`t a`并用monad包裹得到`m (t a)`。
```bash
Prelude> sequence [Just 1,Just 2,Just 3,Just 4]
Just [1,2,3,4]
```
- `sequence_`
`sequence`忽略结果的版本,类型为`(Foldable t, Monad m) => t (m a) -> m ()`。
- `=<<`
`>>=`的反转版本。
- `>=>`
`>=>`的类型为`Monad m => (a -> m b) -> (b -> m c) -> a -> m c`。`(bs >=> cs) a`可以理解为`bs a >>= cs`。
- `<=<`
`>=>`的反转版本。
- `forever`
`forever`函数的类型为`Applicative f => f a -> f b`。该函数会无限重复某个行为。
```bash
Prelude> forever $ print 1
1
1
1
...
```
- `join`
`join`类型为`Monad m => m (m a) -> m a`。该函数将一层的单子结构移除,将绑定的参数投影到外层。
```bash
Prelude> join [[1,2],[3,4]]
[1,2,3,4]
```
- `msum`
`msum`函数的类型为`(Foldable t, MonadPlus m) => t (m a) -> m a`。其将一个容器`t (m a)`中的每个monad元素`m a`使用`mplus`函数进行合并,最终得到一个`m a`。
```bash
Prelude> msum [[1,2],[3,4]]
[1,2,3,4]
Prelude> msum [Nothing, Just 1]
Just 1
```
- `mfilter`
`MonadPlus`版本的`filter`函数,类型为`MonadPlus m => (a -> Bool) -> m a -> m a`。
```bash
Prelude> mfilter odd (Just 1)
Just 1
Prelude> mfilter odd (Just 2)
Nothing
```
- `filterM`
`filterM`的类型为`Applicative m => (a -> m Bool) -> [a] -> m [a]`。该函数可以用来生成`filter`函数。
```bash
Prelude> filterM (\x -> [odd x]) [1,2,3,4]
[[1,3]]
```
- `mapAndUnzipM`
该函数的类型为`Applicative m => (a -> m (b, c)) -> [a] -> m ([b], [c])`。它将第一个参数函数作用在第二个参数列表中的每一个元素,此时得到了一系列`m (b,c)`的元素,接着将每个monad元素中元组中两个部分分别进行合并,最终得到`m ([b],[c])`。
```bash
Prelude> mapAndUnzipM (\x -> [(x,x + 1)]) [1,2,3,4]
[([1,2,3,4],[2,3,4,5])]
```
该函数主要用于处理`State` monad或者复杂的数据结果。
- `zipWithM`
`zipWithM`的类型为`Applicative m => (a -> b -> m c) -> [a] -> [b] -> m [c]`,该函数可以用于生成针对列表的函数`zipWith`。
```bash
Prelude> zipWithM (\x y -> Just $ x + y) [1,2] [3,4]
Just [4,6]
```
- `zipWithM_`
忽略结果版本的`zipWithM`。
- `foldM`
`foldM`函数类似`foldl`函数,不同的是结果被封装在了monad中,因此类型为`(Foldable t,Monad m) => (b -> a -> m b) -> b -> t a -> m b`。
```bash
Prelude> foldM (\x y -> return $ x + y ) 0 [1,2,3,4]
10
```
- `foldM_`
丢弃结果版本的`foldM`。
- `replicateM`
`replicateM`重复某个行为n次,类型为`Applicative m => Int -> m a -> m [a]`。
```bash
Prelude> replicateM 2 [1]
[[1,1]]
```
- `replicateM_`
丢弃结果版本的`replicateM`。
- `when`
有条件执行应用函子表达式,类型为`Applicative f => Bool -> f () -> f ()`。
```bash
Prelude> when True (print ())
()
Prelude> when False (print ())
-- 没有输出
```
- `unless`
与`when`执行条件相反。
- 单子提升操作符
`Control.Monad`提供了单子提升的操作符,用于将函数应用在由单子包裹的元素上,并最终将结果包裹在同样的单子中。根据被应用函数的参数数目,分别有`liftM`、`liftM2`、`liftM3`、`liftM4`以及`liftM5`,其中`liftM`的类型为`Monad m => (a1 -> r) -> m a1 -> m r`,`liftM2`的类型为`Monad m => (a1 -> a2 -> r) -> m a1 -> m a2 -> m r`依此类推。
另外,`ap`函数可以用来替换上述的五个提升函数,或者构造用于接受更高参数数目函数的提升函数,其类型为`Monad m => m (a -> b) -> m a -> m b`。对于一个`liftMn`函数(其中`n`表示被应用函数的参数数目),`liftMn f x1 x2 ... xn`等效于`return f \`ap\` x1 \`ap\` ... \`ap\` xn`。
- `(<$!>)`
严格求值版本的`<$>`函数。
-------------------------------------------
[1] Merely monadic. (2021, March 16). HaskellWiki, . Retrieved 02:27, April 20, 2024 from https://wiki.haskell.org/index.php?title=Merely_monadic&oldid=64044.
[2] Monad. (2022, October 22). HaskellWiki, . Retrieved 02:46, April 20, 2024 from https://wiki.haskell.org/index.php?title=Monad&oldid=65405.
[3] All About Monads. (2021, September 19). HaskellWiki, . Retrieved 03:13, April 22, 2024 from https://wiki.haskell.org/index.php?title=All_About_Monads&oldid=64741.
[4] State Monad. (2018, December 21). HaskellWiki, . Retrieved 06:21, April 22, 2024 from https://wiki.haskell.org/index.php?title=State_Monad&oldid=62675.
[5] New monads/MonadBase. (2006, October 26). HaskellWiki, . Retrieved 06:36, May 2, 2024 from https://wiki.haskell.org/index.php?title=New_monads/MonadBase&oldid=7351.