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 是不是也可以自己定义一个?
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步