2011-11-03 黄毅

写 parser 的时候需要写这样的代码:

takeTill pred

其中 pred 是个判别函数,签名为: Char -> BooltakeTill 的作用就是一直解析到 pred 返回 True 为止。

比如你要解析到下一个 > 或者 = ,你就写:

takeTill (inClass ">=")

想解析到下一个空白字符为止,就写:

takeTill isSpace

想解析各种空白字符呢,就在上面的基础上取个反:

takeTill (not . isSpace)

这个例子是想提醒大家 isSpace 是个函数,所以这里需要进行函数组合,而不是直接调用 not

那我今天想说的是什么呢。现在我想解析到下一个 >= 或空白字符为止,也就是说需要把前两个拼起来,直接写起来是这样的:

takeTill (\c -> inClass ">=" c || isSpace c)

也不麻烦,只不过对于患有 代码洁癖 的我来说,视觉上还不太给力。于是,我继续重构如下:

takeTill (inClass ">=" ||. isSpace)

多实现一个组合函数 ||.

(||.) :: (a -> Bool) -> (a -> Bool) -> a -> Bool
(||.) f g a = f a || g a

把两个判别函数组合成一个新的判别函数。

为了将它推广到其他的组合操作,我们进一步泛化一个通用函数,姑且取名叫 fn 吧 :

fn :: (b -> b -> b) -> (a -> b) -> (a -> b) -> a -> b
fn op f g a = f a `op` g a

前面的 ||. 就成为:

(||.) = fn (||)

大功告成,正当我准备好好欣赏一下最终的产物 fn 的时候,却突然发现, fn 的作用不就是把 b 层面的二元函数提升到 (a -> b) 层面么,正如 ||||. 都是或操作,只不过一个作用在 Bool 值层面,一个作用在 a -> Bool 判别函数层面。如此通用的概念,我意识到我很可能重造轮子了。

于是我请 lambdabot mm帮我诊断一下:

<huangyi> @pl fn op g k a = g a `op` k a
<lambdabot> fn = liftM2

果然,小mm告诉我, fn 其实就是 liftM2liftM2 是专门用来把二元函数提升到 Monad 中去的,而 ((->) a) 正是 Monad 的实例。

instance Monad ((->) a) where
return = const
-- (>>=) :: (a -> b) -> (b -> a -> c) -> (a -> c)
f >>= g = g . f
liftM2 :: Monad m => (a -> b -> c) -> m a -> m b -> m c
liftM2 op ma mb = do
a <- ma
b <- mb
return (a `op` b)

其实只需要提升一下抽象层次的话,还不需动用 Monad 这样的大杀器, Applicative 也可以搞定。

instance Applicative ((->) a) where
-- <$> :: (b -> c) -> (a -> b) -> (a -> c)
f <$> g = f . g
-- (<*>) :: (a -> b -> c) -> (a -> b) -> (a -> c)
f <*> g = \a -> (f a) (g a)

Applicative 的话,前面的 fn 就等价于 liftA2 了。

liftA2 :: Application f => (a -> b -> c) -> f a -> f b -> f c
liftA2 op fa fb = op <$> fa <*> fb

绕了一圈,最后还是没逃出Haskell最基本的框框。


blog comments powered by Disqus

转载请注明出处,收藏或分享这篇文章到: