函子和单子之间的等价性

3

人们说单子是应用函子的扩展,但我并不认同这种说法。让我们来看一个应用函子的例子:(<*>) :: f(a->b) -> f a -> f b

[(+3)] <*> [2,3,4]

现在,我也希望能像单子一样做同样的事情,这意味着我可以应用两个参数:一个包含函数的上下文,以及另一个用于获取上下文的上下文。但对于单子来说,我不能这样做。我需要编写一个丑陋的函数,就像这样:

[2,3,4] >>= (\x->[x+3])

当然,你可以说[(+3)]等同于[\x->(x+3)]。但至少,这个函数是有上下文的。
最后,我不认为这里存在等价性或扩展性。Monad是一种不同的风格,并在另一个故事中很有用。
对于我的无知表示抱歉。

1
好的,pure = returnmf <*> ma = mf >>= \f -> liftM f ma - Vitus
4
应用组合子可以用单子组合子来定义。但类似 [2,3,4] >>= \ x -> replicate x x 这样的操作确实需要 >>= 的额外能力,因为每个值都用于选择生成的列表结构,而不仅仅是其中的值。单子操作更加强大,但相应地,并不像应用组合子那么常见。 - pigworker
3个回答

12

如果TMonad的实例,那么您可以按照以下方式将其变成Applicative的实例:

instance Functor T where
    fmap = liftM

instance Applicative T where
    pure = return
    (<*>) = ap

liftM的定义如下:

liftM   :: (Monad m) => (a1 -> r) -> m a1 -> m r
liftM f m1              = do { x1 <- m1; return (f x1) }

ap的定义如下:

ap                :: (Monad m) => m (a -> b) -> m a -> m b
ap                =  liftM2 id

liftM2  :: (Monad m) => (a1 -> a2 -> r) -> m a1 -> m a2 -> m r
liftM2 f m1 m2          = do { x1 <- m1; x2 <- m2; return (f x1 x2) }

因此,“单子是适用函子的扩展”,这意味着任何单子都可以变成一个适用函子。事实上,标准库中的类Monad没有继承自类Applicative被广泛(但不普遍)认为是一个bug。

9
import Control.Applicative

我认为重新使用Monad来定义<*>可以更清晰地说明它的关系:

(>*>) :: Monad m => m (a -> b) -> m a -> m b
mf >*> ma = do
   f <- mf
   a <- ma
   return (f a)

产生与<*>相同的结果:

*Main> [(+3)] >*> [2,3,4]
[5,6,7]
*Main> [(+3)] <*> [2,3,4]
[5,6,7]

甚至

*Main> [(+3),(*10)] <*> [2,3,4]
[5,6,7,20,30,40]
*Main> [(+3),(*10)] >*> [2,3,4]
[5,6,7,20,30,40]

现在变量 fa 的存在以及对 >*> 进行定义的最后一行是 Monad 和 Applicative 的 关键区别。在 Applicative 中,你只能在结尾处 return 一些东西,而在 Monad 中,你可以随意使用 fa

相似之处

在 Applicative 中,你可以这样做:

getNonEmptyStringA :: IO String
getNonEmptyStringA = (:) <$> getChar <*> getLine

我们可以将其翻译为Monad函数:

getNonEmptyStringM' = (:) `fmap` getChar >*> getLine

或者更典型地,

getNonEmptyStringM :: IO String
getNonEmptyStringM = do
    c <- getChar
    xs <- getLine
    return (c:xs)

区别

在Monad中,你可以这样做

checkFirst :: IO (Maybe String)
checkFirst = do
    c <- getChar
    if c == 'n' then return Nothing
                else fmap Just getLine

例如,
Main> checkFirst >>= print
qwerty
Just "werty"

Main> checkFirst >>= print
nNothing

请注意,checkFirst 在我输入 n 后改变了发生的事情 - 它立即返回 Nothing 而不给我机会输入 getLine 或按回车键,而如果我以 q 开头,它将继续运行 getLine。根据 改变 操作 的能力是 Monad 和 Applicative 之间的关键区别,但是您可以看到使用 >*> 运算符,Monad 执行 Applicative 执行的所有操作。(它们都有 return,Applicative 称为 pure,它们都有 (<$>)fmap,因为它们都是 Functors。)
在 Applicative 中编写类似于 checkFirst 的最接近方法是:
don'tCheckFirst :: IO (Maybe String)
don'tCheckFirst = check <$> getChar <*> getLine  where
   check c xs = if c == 'n' then Nothing
                else Just (c:xs)

这是如何工作的:

Main> don'tCheckFirst >>= print
nI can keep typing because it has to do the getLine anyway
Nothing

Main> don'tCheckFirst >>= print
qwerty
Just "qwerty"

(注意:在Windows ghci中,由于 getChar的Ghc bug,无法区分checkFirstdon'tCheckFirst。)

概述

Monad类似于Applicative,但具有基于值的完全更改所做事情的能力。


6
在Haskell中,一个Monad是一个Applicative加上一个"flatten"该Monad的函数join :: m (m a) -> m a。
"Applicative应用"<*> 的类型为f(a->b)->f a->f b; 如果你选择类型b在相同的Functor内,即b::f c,则类型签名特化为<*> :: f(a->f c)->f a->f(f c)。当你没有一个monad结构时,程序到此为止;然而,使用monadic join函数,你可以将结果压缩,得到与以前相同的monad(而不是两个stacked monads)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接