【问题标题】:Why do we need flatMap (in general)?为什么我们需要 flatMap(通常)?
【发布时间】:2016-03-18 00:35:12
【问题描述】:

我一直在研究 FP 语言(断断续续)一段时间,并使用过 Scala、Haskell、F# 和其他一些语言。我喜欢我所看到的并理解 FP 的一些基本概念(绝对没有类别理论背景 - 所以请不要谈论数学)。

所以,给定一个类型M[A],我们有map,它接受一个函数A=>B并返回一个M[B]。但是我们也有flatMap,它接受一个函数A=>M[B]并返回一个M[B]。我们还有flatten,它接受M[M[A]]并返回M[A]

此外,我读过的许多资料都将flatMap 描述为map,后跟flatten

那么,鉴于flatMap 似乎等同于flatten compose map,它的目的是什么?请不要说它是为了支持“理解”,因为这个问题确实不是 Scala 特有的。我不太关心语法糖,而是关心它背后的概念。 Haskell 的绑定运算符 (>>=) 也会出现同样的问题。我相信它们都与某些类别理论概念有关,但我不会说那种语言。

我不止一次观看了 Brian Beckman 的精彩视频 Don't Fear the Monad,我想我看到 flatMap 是一元组合运算符,但我从未真正看到它使用过他描述此运算符的方式。它执行此功能吗?如果是这样,我如何将该概念映射到flatMap

顺便说一句,我对这个问题写了很长的文章,其中有很多清单显示了我试图弄清flatMap 含义的底部的实验,然后遇到了this question,它回答了我的一些问题。有时我讨厌 Scala 的隐式。他们真的可以搅浑水。 :)

【问题讨论】:

  • 那么xss.flatten 不只是xss.flatMap(identity) 的简写,所以flatten 派生自flatMap,而不是反过来?虽然为了解释事情,我想flatten 更容易理解。
  • @LuigiPlinge,是的,我也看到了这一点。我知道flatMap 提供的许多操作以及如何使用它。我只是不认为我真的了解它。似乎有一个我在某处无法完全理解的概念。

标签: scala functional-programming flatmap


【解决方案1】:

最简单的原因是组成一个输出集,其中输入集中的每个条目都可能产生多个(或零个!)输出。

例如,考虑一个输出地址供人们生成邮件的程序。大多数人只有一个地址。有些有两个或更多。不幸的是,有些人没有。 Flatmap 是一种通用算法,用于获取这些人的列表并返回所有地址,无论每个人有多少人。

输出为零的情况对于单子特别有用,它通常(总是?)返回零或一个结果(想想 Maybe- 如果计算失败则返回零个结果,或者如果计算成功则返回一个)。在这种情况下,您想对“所有结果”执行操作,而这恰好可能是一个或多个。

【讨论】:

  • 我认为这更接近我所寻找的。我了解flatMap 似乎很好地涵盖了 0/1 元素案例。不过,在查看我在网上找到的一些材料时,似乎表明它应该起到与compose 相同的作用。但这只是我试图确认或消除的印象。
  • Compose 完全不同。 Compose 执行第一个(函数)参数并将结果传递给第二个函数。您可能会感到困惑的是 flatMap 是组合 map 和 flatten 函数的结果 - 首先映射输入的结果,然后将这些结果展平为单个序列。
  • 然而,Brian Beckman 将 Haskell 的绑定运算符 >>= 解释为一元组合运算符。而且,否则它似乎与flatMap 相同。他错了吗?
  • 不,但在这种情况下,flatMap 是功能的超集。 Monadic composition 仅适用于 0 或 1 个结果,而 flatMap 适用于 0 或更多。
【解决方案2】:

这与您提供不止一种方法来做任何事情的原因相同:这是一个足够常见的操作,您可能想要包装它。

您可能会问相反的问题:当您已经拥有 flatMap 和在集合中存储单个元素的方法时,为什么还要拥有 mapflatten?也就是说,

x map f
x filter p

可以替换为

x flatMap ( xi => x.take(0) :+ f(xi) )
x flatMap ( xi => if (p(xi)) x.take(0) :+ xi else x.take(0) )

那么为什么要打扰mapfilter

事实上,您需要各种最小操作集来重构许多其他操作(flatMap 是一个不错的选择,因为它具有灵活性)。

务实地,最好有你需要的工具。与非可调扳手相同的原因。

【讨论】:

    【解决方案3】:

    好吧,有人可能会说,你也不需要.flatten。为什么不做类似的事情

    @tailrec
    def flatten[T](in: Seq[Seq[T], out: Seq[T] = Nil): Seq[T] = in match {
       case Nil => out
       case head ::tail => flatten(tail, out ++ head)
    }
    

    地图也是如此:

    @tailrec
    def map[A,B](in: Seq[A], out: Seq[B] = Nil)(f: A => B): Seq[B] = in match {
       case Nil => out
       case head :: tail => map(tail, out :+ f(head))(f)
      } 
    

    那么,为什么库提供.flatten.map?同样的原因.flatMap 是:方便。

    还有.collect,真的只是

    list.filter(f.isDefinedAt _).map(f)
    

    .reduce 实际上只不过是list.foldLeft(list.head)(f).headOption

    list match {
        case Nil => None
        case head :: _ => Some(head)
    }
    

    等等……

    【讨论】:

    • 我希望得到的不仅仅是 TMTOWTDI :) 我已经看到了根据 flatMap 定义的地图,反之亦然(对于 Lists),所以我有点明白这一点。我一直在寻找一些概念方向的更多推动力。虽然我通常会忘记collect。谢谢提醒。
    【解决方案4】:

    “flatMap”或“bind”方法提供了一种将方法链接在一起的宝贵方法,这些方法提供包装在 Monadic 结构中的输出(如 ListOptionFuture)。例如,假设您有两个方法产生Future 的结果(例如,它们对数据库或 Web 服务调用等进行长时间运行的调用,并且应该异步使用):

    def fn1(input1: A): Future[B]  // (for some types A and B)
    
    def fn2(input2: B): Future[C]  // (for some types B and C)
    

    如何结合这些?使用flatMap,我们可以这样做:

    def fn3(input3: A): Future[C] = fn1(a).flatMap(b => fn2(b))
    

    从这个意义上说,我们使用flatMapfn1fn2中“组合”了一个函数fn3,它具有相同的一般结构(因此可以依次组合更多类似的函数) .

    map 方法会给我们一个不太方便的 - 并且不容易链接 - Future[Future[C]]。当然,我们可以使用flatten 来减少这种情况,但是flatMap 方法可以在一次调用中完成,并且可以尽可能多地链接。

    这是一种非常有用的工作方式,事实上,Scala 提供了理解,本质上是一种快捷方式(Haskell 也提供了一种编写绑定操作链的快捷方式 - 我不过,我不是 Haskell 专家,也不记得细节)——因此你会遇到关于理解被“去糖化”成flatMap 调用链的谈话(以及可能的@987654337 @ 调用和最后的map 调用yield)。

    【讨论】:

    • 谢谢。所以flatMap 确实是一种链接单子生成函数的方法,就像map 可以用来链接“正常”函数一样。也就是说,定义为A -> B的函数。
    • @melston 是的,这是一种合理的思考方式。
    【解决方案5】:

    FlatMap,在某些其他语言中称为“绑定”,正如您自己所说的函数组合。

    想象一下你有一些这样的功能:

    def foo(x: Int): Option[Int] = Some(x + 2)
    def bar(x: Int): Option[Int] = Some(x * 3)
    

    函数运行良好,调用foo(3) 返回Some(5),调用bar(3) 返回Some(9),我们都很高兴。

    但是现在你遇到了需要你进行多次操作的情况。

    foo(3).map(x => foo(x)) // or just foo(3).map(foo) for short
    

    工作完成了,对吧?

    除了不是真的。上面表达式的输出是Some(Some(7)),而不是Some(7),如果你现在想在最后链接另一个地图,你不能因为foobar采用Int,而不是@ 987654334@.

    输入flatMap

    foo(3).flatMap(foo)
    

    将返回Some(7),并且

    foo(3).flatMap(foo).flatMap(bar)
    

    返回Some(15)

    这太棒了!使用flatMap 可以将A => M[B] 形状的函数链接到遗忘(在前面的示例中,ABIntMOption)。

    从技术上讲; flatMapbind 具有签名 M[A] => (A => M[B]) => M[B],这意味着它们采用“包装”值,例如 Some(3)Right('foo)List(1,2,3),然后将其推入通常采用未包装值的函数,比如前面提到的foobar。它首先“解包”该值,然后将其传递给函数。

    我已经看到用于此的盒子类比,因此请注意我专业绘制的 MSPaint 插图:

    这种展开和重新包装行为意味着,如果我要引入第三个函数,它不返回 Option[Int] 并尝试将 flatMap it 加入序列,它不会无法正常工作,因为 flatMap 期望您返回一个 monad(在本例中为 Option

    def baz(x: Int): String = x + " is a number"
    
    foo(3).flatMap(foo).flatMap(bar).flatMap(baz) // <<< ERROR
    

    要解决这个问题,如果您的函数不返回 monad,您只需使用常规的 map 函数

    foo(3).flatMap(foo).flatMap(bar).map(baz)
    

    然后返回Some("15 is a number")

    【讨论】:

    • 这个答案简洁明了,准确地回答了问题的要求,并且有很棒的图形。我希望我能投票两次
    • 这可能是我见过的对 flatMap/bind 背后概念的最好描述。谢谢。
    • 不错的答案,我认为虽然图表的flatMap: 部分应该显示 flatMap 映射然后奉承而不是奉承然后映射。
    猜你喜欢
    • 2016-02-01
    • 1970-01-01
    • 2019-06-09
    • 2020-06-07
    • 2014-06-18
    • 2017-02-26
    • 2011-04-03
    • 2017-07-27
    • 2020-09-21
    相关资源
    最近更新 更多