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

时间:2015-12-12 00:19:09

标签: scala functional-programming flatmap

我一直在研究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是monadic组合运算符,但我从未真正看到它使用过他所描述的方式这个运营商。它执行此功能吗?如果是这样,我该如何将该概念映射到flatMap

顺便说一句,我在这个问题上写了很长的文章,上面有很多列表显示我试图找到flatMap意义底部的实验,然后遇到了this question,我回答了一些问题。的问题。有时我讨厌Scala暗示。他们真的可以在水中浑浊。 :)

5 个答案:

答案 0 :(得分:23)

FlatMap,在其他一些语言中被称为“bind”,就像你自己为功能组合所说的那样。

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

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

完成工作,对吧?

除非不是。上面的xpression的输出是Some(Some(7)),而不是Some(7),如果您现在想要在最后链接另一个地图,则不能,因为foobar采取Int,而不是Option[Int]

输入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插图: enter image description here

这种展开和重新包装行为意味着如果我要引入第三个不返回Option[Int]并且尝试flatMap 到序列的函数,它不起作用,因为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")

答案 1 :(得分:2)

这与您提供多种方式做任何事情的原因相同:它是一种常见的操作,您可能想要将其包装起来。

您可以提出相反的问题:为什么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因其灵活性而成为一个不错的选择。)

务实地说,拥有您需要的工具会更好。同样有不可调扳手的原因。

答案 2 :(得分:1)

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

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

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

答案 3 :(得分:1)

“flatMap”或“bind”方法提供了一种非常有效的方法,可以将提供包含在Monadic构造中的输出的方法链接在一起(如ListOption或{{1 }})。例如,假设您有两个方法产生Future结果(例如,它们对数据库或Web服务调用等进行长时间调用,并且应该异步使用):

Future

如何结合这些?使用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)) fn3fn1“组成”了一个函数fn2,它具有相同的一般结构(因此可以是反过来又有类似的功能)。

flatMap方法会给我们一个不那么方便 - 而且不容易链接 - map。当然,我们可以使用Future[Future[C]]来减少这种情况,但flatten方法会在一次调用中执行此操作,并且可以根据需要进行链接。

事实上,这是一种非常有用的工作方式,Scala提供的理解本质上是一个快捷方式(Haskell也提供了编写绑定操作链的简便方法 - 我但是,我不是Haskell的专家,也不记得细节了 - 因此,您将会发现有关for-comprehension被“脱糖”成flatMap次调用的话题(以及可能的话) flatMap来电filter来电map {/ 1}}。

答案 4 :(得分: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)
}

等...