liftM

同时使用puer和monadic代码

迄今为止我们看到的Monad好像有一个非常明显的缺陷:Monad的类型构造器把一个值包装成一个monadic的值,这样导致在monad里面使用普通的纯函数有点困难。举个例子,假设我们有一段运行在monad里面的代码,它所做的就是返回一个字符串:

1
ghci> let m = return "foo" :: Logger String

如果我们想知道字符串的长度是多少,我们不能直接调用 length 函数:因为这个字符串被 Logger 这个monad包装起来了,因此类型并不匹配。

1
2
3
4
5
6
7
8
ghci> length m
 
<interactive>:1:7:
    Couldn't match expected type `[a]'
           against inferred type `Logger String'
    In the first argument of `length', namely `m'
    In the expression: length m
    In the definition of `it': it = length m

我们能做的事情就是下面这样:

ghci> :type   m >>= \s -> return (length s)
m >>= \s -> return (length s) :: Logger Int

我们使用 (>>=) 把字符串从monad里面取出来,然后使用一个匿名函数调用 length 接着用 return 把这个字符串重新包装成 Logger 。

由于这种形式的代码经常在Haskell里面出现,因此已经有一个类似的操作符存在了。在 Functor 简介 里面我们介绍了 lifting 这种技术;把一个纯函数 Lift 为一个函子通常意味着从一个带有上下文的特殊值里面取出那个值,然后使用这个普通的值调用纯函数,得到结果之后用特定的类型构造器包装成原来有着上下文的特殊值。

在monad里面,我们需要干同样的一件事。由于 Monad 这个类型类已经提供了 (>>=) 和 return 这两个函数处理monadic的值和普通值之间的转换,因此 liftM 函数不需要知道monad的任何实现细节。

-- file: ch14/Logger.hs
liftM :: (Monad m) => (a -> b) -> m a -> m b
liftM f m = m >>= \i ->
            return (f i)

当我们把一个类型声明为 Functor 这个类型类的实例之后,我们必须根据这个特定的类型实现对应的 fmap 函数;但是, 由于 (>>=) 和 return 对monad的进行了抽象,因此``liftM`` 不需要知道任何monad的任何实现细节。我们只需要实现一次并配上合适的类型签名即可。

在标准库的 Control.Monad 模块里面已经为我们定义好了 liftM 函数。

我们来看看使用 liftM 对于提升我们代码可读性有什么作用;先看看没有使用 liftM 的代码:

-- file: ch14/Logger.hs
charClass_wordy (']':cs) =
    globToRegex' cs >>= \ds ->
    return (']':ds)
charClass_wordy (c:cs) =
    charClass_wordy cs >>= \ds ->
    return (c:ds)

然后我们用 liftM 去掉那些 (>>=)) 和匿名函数:

-- file: ch14/Logger.hs
charClass (']':cs) = (']':) `liftM` globToRegex' cs
charClass (c:cs) = (c:) `liftM` charClass cs

正如 fmap 一样,我们通常用中缀的方式调用 liftM 。可以用这种方式来阅读这个表达式:把右边操作得到的monadic的值应用到左边的纯函数上。

liftM 函数实在是太有用了,因此 Control.Monad 定义了它的几个变种,这些变种可以处理更长的参数;我们可以看一看 globToRegex 这个函数的最后一个分句:

-- file: ch14/Logger.hs
globToRegex' (c:cs) = liftM2 (++) (escape c) (globToRegex' cs)

escape :: Char -> Logger String
escape c
    | c `elem` regexChars = record "escape" >> return ['\\',c]
    | otherwise           = return [c]
  where regexChars = "\\+()^$.{}]|"

上面这段代码用到的 liftM2 函数的定义如下:

-- file: ch14/Logger.hs
liftM2 :: (Monad m) => (a -> b -> c) -> m a -> m b -> m c
liftM2 f m1 m2 =
    m1 >>= \a ->
    m2 >>= \b ->
    return (f a b)

它首先执行第一个动作,接着执行第二个操作,然后把这两个操作的结果组合起来应用到那个纯函数上并包装返回的结果。 Control.Monad 里面定义了 liftM liftM2 直到 liftM5 。

 

liftIO 是不是也可以自己定义一个?

posted @   Gyoung  阅读(169)  评论(0编辑  收藏  举报
努力加载评论中...
点击右上角即可分享
微信分享提示