为什么我们需要 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" 执行操作,它恰好可能是一个或多个。
这与您提供不止一种方法来做任何事情的原因相同:这是一个足够常见的操作,您可能想要包装它。
您可能会问相反的问题:为什么要有 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
是一个不错的选择,因为它的灵活性)。
从实用的角度来说,最好有你需要的工具。同理为什么会有不可调扳手。
好吧,有人可能会争辩说,您也不需要 .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 构造中的输出(如 List
、Option
,或 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
来自 fn1
和 fn2
使用 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)
,如果你现在想在末尾链接另一个映射,你不能,因为 foo
和 bar
需要一个Int
,而不是 Option[Int]
。
输入flatMap
foo(3).flatMap(foo)
将 return Some(7)
和
foo(3).flatMap(foo).flatMap(bar)
ReturnsSome(15)
。
太棒了!使用 flatMap
可以将形状为 A => M[B]
的函数链接到遗忘(在前面的示例中 A
和 B
是 Int
,而 M
是Option
).
从技术上讲更准确; flatMap
和 bind
具有签名 M[A] => (A => M[B]) => M[B]
,这意味着它们采用 "wrapped" 值,例如 Some(3)
、Right('foo)
或 List(1,2,3)
并通过通常采用未包装值的函数推送它,例如前面提到的 foo
和 bar
。它首先 "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")
我研究 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
的含义然后 运行 进入
最简单的原因是组成一个输出集,其中输入集中的每个条目可能产生多个(或零个!)输出。
例如,考虑一个为人们生成邮件程序输出地址的程序。大多数人只有一个地址。有些有两个或更多。不幸的是,有些人 none。 Flatmap 是一种通用算法,用于获取这些人的列表和 return 所有地址,而不管每个人有多少。
零输出情况对于 monad 特别有用,monad 通常(总是?)return 恰好为零或一个结果(想想如果计算失败可能 - return 为零结果,或一个如果成功)。在那种情况下,你想对 "all of the results" 执行操作,它恰好可能是一个或多个。
这与您提供不止一种方法来做任何事情的原因相同:这是一个足够常见的操作,您可能想要包装它。
您可能会问相反的问题:为什么要有 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
是一个不错的选择,因为它的灵活性)。
从实用的角度来说,最好有你需要的工具。同理为什么会有不可调扳手。
好吧,有人可能会争辩说,您也不需要 .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 构造中的输出(如 List
、Option
,或 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
来自 fn1
和 fn2
使用 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)
,如果你现在想在末尾链接另一个映射,你不能,因为 foo
和 bar
需要一个Int
,而不是 Option[Int]
。
输入flatMap
foo(3).flatMap(foo)
将 return Some(7)
和
foo(3).flatMap(foo).flatMap(bar)
ReturnsSome(15)
。
太棒了!使用 flatMap
可以将形状为 A => M[B]
的函数链接到遗忘(在前面的示例中 A
和 B
是 Int
,而 M
是Option
).
从技术上讲更准确; flatMap
和 bind
具有签名 M[A] => (A => M[B]) => M[B]
,这意味着它们采用 "wrapped" 值,例如 Some(3)
、Right('foo)
或 List(1,2,3)
并通过通常采用未包装值的函数推送它,例如前面提到的 foo
和 bar
。它首先 "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")