为什么我们需要 flatMap(一般来说)?

Why do we need flatMap (in general)?

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

因此,给定一个类型 M[A],我们有 map 接受一个函数 A=>B 和 returns 一个 M[B]。但是我们也有 flatMap 接受一个函数 A=>M[B] 和 returns 一个 M[B]。我们还有 flatten ,它需要一个 M[M[A]] 和 returns 一个 M[A].

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

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

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

顺便说一句,我在这个问题上写了很长的一篇文章,其中有很多列表显示我 运行 试图深入了解 flatMap 的含义然后 运行 进入 回答了我的一些问题。有时我讨厌 Scala 的隐式。他们真的可以搅浑水。 :)

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

例如,考虑一个为人们生成邮件程序输出地址的程序。大多数人只有一个地址。有些有两个或更多。不幸的是,有些人 none。 Flatmap 是一种通用算法,用于获取这些人的列表和 return 所有地址,而不管每个人有多少。

零输出情况对于 monad 特别有用,monad 通常(总是?)return 恰好为零或一个结果(想想如果计算失败可能 - return 为零结果,或一个如果成功)。在那种情况下,你想对 "all of the results" 执行操作,它恰好可能是一个或多个。

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

您可能会问相反的问题:为什么要有 mapflatten,而您已经有 flatMap 以及一种在集合中存储单个元素的方法?也就是说,

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 是一个不错的选择,因为它的灵活性)。

从实用的角度来说,最好有你需要的工具。同理为什么会有不可调扳手。

好吧,有人可能会争辩说,您也不需要 .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)
}

等等...

"flatMap" 或 "bind" 方法提供了一种非常宝贵的方法来将方法链接在一起,这些方法提供包装在 Monadic 构造中的输出(如 ListOption,或 Future)。例如,假设您有两个方法产生 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))

从这个意义上说,我们有 "composed" 函数 fn3 来自 fn1fn2 使用 flatMap,它具有相同的一般结构(因此可以依次组合更多类似的功能)。

map 方法会给我们带来不那么方便的 - 并且不容易链接 - Future[Future[C]]。当然我们可以使用 flatten 来减少它,但是 flatMap 方法一次调用就完成了,并且可以按照我们的意愿进行链接。

这是一种非常有用的工作方式,事实上,Scala 提供了 for-comprehension 作为本质上的捷径(Haskell,也提供了一种速记方式来编写绑定操作链 - 不过我不是 Haskell 专家,也不记得细节) - 因此你会遇到关于 for-comprehensions 被 "de-sugared" 变成链的谈话flatMap 次调用(以及可能的 filter 次调用和最终 map 次调用 yield)。

FlatMap,在其他一些语言中被称为"bind",正如你自己所说的函数组合。

想象一下,您有一些像这样的函数:

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

函数运行良好,调用 foo(3) returns Some(5),调用 bar(3) returns Some(9),我们'都很开心。

但是现在您运行陷入了需要您多次执行该操作的情况。

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

完成任务,对吧?

除了不是真的。上面表达式的输出是 Some(Some(7)),而不是 Some(7),如果你现在想在末尾链接另一个映射,你不能,因为 foobar 需要一个Int,而不是 Option[Int]

输入flatMap

foo(3).flatMap(foo)

将 return Some(7)

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

ReturnsSome(15)

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

从技术上讲更准确; flatMapbind 具有签名 M[A] => (A => M[B]) => M[B],这意味着它们采用 "wrapped" 值,例如 Some(3)Right('foo)List(1,2,3) 并通过通常采用未包装值的函数推送它,例如前面提到的 foobar。它首先 "unwrapping" 值,然后通过函数传递它。

我已经看到用于此的盒子类比,所以请观察我熟练绘制的 MSPaint 插图:

这种展开和重新包装行为意味着如果我要引入第三个函数,它不是 return Option[Int] 并尝试 flatMap 到序列,它不会工作,因为flatMap期望你return一个monad(在这种情况下是Option

def baz(x: Int): String = x + " is a number"

foo(3).flatMap(foo).flatMap(bar).flatMap(baz) // <<< ERROR

为了解决这个问题,如果你的函数不是 return monad,你只需要使用常规的 map 函数

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

然后 return Some("15 is a number")