我一直在研究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暗示。他们真的可以在水中浑浊。 :)
答案 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)
,如果您现在想要在最后链接另一个地图,则不能,因为foo
和bar
采取Int
,而不是Option[Int]
。
输入flatMap
foo(3).flatMap(foo)
将返回Some(7)
和
foo(3).flatMap(foo).flatMap(bar)
返回Some(15)
。
太好了!使用flatMap
可以将形状A => M[B]
的函数链接到遗忘(在上一个示例中A
和B
是Int
,M
是Option
)。
从技术角度讲; flatMap
和bind
具有签名M[A] => (A => M[B]) => M[B]
,这意味着它们采用“包裹”值,例如Some(3)
,Right('foo)
或List(1,2,3)
和通过一个通常采用未打包值的函数推送它,例如前面提到的foo
和bar
。它通过首先“展开”值,然后将其传递给函数来完成此任务。
我已经看到了用于此的盒子类比,所以请观察我熟练绘制的MSPaint插图:
这种展开和重新包装行为意味着如果我要引入第三个不返回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)
这与您提供多种方式做任何事情的原因相同:它是一种常见的操作,您可能想要将其包装起来。
您可以提出相反的问题:为什么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
和filter
?
事实上,您需要重建其他许多操作的各种最小操作集(flatMap
因其灵活性而成为一个不错的选择。)
务实地说,拥有您需要的工具会更好。同样有不可调扳手的原因。
答案 2 :(得分:1)
最简单的原因是组成一个输出集,其中输入集中的每个条目可能产生多个(或零!)输出。
例如,考虑一个为人们输出地址以生成邮件程序的程序。大多数人都有一个地址。有些人有两个或更多。不幸的是,有些人没有。 Flatmap是一种通用算法,可以获取这些人的列表并返回所有地址,无论每个人有多少地址。
零输出情况对monad特别有用,monad通常(总是?)返回正好零或一个结果(想想如果计算失败则返回零结果,如果成功则返回一个)。在这种情况下,您希望对“所有结果”执行操作,它恰好可能是一个或多个。
答案 3 :(得分:1)
“flatMap”或“bind”方法提供了一种非常有效的方法,可以将提供包含在Monadic构造中的输出的方法链接在一起(如List
,Option
或{{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))
从fn3
和fn1
“组成”了一个函数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)
}
等...