为什么我们通常需要flatMap?

问题描述 投票:10回答:5

我一直在研究FP语言(打开和关闭),并曾与Scala,Haskell,F#和其他一些语言一起玩。我喜欢我所看到和理解的FP的一些基本概念(具有类别理论中的绝对no背景-请不要谈论数学)。

因此,给定类型为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隐式。他们真的可以使水浑浊。 :)

scala functional-programming flatmap
5个回答
26
投票

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,而不是[ C0]。

输入Option[Int]

flatMap

将返回foo(3).flatMap(foo)

Some(7)

返回foo(3).flatMap(foo).flatMap(bar)

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

从技术上讲; OptionflatMap具有签名bind,表示它们采用“包装”值,例如M[A] => (A => M[B]) => M[B]Some(3)Right('foo),并通过通常采用未包装值的功能将其推入,例如前述的List(1,2,3)foo。它首先通过“解包”值,然后将其传递给函数来完成此操作。

我已经看到了用于此的类比框,因此请观察我熟练绘制的MSPaint插图:bar

这种展开和重新包装的行为意味着,如果我要引入第三个不返回enter image description here并试图将Option[Int] it传递给序列的函数,则它将不起作用,因为[ C0]希望您返回一个Monad(在这种情况下为flatMap

flatMap

要解决这个问题,如果您的函数没有返回单子,您只需使用常规的Option函数

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

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

然后返回map


2
投票

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

[您可能会问相反的问题:当您已有foo(3).flatMap(foo).flatMap(bar).map(baz) 并在集合中存储单个元素的方式时,为什么会有Some("15 is a number")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) )

实际上,您需要重构各种其他最小的操作集(map由于其灵活性而是一个不错的选择)。

实用上,最好拥有所需的工具。有不可调节扳手的相同原因。


1
投票

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

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

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


1
投票

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

Future

如何结合这些?使用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)

从这个意义上说,我们使用flatMapdef fn3(input3: A): Future[C] = fn1(a).flatMap(b => fn2(b)) fn3中“组合”了函数fn1,该函数具有相同的一般结构(因此可以依次使用其他类似的函数来构成)。

fn2方法会给我们带来不便-并且不容易链接-flatMap。当然,我们可以使用map来减少这种情况,但是Future[Future[C]]方法可以在一个调用中做到这一点,并且可以根据需要链接起来。

实际上,这是一种非常有用的工作方式,Scala为此提供了理解,实质上是它的捷径(Haskell也提供了编写绑定操作链的捷径-我是我不是Haskell专家,并且不记得详细信息)-因此,您将遇到关于将理解“消减”成flatten个电话链(以及可能的flatMap)的话题呼叫和flatMap的最终filter呼叫)。


0
投票

嗯,有人可能会争论,您也不需要map。为什么不做类似

的事情
yield

关于地图也可以这样说:

.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)

等等...

© www.soinside.com 2019 - 2024. All rights reserved.