fmap和bind之间的能力差异?

25

我对函数式编程比较陌生(来自JavaScript),很难区分这两个概念,这也影响了我对于函子和单子的理解。

函子:

class Functor f where
    fmap :: (a -> b) -> f a -> f b

单子(简化版):

class Monad m where
    (>>=) :: m a -> (a -> m b) -> m b
  • fmap接受一个函数和一个functor,并返回一个functor。
  • >>=接受一个函数和一个monad,并返回一个monad。

两者之间的区别在于函数参数:

  • fmap - (a -> b)
  • >>= - (a -> m b)

>>= 接受一个返回monad的函数参数。我知道这很重要,但我很难看到这个微小的差别是如何使monads比functors更强大的。有人能解释一下吗?


4
使用翻转版本的(>>=)(=<<)更容易理解。对于f a -> f b,函数g :: a -> b 不影响f的“包装”--不会改变它。对于m a -> m b,函数k :: a -> m b本身创建了新的m“包装”,因此它可以改变它。 - Will Ness
@WillNess 我可以“理解”这个,但我看不到它。我认为我真正的问题是我看不到 >>= 可以做什么 fmap 不能做的事情。在我的脑海中,它们是等效的,因为我没有看到一个例子,表明 fmap 是不足够的。 - m0meni
4
使用map函数从列表中筛选元素是不可能的,但是使用concatMap函数可以实现。比如对于 [1,2,3] 这个列表,使用 map (\x->x+1) [1,2,3] 只能得到 [2,3,4] ,而使用 concatMap (\x-> [x,x+1|even x]) [1,2,3]) 则可以得到 [1,2,3,3,4] - Will Ness
@WillNess 好的,我明白了!我一直以为 filter 操作只是 map 的一种简化变体,但现在我意识到这根本不可能。 - m0meni
我们可以使用 map (\x -> [x|even x]) [1,2,3] 几乎实现“过滤”操作,但它会产生 [[],[2],[]] 的结果,因此需要通过 concat 进行另一层解释才能真正实现“过滤”。 - Will Ness
3个回答

22

好的,(<$>)fmap的别名,而(=<<)与参数交换后等同于(>>=)

(<$>) :: (x ->   y) -> b x -> b y
(=<<) :: (x -> b y) -> b x -> b y

区别现在已经很清晰:使用bind函数,我们应用的是返回y而不是y的函数。那么这有什么区别呢?
考虑下面的小例子:
foo <$> Just 3

请注意,(<$>)foo应用于3,并将结果放回Just。换句话说,这个计算的结果不能Nothing。相反:
bar =<< Just 3

这个计算可能返回Nothing。例如,bar x = Nothing就会这样做。
我们可以用列表单子做类似的事情:
foo <$> [Red, Yellow, Blue]   -- Result is guaranteed to be a 3-element list.
bar =<< [Red, Yellow, Blue]   -- Result can be ANY size.

简而言之,使用(<$>)(即fmap)时,结果的“结构”始终与输入相同。但是使用(=<<)(即(>>=))时,结果的结构可能会发生变化。这允许条件执行、对输入做出反应以及许多其他事情。

6
完整性考虑,Applicative也可以返回“Nothing”:Nothing <*> Just 3。不同之处在于,在组合计算之前,“管道”(即计算结构)是固定的,而在Monads中,“管道”可以根据运行时生成的值而变化。(在IO的情况下,3 可能作为用户输入接收)。-- 列表示例在这里特别好:(foo <$>)保持结构(列表长度); ([baz, quux] <*>)可预测地更改结构(创建长度为6的列表);但是使用Monad时,所有打赌都是无效的。 - Will Ness

7
简短的回答是,如果您可以以一种有意义的方式将m(m a)转换为m a,那么它就是一个Monad。所有Monad都可以这样做,但Functor不一定能做到。
我认为最令人困惑的是,所有常见的Functor示例(例如ListMaybeIO)也都是Monad。我们需要一个Functor但不是Monad的示例。
我将使用一个来自虚构日历程序的示例。以下代码定义了一个Event Functor,其中存储与事件相关的一些数据和发生时间。
import Data.Time.LocalTime

data Event a = MkEvent LocalTime a

instance Functor Event where
    fmap f (MkEvent time a) = MkEvent time (f a)
< p > 事件 对象存储事件发生的时间以及一些额外的数据,可以使用 fmap 进行更改。现在让我们尝试将其变成一个 Monad:

instance Monad Event where
    (>>=) (MkEvent timeA a) f = let (MkEvent timeB b) = f a in
                                MkEvent <notSureWhatToPutHere> b

我们发现不能这样做,因为你最终会得到两个LocalTime对象。timeA来自给定的Event,而timeB来自fa的结果所给出的Event。我们的Event类型被定义为仅具有一项LocalTime(time),表示它发生的时间,因此,如果不将两个LocalTime合并成一个,就无法将其变成一个Monad。(也许有某些情况下这样做是有意义的,如果你确实想这么做,可以将其转换为一个Monad)。

3
一个经典/常见的函数子例子,它不是单子的是 newtype Const a b = Const a - dfeuer
3
根据单子法则,pure x >>= f 应该等于 f x,但是 pure :: b -> Const a b 没有可能使用它的参数。 - dfeuer
1
@dfeuer,这似乎“过于简单而不简单”(https://ncatlab.org/nlab/show/too+simple+to+be+simple)。另外,我找不到其他编写Functor实例的方法,除了`fmap f (Const x) = Const x`。 - HEGX64
3
@HEGX64说的没错。但对于Functor来说,这并不是问题 - 实际上,它立即保证了函子法则fmap id ≡ id - leftaroundabout
1
一个不那么简单的例子:在FRP框架中,事件和信号往往是函子,但不是单子。这对于高效实现非常重要。 - dfeuer

3
假设现在的 IO 只是一个 Functor,而不是 Monad。我们该如何按顺序执行两个操作呢?比如,像 getChar :: IO CharputChar :: Char -> IO ()
我们可以尝试对 getChar 进行映射(当其被执行时,从标准输入中读取一个 Char),使用 putChar
fmap putChar getChar :: IO (IO ())

现在我们有一个程序,当执行时,从stdin读取一个Char并生成一个程序,当执行时,将Char写入stdout。但实际上我们想要的是一个程序,当执行时,从stdin读取一个Char并将其写入stdout。因此,我们需要一个具有下列类型的“平铺”(在IO情况下为“顺序化”)函数:
join :: IO (IO ()) -> IO ()
Functor本身并不提供该功能。但它是Monad的一个函数,其中它具有更一般的类型:
join :: Monad m => m (m a) -> m a

所有这些与>>=有什么关系呢?恰巧,单子绑定只是fmapjoin的组合:

:t \m f -> join (fmap f m)
(Monad m) => m a1 -> (a1 -> m a) -> m a

另一种理解这种差异的方式是,fmap 永远不会改变映射值的整体结构,但 join(因此也包括 >>=)可以做到。

IO 操作而言,fmap 永远不会导致额外的读取/写入或其他效果。但是,join 在外部操作之后顺序执行内部操作的读取/写入。


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