为啥我们需要使用 flatMap?

Posted

技术标签:

【中文标题】为啥我们需要使用 flatMap?【英文标题】:Why do we need to use flatMap?为什么我们需要使用 flatMap? 【发布时间】:2016-02-01 23:26:30 【问题描述】:

我开始使用 RxJS,但我不明白为什么在这个例子中我们需要使用像 flatMapconcatAll 这样的函数;这里的数组数组在哪里?

var requestStream = Rx.Observable.just('https://api.github.com/users');

var responseMetastream = requestStream
  .flatMap(function(requestUrl) 
    return Rx.Observable.fromPromise(jQuery.getJSON(requestUrl));
  );

responseMetastream.subscribe(url => console.log(url))

如果有人可以直观地解释正在发生的事情,那将非常有帮助。

【问题讨论】:

这个答案很棒,因为它提供了有价值的参考资料,但是 rxjs 术语不能很好地翻译成英文。 (图片更好)。这就是为什么我建议改为运行像这样的简单示例,或者在 rxjs 存储库中运行更复杂的示例,并在 flatmap 和 map 运算符之前和之后添加“.do”运算符,然后只需使用 Chrome 调试器设置断点。你会立即看到每个产生不同的输出 我认为如果flatMap 被命名为mapThenFlatten,那么它就不会那么混乱了。 我个人不喜欢这个例子。你为什么要订阅一个可观察的 url 字符串。当然,它会让你想到“rx”的方式,但就代码而言,我觉得不直观,除非你使用它的时间足够长,这样你就不会再质疑它了。但对我来说看起来有点矫枉过正。难怪人们难以理解。 【参考方案1】:

人们倾向于通过给出以下定义来使事情​​过于复杂

flatMap 将 Observable 发出的项目转换为 Observables,然后将这些排放量扁平化为一个单一的 可观察的

我发誓这个定义仍然让我感到困惑,但我将以最简单的方式解释它,即使用示例

我们的简单示例

1- 我们有一个 observable,它返回一个简单的 URL 字符串。

2- 我们必须使用该 URL 进行第二次 HTTP 调用。

3- 第二个 HTTP 调用将返回一个包含我们需要的数据的 observable。

所以我们可以像这样想象这种情况:

Observable 1
    |_
       Make Http Call Using Observable 1 Data (returns Observable_2)
            |_
               The Data We Need

如您所见,我们无法直接获得所需的数据?

所以要检索数据,我们可以像这样使用普通订阅:

Observable_1.subscribe((URL) => 
         Http.get(URL).subscribe((Data_We_Need) => 
                  console.log(Data_We_Need);
          );
);

这可行,但正如您所见,我们必须嵌套订阅才能获取我们的数据,这目前看起来还不错,但想象一下我们有 10 个嵌套订阅将变得无法维护!

所以更好的处理方法是使用运算符flatMap,它会做同样的事情,但让我们避免嵌套订阅:

Observable_1
    .flatMap(URL => Http.get(URL))
    .subscribe(Data_We_Need => console.log(Data_We_Need));

【讨论】:

【参考方案2】:

flatMap 用于将数组数组展平为单个数组。

map 只是将一个数组转换为另一个数组。例如,假设您有一个这样的人员对象列表:

const friends = [
    name: 'Dave', kids: ['Max', 'Jack'],
    name: 'Max', kids: ['Sam', 'Alex', 'Megan'],
    name: 'Jordan', kids: ['Mason', 'Cameron', 'Kaylin']
];

但你真正需要的是一个人名数组(即字符串:[“Dave”、“Max”、“Jordan”])。要将此人员对象数组转换为字符串数组,您首先需要像这样定义映射函数:

const mapFunction = p -> p.name;

然后,像这样使用array.map:

const names = friends.map(mapFunction);

返回:

["Dave", "Max", "Jordan"]

flatMap 与 map 类似,因为您将一个数组转换为另一个数组。但是有一些细微的区别: 首先,map一般是一对一的东西。映射函数取一个对象,返回一个对象:

p -> p.name

这意味着 3 个人对象将产生 3 个名称。

另一方面,

flatMap 是一对多的东西。映射函数接受一个对象,但返回一个数组:

p -> p.kids

最终结果:3 个人对象将产生 8 个孩子的名字。因此,这段代码:

const mapFunction = p -> p.kids;
const kidNames = friends.flatMap(mapFunction);

将返回:

["Max", "Jack", "Sam", "Alex", "Megan", "Mason", "Cameron", "Kaylin"]

【讨论】:

【参考方案3】:

flatMap 将 Observable 发出的项目转换为 Observables, 然后将这些排放量扁平化为单个 Observable

我不傻,但读了 10 遍还是没看懂。当我阅读代码 sn-p 时:

[1,2,3].map(x => [x, x * 10])
// [[1, 10], [2, 20], [3, 30]]

[1,2,3].flatMap(x => [x, x * 10])
// [1, 10, 2, 20, 3, 30]

然后我可以理解发生了什么,它做了两件事:

平面地图

    ma​​p:将 *) 发射的项目转换为 Observables。 flat:然后将这些 Observable 合并为一个 Observable。

*) 转换词表示该项目可以转换成其他东西。

然后 merge 运算符变得清晰,它在没有映射的情况下进行展平。为什么不叫它mergeMapflatMap 似乎还有一个别名 mergeMap

【讨论】:

【参考方案4】:

当我开始看Rxjs 时,我也偶然发现了那块石头。对我有帮助的是:

来自 reactivex.io 的文档。例如,对于flatMap:http://reactivex.io/documentation/operators/flatmap.html 来自 rxmarbles 的文档:http://rxmarbles.com/。你不会在那里找到flatMap,你必须查看mergeMap(另一个名字)。 您错过的 Rx 简介:https://gist.github.com/staltz/868e7e9bc2a7b8c1f754。它解决了一个非常相似的例子。特别是它解决了这样一个事实,即 Promise 类似于仅发出一个值的 observable。

终于从 RxJava 中查看了类型信息。没有输入 javascript 在这里没有帮助。基本上,如果Observable<T> 表示一个可观察对象,它推送类型为 T 的值,那么flatMap 将一个类型为T' -> Observable<T> 的函数作为其参数,并返回Observable<T>map 接受 T' -> T 类型的函数并返回 Observable<T>

回到您的示例,您有一个从 url 字符串生成承诺的函数。所以T' : stringT : promise。根据我们之前所说的promise : Observable<T''>,所以T : Observable<T''>,和T'' : html。如果你把这个承诺产生函数放在map 中,当你想要的是Observable<T''> 时,你会得到Observable<Observable<T''>>:你希望observable 发出html 值。 flatMap 之所以这样调用,是因为它将map 的结果展平(删除了可观察层)。根据你的背景,这对你来说可能是中文,但我输入信息和来自这里的绘图对我来说一切都变得一清二楚:http://reactivex.io/documentation/operators/flatmap.html。

【讨论】:

我忘了提到你应该能够将return Rx.Observable.fromPromise(jQuery.getJSON(requestUrl)); 简化为return jQuery.getJSON(requestUrl);,因为flatMap 还接受一个选择器函数,该函数返回一个承诺,即T' -> Promise 类型的函数。 哇,GitHub Gist (gist.github.com/staltz/868e7e9bc2a7b8c1f754) 真是太棒了。我向任何使用任何 ReactiveX 库(如 RxJS)的人推荐它。 @JacobStamm 我同意。只是让事情变得更容易。 这个语法是什么意思:T’ -> T?我将T 理解为泛型,但撇号和非胖箭头是什么? 您可以用 X 或 Y 替换 T' 而不会改变答案中任何地方的含义。箭头是类型签名的 Haskell 符号。所以 T' -> T 是一个函数的签名,它接受一个 T' 类型的元素并返回一个 T 类型的元素【参考方案5】:

这里展示了使用订阅的 flatMap 的等效实现。

没有平面地图:

this.searchField.valueChanges.debounceTime(400)
.subscribe(
  term => this.searchService.search(term)
  .subscribe( results => 
      console.log(results);  
      this.result = results;
    
  );
);

使用平面地图:

this.searchField.valueChanges.debounceTime(400)
    .flatMap(term => this.searchService.search(term))
    .subscribe(results => 
      console.log(results);
      this.result = results;
    );

http://plnkr.co/edit/BHGmEcdS5eQGX703eRRE?p=preview

希望对您有所帮助。

奥利维尔。

【讨论】:

对于像我这样想知道为什么缺少管道的人,管道是从 rxjs 5.5 及更高版本开始使用的,但在此响应中可以看到,运算符在早期版本中与 . 结合使用。【参考方案6】:

它不是数组数组。它是 observable 中的 observable。

以下返回一个可观察的字符串流。

requestStream
  .map(function(requestUrl) 
    return requestUrl;
  );

虽然这会返回一个可观察的 json 流的可观察流

requestStream
  .map(function(requestUrl) 
    return Rx.Observable.fromPromise(jQuery.getJSON(requestUrl));
  );

flatMap 自动为我们展平 observable,以便我们可以直接观察 json 流

【讨论】:

这个概念很难理解,能否请您将 cmets 添加到视觉中,您的意思是“返回可观察的 json 流的可观察流”。谢谢。 @user233232,如 [x,x,x,x] 到 [[xxx],[[xxx],[xxx]]] 理解第一句话的关键是理解flatMap(和map)对于数组来说并不特殊。可以在任何通用容器或包装器上定义这些操作,包括数组、字典、“可选”、反应流、承诺、指针,甚至函数本身。这是称为 monad 的数学结构的一个涌现属性。上面所有的例子都满足成为 monad 的要求,所以它们都可以被定义为 mapflatMap(有一些注意事项)。【参考方案7】:

flatMap 将 Observable 发射的项目转换为新的 Observable,然后将这些项目的发射扁平化为单个 Observable。

查看下面的场景,get("posts") 返回一个被flatMap“展平”的 Observable。

myObservable.map(e => get("posts")).subscribe(o => console.log(o));
// this would log Observable objects to console.  

myObservable.flatMap(e => get("posts")).subscribe(o => console.log(o));
// this would log posts to console.

【讨论】:

不错,简单的答案。我认为这可能是最好的。 "flatMap 将 Observable 发射的项目转换为新的 Observable,然后将这些项目的发射扁平化为单个 Observable。"这是很棒的东西。【参考方案8】:
['a','b','c'].flatMap(function(e) 
    return [e, e+ 'x', e+ 'y',  e+ 'z'  ];
);
//['a', 'ax', 'ay', 'az', 'b', 'bx', 'by', 'bz', 'c', 'cx', 'cy', 'cz']


['a','b','c'].map(function(e) 
    return [e, e+ 'x', e+ 'y',  e+ 'z'  ];
);
//[Array[4], Array[4], Array[4]]

当你有一个结果是更多 Observable 的 Observable 时,你使用 flatMap。

如果你有一个由另一个 observable 生成的 observable,你不能直接过滤、减少或映射它,因为你有一个 Observable 而不是数据。如果你产生一个可观察的选择 flatMap 而不是 map;那你就没事了。

和第二个 sn-p 一样,如果你在做异步操作,你需要使用 flatMap。

var source = Rx.Observable.interval(100).take(10).map(function(num)
    return num+1
);
source.subscribe(function(e)
    console.log(e)
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.4.1/Rx.min.js"></script>

var source = Rx.Observable.interval(100).take(10).flatMap(function(num)
    return Rx.Observable.timer(100).map(() => num)
);
source.subscribe(function(e)
    console.log(e)
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.4.1/Rx.min.js"></script>

【讨论】:

【参考方案9】:

简单:

[1,2,3].map(x => [x, x * 10])
// [[1, 10], [2, 20], [3, 30]]

[1,2,3].flatMap(x => [x, x * 10])
// [1, 10, 2, 20, 3, 30]]

【讨论】:

【参考方案10】:

使用平面地图

var requestStream = Rx.Observable.just('https://api.github.com/users');

var responseMetastream = requestStream
  .flatMap(function(requestUrl) 
    return Rx.Observable.fromPromise(jQuery.getJSON(requestUrl));
  );

responseMetastream.subscribe(json => console.log(json))

没有平面地图

var requestStream = Rx.Observable.just('https://api.github.com/users');

var responseMetastream = requestStream
  .map(function(requestUrl) 
    return Rx.Observable.fromPromise(jQuery.getJSON(requestUrl));
  );

responseMetastream.subscribe(jsonStream => 
  jsonStream.subscribe(json => console.log(json))
)

【讨论】:

【参考方案11】:

Observable 是一个发出事件流的对象:Next、Error 和 Completed。

当你的函数返回一个 Observable 时,它​​不是返回一个流,而是一个 Observable 的实例。 flatMap 运算符只是将该实例映射到流。

这是flatMapmap 相比的行为:执行给定的函数并将生成的对象展平为流。

【讨论】:

以上是关于为啥我们需要使用 flatMap?的主要内容,如果未能解决你的问题,请参考以下文章

2021年大数据常用语言Scala(二十三):函数式编程 扁平化映射 flatMap

为啥 flatMap 不将 Stream<Stream<SomeClass>> 扁平化到 SomeClass 而是扁平化到 Stream<SomeClass>?

谈谈Combine中一个有趣操作符flatMap的使用与陷阱

谈谈Combine中一个有趣操作符flatMap的使用与陷阱

RxSwift:在使用 flatMap 和 reduce 时需要帮助

结合 flatMap/Scan 携带中间结果