11

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

因此,给定一个类型M[A]map它接受一个函数A=>B并返回一个M[B]. 但是我们也有flatMapwhich 接受一个函数A=>M[B]并返回一个M[B]. 我们也有flattenwhich 接受 aM[M[A]]并返回 a M[A]

此外,我读过的许多资料都描述flatMap为.mapflatten

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

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

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

4

5 回答 5

31

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)), not Some(7),如果你现在想在最后链接另一个地图,你不能因为fooandbar取一个Int,而不是一个Option[Int]

进入flatMap

foo(3).flatMap(foo)

将返回Some(7),并且

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

退货Some(15)

这很棒!使用flatMap可以让你将形状的函数链接A => M[B]到遗忘(在前面的例子中ABInt,和MOption)。

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

我已经看到用于此的盒子类比,因此请注意我专业绘制的 MSPaint 插图: 在此处输入图像描述

这种展开和重新包装行为意味着,如果我要引入第三个不返回 an 的函数Option[Int]并尝试将flatMap 添加到序列中,它将无法工作,因为flatMap期望您返回一个 monad(在本例中为 an 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")

于 2015-12-12T10:05:32.510 回答
2

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

您可能会问相反的问题:为什么拥有map以及flatten何时拥有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) )

那么为什么要打扰mapandfilter呢?

事实上,您需要使用各种最小操作集来重构许多其他操作(flatMap由于其灵活性,这是一个不错的选择)。

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

于 2015-12-12T01:26:24.217 回答
1

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

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

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

于 2015-12-12T01:20:29.413 回答
1

“flatMap”或“bind”方法提供了一种将方法链接在一起的宝贵方法,这些方法提供包装在 Monadic 构造(如ListOptionFuture)中的输出。例如,假设您有两个方法产生 a Futureof 结果(例如,它们对数据库或 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))

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

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

这是一种非常有用的工作方式,事实上,Scala 提供了理解,本质上是一个捷径(Haskell 也提供了一种编写绑定操作链的捷径 - 我不是一位 Haskell 专家,但不记得细节) - 因此您会遇到关于理解被“去糖化”成一系列flatMap调用的谈话(以及可能的filter调用和最终map调用yield) .

于 2015-12-12T07:31:10.713 回答
0

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

ETC ...

于 2015-12-12T02:20:17.587 回答