MaybeT介绍

指引

先来看一个例子,在后端收到一个请求后需要解析该请求的请求头获取token,获取到token后需要从redis中查询该token的用户数据。现在有个现成的方法如下

1
2
3
4
5
6
-- 获取请求头的token
lookupBearerAuth :: Handler (Maybe String)
-- 根据key从redis查询value
getFromRedis :: String -> Handler (Maybe String)
-- 将user的json字符串转为数据
decodeUser :: String -> Handler (Maybe User)

那么现在获取用户信息json的方法如下

1
2
3
4
5
6
7
8
9
10
getUser :: Handler (Maybe User)
getUser = do
mToken <- lookupBearerAuth
case mToken of
Nothing -> return Nothing
Just token -> do
mUserJson <- getFromRedis token
case mUserJson of
Nothing -> return Nothing
Just userJson -> decodeUser userJson

可以发现,因为每一步都存在Nothing的可能性,所以每一步都需要判断,那么有没有什么简写的方式呢?答案是,有的。

实现思路:Monad的隐式操作

Monad的签名如下

1
2
3
4
class Applicative m => Monad m where
(>>=) :: m a -> (a -> m b) -> m b -- bind 操作符
(>>) :: m a -> m b -> m b -- 序列操作
return :: a -> m a -- 注入操作

>>=用于将m a转为m b,其中(a -> mb)是转换的方法。形象的理解就是m a可以当成包含元素a的集合,m b可以当成包含元素b的集合,a -> m b就是将元素a转为包含元素b的集合,用js的命令式写法就是

1
2
3
function >>=(list: a[], fun: (item: a) => b[]): b[]{
return list.flatmap(fun)
}

当然,m不等于数组,这里只是为了形象。
在实现的内容里,我们除了可以使用a -> m b来处理m a中的元素,我们还能添加额外的操作,而这些额外的操作在doreturn语法糖中是看不到的。

下面是一个实现隐式操作的Monad

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
data MyContainer a = MyContainer a

-- 为MyContainer实现Monad方法,定义>>=的逻辑
instance Monad MyContainer where
(>>=) :: MyContainer a -> (a -> MyContainer b) -> MyContainer b -- bind 操作符
MyContainer a >>= f =
-- 额外的操作
doSomeThing
-- 将转换函数应用于a
f a

-- 转换函数
appendB :: String -> MyContainer String
appendB str = MyContainer $ str ++ "B"

-- 使用MyContainer
test :: MyContainer String
test = MyContainer "A" >>= appendB

-- test2是test1的do写法
test2 :: MyContainer String
test2 = do
a <- MyContainer "A"
-- 执行了doSomething隐式操作
appendB a

知道了Monad可以实现隐式操作之后,我们就可以将Nothing的判断放到Monad的隐式操作中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
data MaybeT a = MaybeT
{ maybeTValue :: Maybe a
}

instance Monad m => Monad MaybeT where
return = MaybeT . Just

-- 根据Monad的签名,可以推导出x和f的定义
-- x: MaybeT a
-- f: a -> MaybeT b
x >>= f =
let v = maybeTValue x
in case v of
-- 若value是nothing不处理
Nothing -> return Nothing
Just y -> (f y)

主题:MaybeT

下面是mtl包下的MaybeT的Monad实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
newtype MaybeT m a = MaybeT { 
runMaybeT :: m (Maybe a)
}

-- (MaybeT m)的monad实现
instance Monad m => Monad (MaybeT m) where
return = MaybeT . return . Just

-- 根据Monad的签名,可以推导出x和f的定义
-- x: MaybeT m a
-- f: a -> MaybeT m b
(>>=) x f = MaybeT $ do
v <- runMaybeT x
case v of
Nothing -> return Nothing
Just y -> runMaybeT (f y)

对比上面的设计,这种设计增加了一个Monad参数m。这么做的原因是因为MaybeT 的主要目的是作为 Monad Transformer,使 Maybe Monad 能够与其他 Monad(如 IO、List 等)堆叠。通过定义为 MaybeT m a = m (Maybe a),它可以包裹任何 Monad 上下文 m,使其兼容多种 Monad,而不是仅限于单一的 Maybe 值。

使用MaybeT实现隐式判断nothing

1
2
3
4
5
6
7
getUser :: Handler (Maybe User)
getUser = runMaybeT $ do
token <- MaybeT lookupBearerAuth
-- 隐式执行判断
userJson <- MaybeT $ getFromRedis token
-- 隐式执行判断
decodeUser userJson

总结,Maybe的Monad实现使得在Maybe的上下文中能够自动过滤Nothing,但在m (Maybe a)是在m的上下文,为了使其能实现Maybe上下文相同的效果,通过MaybeT包装m (Maybe a),并为其实现过滤Nothing的逻辑。