【问题标题】:Difference in capability between fmap and bind?fmap和bind之间的能力差异?
【发布时间】:2016-05-25 01:21:57
【问题描述】:

我是函数式编程的新手(来自 javascript),我很难区分两者之间的区别,这也混淆了我对函子与单子的理解。

函子:

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

Monad(简体):

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

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

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

>>= 接受一个返回 monad 的函数参数。我知道这很重要,但我很难看出这一点小事如何使单子比函子更强大。谁能解释一下?

【问题讨论】:

  • 这在(>>=)(=<<) 的翻转版本中更容易看出。对于(g <$>) :: f a -> f b,函数g :: a -> bf“包装”没有影响——不会改变它。使用(k =<<) :: m a -> m b,函数k :: a -> m b 本身创建新的m“包装”,所以它可以改变。
  • @WillNess 我可以“理解”这一点,但我看不到。我认为我遇到的真正问题是我看不到>>= 可以做到fmap 无法做到的事情。在我的脑海中它们是等价的,因为我没有看到一个例子,这表明 fmap 是不够的。
  • 使用列表,尝试从列表中过滤掉一些元素,使用map。你不能。但是使用concatMap,您可以:map (\x->x+1) [1,2,3] vs concatMap (\x-> [x,x+1|even x]) [1,2,3])
  • @WillNess 好吧,我明白了!我一直认为filter 操作只是map 的糖变体,但现在我意识到这根本不可能。
  • 我们可以使用map (\x -> [x|even x]) [1,2,3] 编写几乎-filter,但它会产生[[],[2],[]],然后需要concat 完成另一个级别的解释才能使其真正成为@ 987654350@.

标签: haskell functional-programming monads functor


【解决方案1】:

嗯,(<$>)fmap 的别名,(=<<)(>>=) 相同,只是交换了参数:

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

现在区别很明显了:使用 bind 函数,我们应用一个返回 b y 而不是 y 的函数。那么这有什么区别呢?

考虑这个小例子:

foo <$> Just 3

注意(&lt;$&gt;) 会将foo 应用到3,并将结果放回Just。换句话说,这个计算的结果不能Nothing。相反:

bar =<< Just 3

这个计算可以返回Nothing。 (例如,bar x = Nothing 会这样做。)

我们可以用 list monad 做类似的事情:

foo <$> [Red, Yellow, Blue]   -- Result is guaranteed to be a 3-element list.
bar =<< [Red, Yellow, Blue]   -- Result can be ANY size.

简而言之,使用(&lt;$&gt;)(即fmap),结果的“结构”始终与输入相同。但是使用(=&lt;&lt;)(即(&gt;&gt;=)),结果的结构可以改变。这允许条件执行、对输入做出反应以及一大堆其他事情。

【讨论】:

  • 为了完整性,Applicative 也可以返回NothingNothing &lt;*&gt; Just 3。不同之处在于,“管道”(即计算结构)在计算组合时固定之前“运行”。但是对于 Monads,管道可以根据产生的值而改变它“运行”。 (在 IO 的情况下,3 可能被接收,例如作为用户的输入)。 -- list 例子是 esp。很好:(foo &lt;$&gt;) 保持结构(列表的长度); ([baz, quux] &lt;*&gt;) 将改变结构predictably(创建长度为 6 的列表);与 Monad 一起,一切都结束了。
【解决方案2】:

简短的回答是,如果你能以一种有意义的方式将 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)

Event 对象存储事件发生的时间和一些可以使用fmap 更改的额外数据。现在让我们试着把它变成一个 Monad:

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

我们发现我们不能,因为您最终会得到两个 LocalTime 对象。 timeA 来自给定的EventtimeB 来自Event,由f a 的结果给出。我们的Event 类型被定义为只有一个LocalTime (time) 出现,因此如果不将两个LocalTimes 合二为一,就不可能使其成为Monad。 (在某些情况下这样做可能有意义,所以如果你真的想的话,你可以把它变成一个 monad)。

【讨论】:

  • 不是单子的经典/通用函子的一个例子是newtype Const a b = Const a
  • pure x &gt;&gt;= f 被单子定律要求为f x,但pure :: b -&gt; Const a b 不可能使用它的参数。
  • @dfeuer 这接缝too simple to be simple。除了fmap f (Const x) = Const x,我也找不到编写 Functor 实例的方法
  • @HEGX64:对。但是对于Functor,这没问题——事实上,它立即保证了函子定律fmap id ≡ id
  • 一个不太简单的例子:在 FRP 框架中,事件和信号往往是函子,而不是单子。事实证明,这对于有效实施很重要。
【解决方案3】:

暂时假设IO 只是Functor,而不是Monad。我们如何对两个动作进行排序?比如getChar :: IO CharputChar :: Char -&gt; IO ()

我们可以尝试使用putChar 映射getChar(执行时从标准输入读取Char 的操作)。

fmap putChar getChar :: IO (IO ())

现在我们有一个程序,它在执行时从标准输入读取Char,并生成一个程序,在执行时将Char 写入标准输出。但我们真正想要的是一个程序,它在执行时从标准输入读取Char,并将Char 写入标准输出。所以我们需要一个“扁平化”(在IO 的情况下为“排序”)函数,其类型为:

join :: IO (IO ()) -> IO ()

Functor 本身不提供此功能。但它是Monad的一个函数,它有更通用的类型:

join :: Monad m => m (m a) -> m a

这一切与&gt;&gt;= 有什么关系?碰巧,一元绑定只是fmapjoin 的组合:

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

另一个区别是fmap 永远不会改变映射值的整体结构,但join(因此&gt;&gt;= 也可以)可以做到这一点。

IO 操作而言,fmap永远不会导致额外的读/写或其他影响。但是join 将内部操作的读/写顺序排在外部操作之后。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-06-29
    • 1970-01-01
    • 2010-11-21
    • 2012-05-26
    • 2013-09-02
    • 2014-03-15
    • 2010-12-07
    • 2017-06-05
    相关资源
    最近更新 更多