JavaScript Developers Fix Nested Streams by Adopting Monad FlatMap

Monad - FunFunFunction #21 — Fun Fun Function

Fun Fun Function

Standard map operations fail when callbacks return data containers, requiring monads to unwrap nested structures through flatMap or promise chaining.

AI-assisted summary of Monad - FunFunFunction #21 — original source. Check the original for full context.

Source published:

Key ideas

  • A monad is fundamentally a more powerful functor that implements flatMap (or equivalent methods like bind, chain, or then) in addition to map. (0:00)
  • Using standard map on functions returning container structures like streams causes nested wrapping (Stream<Stream<Value>>), whereas flatMap flattens and unwraps the inner value. (5:07)
  • JavaScript Promises act as monads through .then(), which automatically unwraps nested promises returned within its callback. (8:10)

Detailed notes

## Background Published by the YouTube channel Fun Fun Function on February 29, 2016, this video tackles the concept of monads in JavaScript. Host MPJ explains that while a functor is an object implementing `map`, a monad is a more powerful functor that also implements `flatMap` to eliminate nested containers.

## TL;DR In this episode, MPJ cuts through academic category theory and Haskell jargon to deliver an intuitive, practical explanation of monads for everyday developers. Using reactive event streams with Bacon.js and the Google Translate API, he demonstrates how applying `.map()` to an asynchronous operation results in awkward nested streams. By replacing `.map()` with `.flatMap()`, the inner stream is automatically unwrapped, revealing the bare value. He concludes by explaining how common tools like JavaScript Promises use `.then()` as a monad flattening mechanism under an alternate name, alongside Haskell's `bind` and Fantasy Land's `chain`.

## Atmosphere & style The video maintains an energetic, pragmatic, and approachable tone typical of MPJ's 'Failing Together' ethos. Rather than intimidating viewers with rigid mathematical formalism, MPJ adopts an empathetic, conversational delivery filled with lighthearted self-deprecation and expressive reactions when code fails. It feels like pairing with an enthusiastic senior colleague who deliberately demystifies complex functional programming concepts through live coding and relatable real-world examples.

## Bulletin - [00:00](https://www.youtube.com/watch?v=9QveBbn7t_c&t=0s) A monad is fundamentally a more powerful functor that implements flatMap (or equivalent methods like bind, chain, or then) in addition to map. - [05:07](https://www.youtube.com/watch?v=9QveBbn7t_c&t=307s) Using standard map on functions returning container structures like streams causes nested wrapping (Stream<Stream<Value>>), whereas flatMap flattens and unwraps the inner value. - [08:10](https://www.youtube.com/watch?v=9QveBbn7t_c&t=490s) JavaScript Promises act as monads through .then(), which automatically unwraps nested promises returned within its callback.

## Key takeaways - [00:00](https://www.youtube.com/watch?v=9QveBbn7t_c&t=0s) Monads build directly on functors, requiring an understanding of functors before tackling monads. - [00:00](https://www.youtube.com/watch?v=9QveBbn7t_c&t=0s) While a functor is an object implementing map, a monad is a functor that also implements flatMap. - [00:00](https://www.youtube.com/watch?v=9QveBbn7t_c&t=0s) MPJ advocates prioritizing intuitive, practical utility over getting bogged down in academic category theory and Haskell-specific laws. - [01:42](https://www.youtube.com/watch?v=9QveBbn7t_c&t=102s) Reactive streams in libraries like Bacon.js, RxJS, and Highland act as functors by supporting transformations via .map(). - [01:42](https://www.youtube.com/watch?v=9QveBbn7t_c&t=102s) Asynchronous operations returning promises can be integrated into reactive workflows using wrappers like Bacon.fromPromise(). - [05:07](https://www.youtube.com/watch?v=9QveBbn7t_c&t=307s) Applying regular .map() to an asynchronous operation that returns another stream produces nested stream objects rather than actual values. - [05:07](https://www.youtube.com/watch?v=9QveBbn7t_c&t=307s) The flatMap method resolves nesting by awaiting inner stream emission and unwrapping the contained value before forwarding it. - [08:10](https://www.youtube.com/watch?v=9QveBbn7t_c&t=490s) ECMAScript Promises effectively behave as monads because .then() automatically flattens nested promises returned by callbacks. - [08:10](https://www.youtube.com/watch?v=9QveBbn7t_c&t=490s) The flattening operation known as flatMap is referred to as bind in Haskell, chain in Fantasy Land libraries, and then in JavaScript Promises. - [08:10](https://www.youtube.com/watch?v=9QveBbn7t_c&t=490s) Once flattened, stream values can immediately be chained back into standard functor transformations like .map().

## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=9QveBbn7t_c&t=0s) Demystifying the Monad: Practical Understanding Over Haskell Theory

MPJ opens the episode by emphasizing that monads build directly on the concept of functors, meaning viewers must understand functors first for the discussion to make any sense. While a functor is fundamentally an object or data structure that implements `map`, a monad is simply a more powerful functor that also implements `flatMap`.

MPJ issues a warning about how monads are commonly taught across the internet. He points out that most existing material gets hopelessly bogged down in mathematical precision, rigid category theory laws, and Haskell-specific implementations. MPJ explains his philosophy for cutting through the academic jargon: “I think that doing it with that level of detail and correctness is going to cause you to not see the forest for all the trees so we're not going to do that in this video.” Rather than memorizing abstract monad laws upfront, the goal is to give developers a clear, intuitive sense of what a monad is and how it solves everyday programming problems. Once that practical mental model clicks, the deeper theoretical details can always be learned later.

```mermaid flowchart LR accTitle: Hierarchy of Functors and Monads accDescr: Visual diagram showing that Monad is a specialized Functor implementing both map and flatMap.

subgraph FunctorSpace["Functor Interface"] F["Functor"] -->|"implements"| M["map()"] end

subgraph MonadSpace["Monad Interface"] MO["Monad"] -->|"inherits / implements"| M2["map()"] MO -->|"additionally implements"| FM["flatMap() / then / bind / chain"] end ```

[01:42](https://www.youtube.com/watch?v=9QveBbn7t_c&t=102s) Functors with Bacon.js Streams and Translating Words

To understand why `flatMap` is necessary and what is actually "flat" about it, MPJ demonstrates working with reactive event streams using Bacon.js. While Bacon.js is used here, he notes that stream concepts are identical across other reactive programming libraries like RxJS or Highland.

MPJ creates a Bacon `Bus`, which represents the simplest form of stream in Bacon.js. By calling `stream.onValue(callback)`, the provided callback executes whenever an item is emitted. When three strings—`"cat"`, `"meal"`, and `"trumpet"`—are pushed into the bus, `console.log` immediately prints each word. Because streams are functors, they support `.map()`. MPJ illustrates this by transforming each incoming item with `.map(word => word.toUpperCase())`, which instantly outputs `"CAT"`, `"MEAL"`, and `"TRUMPET"`.

Next, MPJ elevates the example to a realistic asynchronous task: using the Google Translate API to translate each word into Portuguese. He mentions as a brief aside that he will be speaking in August at BrazilJS, the world's largest JavaScript conference. He constructs a helper function called `getInPortuguese(word)` that issues an HTTP GET request via the Fetch API. Because Fetch returns an ES6 promise while the rest of the application runs on Bacon.js, MPJ bridges the two by passing the promise into `Bacon.fromPromise(promise)`. This allows `getInPortuguese` to return a Bacon stream that emits the translated word once the network request finishes.

[05:07](https://www.youtube.com/watch?v=9QveBbn7t_c&t=307s) The Problem with Nested Streams and the Need for `flatMap`

When MPJ attempts to connect the bus stream to the translation helper using regular `.map()`, everything breaks down:

```javascript stream .map(word => getInPortuguese(word)) .onValue(word => console.log(word)); ```

Instead of printing the translated strings, running the program outputs stream instances. Because `getInPortuguese` itself returns a stream, mapping over the input stream results in a stream containing inner streams. As MPJ humorously laments: “Functor and map you have failed us.”

```mermaid flowchart TD accTitle: Map versus FlatMap behavior on nested streams accDescr: Comparison diagram showing how map produces nested streams whereas flatMap unwraps inner values.

subgraph MapFlow["Stream.map(getInPortuguese)"] A["'cat'"] --> B["map()"] B --> C["Stream (Stream &lt;'gato'&gt;)"] C --> D["Output: [Bacon Stream Object]"] end

subgraph FlatMapFlow["Stream.flatMap(getInPortuguese)"] E["'cat'"] --> F["flatMap()"] F --> G["Stream &lt;'gato'&gt; (unwrapped)"] G --> H["Output: 'gato'"] end ```

This is the exact problem monads are designed to resolve. MPJ replaces `.map()` with `.flatMap()`, and the application suddenly logs the actual Portuguese translations. FlatMap intercepts the inner stream returned by `getInPortuguese`, waits for it to emit its value, unwraps (flattens) that contained value out of the stream container, and forwards the bare value to the downstream `.onValue()` listener. When MPJ temporarily switches the code back to `.map()` to prove the contrast, the console reverts to spewing raw Bacon stream objects. Switching back to `.flatMap()` immediately restores the unwrapped strings.

- Calling `.map()` on a functor that returns another functor creates unwanted nesting: `Stream<Stream<Value>>`. - Calling `.flatMap()` automatically unpacks the inner functor into a single flat stream: `Stream<Value>`.

[08:10](https://www.youtube.com/watch?v=9QveBbn7t_c&t=490s) Monads Under Other Names: Promises, `.then()`, `chain`, and `bind`

MPJ addresses a common point of confusion among JavaScript developers: whether native Promises are monads. While promises lack a method literally named `flatMap`, their `.then()` method acts as a `flatMap` in disguise. When a function passed to `.then()` returns another promise, `.then()` automatically unwraps that promise rather than nesting it.

MPJ illustrates this by looking at how `fetch()` operates:

```javascript fetch(url) .then(response => response.json()) .then(parsedResponse => {... }); ```

Calling `response.json()` does not return a raw object literal; it returns another Promise. The surrounding `.then()` automatically flattens that promise down to the resolved parsed JSON object before handing it to the subsequent `.then()` callback.

MPJ clarifies that the functional programming community uses several names for the exact same flattening operation: - `flatMap` (common in modern JavaScript and stream libraries like Bacon.js and RxJS) - `bind` (standard Haskell terminology) - `chain` (common in Fantasy Land and algebraic JavaScript libraries) - `then` (standard ECMAScript Promises)

Because the flattened stream outputs clean word values, it can immediately be chained back into regular functor operations like `.map(word => word.toUpperCase())`. In summary: “flat map has the same principle as map with the exception that if the callback pass to flat map returns a monad of the same type you know if if stream if you're calling flat map on stream it returns a stream monad that stream monad will be flattened into its containing value before it's it's passed on”.

MPJ wraps up by inviting viewers to share their lingering questions and confusion in the comments for an upcoming follow-up video, reminding the audience that new episodes drop every Monday morning at 08:00 GMT.

## Mentioned Fun Fun Function, MPJ, Haskell, Bacon.js, RxJS, Highland, Google Translate API, BrazilJS, Fetch API, Fantasy Land, @drboolean, CircleCI, Quokka

## Video description 💖 Support the show by becoming a Patreon https://www.patreon.com/funfunfunction

Monads is a type of Functor, so you need to know what a Functor is in order for this episode to make sense. Thankfully, there there is a FunFunFunction episode for that, and you can watch it here: So, what is a monad? Remember that a Functor is an object that implements *map*? Well, a monad is a more powerful functor that also implements *flatMap*.

Code from the episode https://gist.github.com/mpj/ca167aad371c67372f3b

Mad props to @drboolean for helping me to understand Monads. Besides following him on Twitter, you should watch his awesome “Classroom Coding with Prof. Frisby Pt1” https://www.youtube.com/watch?v=h_tkIpwbsxY&list=PLK_hdtAJ4KqX0JOs_KMAmUNTNMRYhWEaC

Quick intro to Fetch API http://www.sitepoint.com/introduction-to-the-fetch-api/

BaconJS https://baconjs.github.io/

Google Translate API https://cloud.google.com/translate/v2/getting_started

💛 Follow on Twitch and support by becoming a Subscriber We record the show live Mondays 7 AM PT https://twitch.tv/funfunfunction

💛 Fun Fun Forum Private discussion forum with other viewers in between shows. https://www.funfunforum.com. Available to patron members, become one at https://www.patreon.com/funfunfunction

💛 mpj on Twitter https://twitter.com/mpjme

💛 CircleCI (Show sponsor) Robust and sleek Docker-based Continuous Integration as a service. I used CircleCI prior to them becoming a sponsor and I love that their free tier is powerful enough for small personal projects, even if they are private. Use this link when you sign up to let them know you came from here: https://circleci.funfunfunction.com

💛 Quokka (Show sponsor) Wonder how MPJ evaluates JavaScript inline his editor. Quokka is the answer - use this link when you buy to let them know you came from here: http://quokka.funfunfunction.com

💛 FUN FUN FUNCTION Since 2015, Fun Fun Function (FFF) is one of the longest running weekly YouTube shows on programming 🏅 thanks to its consistency and quality reaching 200,000+ developers.

🤦‍♂️ The Failing Together concept is what makes FFF unique. Most coding content out there focus on step-by-step tutorials. We think tutorials are too far removed from what everyday development is like. Instead, FFF has created a completely new learning environment where we grow from failure, by solving problems while intensively interacting with a live audience.

Tutorials try to solve a problem. Failing Together makes you grow as a developer and coworker.

📹 Each show is recorded live on Twitch in a 2-hour livestream on Mondays. The host, assisted by the audience, is tasked to complete a programming challenge by an expert guest. Like in the real world, we often fail, and learn from it. This, of course, reflects what the audience identifies with, and is one of the most praised aspects of the show.

⏯ On Fridays, an edited version of the show is adapted for and published on YouTube.

Content Topics revolve around: JavaScript, Functional Programming, Software Architecture, Quality Processes, Developer Career and Health, Team Collaboration, Software Development, Project Management

[Source](https://www.youtube.com/watch?v=9QveBbn7t_c)

中文简报

JavaScript 开发者引入单子 FlatMap 解决流嵌套问题 当回调返回数据容器时,标准 map 操作往往失效,因而需要单子通过 flatMap 或 Promise 链式调用解包嵌套结构。 在本期内容中,MPJ 抛开学术性的范畴论与 Haskell 行话,为普通开发者通俗易懂地讲解了单子(Monad)的实际应用。结合 Bacon.js 响应式事件流与 Google 翻译 API,他展示了对异步操作使用 `.map()` 如何导致麻烦的流嵌套问题。而将 `.map()` 替换为 `.flatMap()` 后,内部流会被自动解包并提取出原始值。他最后解释道,像 JavaScript Promise 这样的常用工具本质上也是以 `.then()` 作为别名实现了单子的扁平化机制,这与 Haskell 中的 `bind` 以及 Fantasy Land 规范中的 `chain` 概念一致。 单子本质上是一种功能更强大的函子(Functor),它在 map 的基础上还实现了 flatMap(或诸如 bind、chain、then 等等效方法)。 (0:00) 对返回流等容器结构的处理函数使用标准 map 会导致嵌套包装(Stream<Stream<Value>>),而 flatMap 则能将其扁平化并解包出内部的值。 (5:07) JavaScript 的 Promise 通过 .then() 发挥单子的作用,能够自动解包其回调内部返回的嵌套 Promise。 (8:10)

Reading summary

MPJ argues that a monad is best understood practically as a functor that additionally implements flatMap to unwrap nested containers, rather than through abstract category theory. By focusing on everyday asynchronous programming challenges, developers can build an intuitive mental model before tackling formal mathematical definitions. A Practical Alternative to Category Theory (0:00) MPJ cautions that mainstream programming tutorials often fail developers by getting bogged down in category theory laws, mathematical precision, and Haskell-specific implementations. He argues that this academic approach prevents learners from seeing the broader practical utility of the pattern. Instead, he presents a monad simply as an extension of a functor: while a functor is any object or data structure implementing map, a monad is a more capable functor that also implements flatMap. In MPJ's view, establishing an intuitive mental model through everyday code takes precedence over memorizing abstract monad laws upfront. Once developers understand the concrete problems monads solve, deeper theoretical details can be explored later without impeding day-to-day work. Nested Functor Failures in Reactive Streams (5:07) To demonstrate where standard functors fall short, MPJ examines reactive event streams in Bacon.js, noting that the mechanics are identical across libraries like RxJS or Highland. Emitting strings through a stream and applying a synchronous transformation via map cleanly outputs transformed values. However, the architecture breaks down when introducing asynchronous workflows, such as querying the Google Translate API using the Fetch API wrapped in Bacon.fromPromise. Because the asynchronous translation helper returns a stream of its own, applying regular map over the input stream results in nested stream objects (Stream<Stream<Value>>) rather than clean values. The downstream subscriber receives raw Bacon stream instances instead of translated strings, demonstrating that regular functor mapping fails whenever the mapping callback itself returns a container. Unwrapping Values and Monad Equivalents (8:10) Monads resolve container nesting by replacing map with flatMap. When a callback returns another monad of the same type, flatMap intercepts the inner stream, waits for it to emit, unpacks the contained value, and forwards the bare value to downstream listeners. Once flattened back into an unwrapped stream, the data can immediately be chained back into standard functor operations like map. MPJ points out that JavaScript developers already rely on monads under alternative names. In native ECMAScript Promises, the .then() method functions as a disguised flatMap by automatically unpacking returned promises, such as when parsing response.json() after a fetch call. Across functional programming ecosystems, this exact flattening behavior appears under various identifiers, including bind in Haskell, chain in Fantasy Land libraries, and flatMap in reactive programming. MPJ concludes that monads exist to solve the nesting dilemma created when functor transformations return new container instances. He leaves open questions for an upcoming follow-up video to address viewer confusion and edge cases.

精炼阅读摘要

主讲人 MPJ 认为,开发者理解 Monad(单子)并不需要死记硬背范畴论数学法则或 Haskell 专有语法,其本质就是一个在函子(Functor)基础上额外实现了扁平化方法(flatMap)的高阶容器接口。引入 Monad 的核心工程价值在于解决嵌套容器结构无法直接消费内部值的问题,从而在异步与响应式编程中实现流畅的链式调用。 超越学术定义的实用主义认知模型 (0:00) MPJ 在节目开篇指出,Monad 的概念完全建立在函子之上,因此理解 Monad 必须以函子为基础。从数据结构接口来看,函子是实现了 `map` 方法的容器对象,而 Monad 则是一个功能更强大的函子,它在保留 `map` 能力的同时进一步实现了 `flatMap`。 针对网络上普遍存在的教学方式,MPJ 提出了批评,认为绝大多数教程过度沉溺于数学上的严谨性、刻板的范畴论定律以及 Haskell 的具体实现细节。他主张开发者应当避开这种“只见树木不见森林”的学术繁文缛节,首要目标是建立直观的实用心智模型,掌握 Monad 如何解决日常编码中的实际问题;至于高深的理论证明与定律,完全可以在具备直观认识后再深入探究。 响应式数据流与嵌套容器引发的失效 (1:42) 为了具象化展示扁平化操作的必要性,MPJ 以响应式流库 Bacon.js 为例展开演示,并说明该机制在 RxJS 或 Highland 等流式库中完全通用。当使用基础的 Bacon `Bus` 推送包含 `"cat"`、`"meal"` 与 `"trumpet"` 的字符串流时,由于流本身是函子,调用常规的 `.map(word => word.toUpperCase())` 能够立即通过 `onValue` 监听器输出全大写词汇。 然而,当业务逻辑升级为真实异步任务——通过 Google Translate API 将英文单词翻译为葡萄牙语时,传统函子便陷入困境。MPJ 编写了辅助函数 `getInPortuguese(word)`,该函数利用 Fetch API 发送 HTTP GET 请求,并通过 `Bacon.fromPromise` 将 ES6 Promise 桥接转换为 Bacon 流对象。在此结构下,如果直接对原始流调用 `.map(word => getInPortuguese(word))`,输出端得到的不再是翻译后的文本内容,而是一连串内部流实例对象,形成了未解包的 `Stream<Stream<Value>>` 嵌套层级,常规 `map` 方法彻底失效。 flatMap 的自动解包与单子运行逻辑 (5:07) 针对嵌套流导致的消费阻塞,MPJ 说明这正是 Monad 旨在解决的核心痛点。在代码中将 `.map()` 替换为 `.flatMap()` 后,应用程序立即正常输出了对应的葡萄牙语翻译字符串。从运行机制来看,`flatMap` 能够拦截 `getInPortuguese` 返回的内部流,主动等待其发出数据,随后将包含在流容器中的纯净值拆封(扁平化)出来,并直接转发给下游的 `.onValue()` 监听器。 MPJ 通过反复切换 `.map()` 与 `.flatMap()` 证明了两者在容器处理上的本质差异:对返回函子的操作调用 `.map()` 会不可避免地产生深层嵌套结构(如 `Stream<Stream<Value>>`);而调用 `.flatMap()` 则会自动将内层函子展开解包,合并还原为单一扁平的 `Stream<Value>`。经过扁平化处理后的流输出的是纯粹值,因此可以继续无缝接入常规函子操作,例如链式追加 `.map(word => word.toUpperCase())` 进行二次转换。 不同语境中的单子别名与 Promise 本质 (8:10) 针对 JavaScript 生态中原生 Promise 是否属于 Monad 的长期争议,MPJ 澄清指出,尽管 Promise 在 API 层面并未字面提供名为 `flatMap` 的方法,但其原生的 `.then()` 方法在逻辑上正是 `flatMap` 的化身。当传递给 `.then()` 的回调函数返回另一个 Promise 时,`.then()` 并不会产生嵌套 Promise,而是自动将其内部解析的值解包摊平并传递给下一个 `.then()` 回调,这一行为在处理 `fetch(url).then(response => response.json())` 解析 JSON 数据时体现得尤为明显。 MPJ 进一步梳理了函数式编程领域对同一种扁平化操作的多样化命名:在现代 JavaScript、Bacon.js 与 RxJS 等流式库中通常称为 `flatMap`;在 Haskell 语言规范中被定义为 `bind`;在 Fantasy Land 规范及代数式 JavaScript 库中写作 `chain`;而在 ECMAScript Promise 标准中则表现为 `.then`。这些不同的术语本质上共享相同的核心准则:当回调返回同类型的容器时,系统会自动将该容器解包为内部所承载的值后再向下传递。 MPJ 总结强调,Monad 的实质并非晦涩的理论教条,而是通过特定解包逻辑化解同类型容器多层嵌套的工程模式;对于观众可能存在的更深层疑虑,他计划在后续视频中针对具体问题展开进一步解答。

Detailed notes — zh

## Background 该视频由 YouTube 频道 Fun Fun Function(主持人为 MPJ)于 2016 年 2 月 29 日发布。节目简介指出 Monad 本质上是一种功能更强大的 Functor(函子),它不仅实现了 `map`,还实现了 `flatMap`,本期内容旨在帮助开发者建立对 Monad 的直观理解。 ## TL;DR 本期节目避开了 Haskell 范畴论中晦涩的“单子公理”,从实用角度直观解析 Monad 的核心作用。MPJ 首先利用 Bacon.js 响应式流演示了 Functor 的 `map` 操作,随后引入异步 Google Translate API 场景,展示了普通 `map` 会导致产生“流中之流”的嵌套容器问题。接着,他演示了 Monad 如何通过 `flatMap` 压平并解包内层容器以获取真实数据,并指出 JavaScript 中的 `Promise.prototype.then` 以及函数式库中的 `bind`、`chain` 本质上都是 `flatMap` 的别名与具体实现。 ## Atmosphere & style 视频基调轻快诙谐、生动平易近人,MPJ 延续了他标志性的周一早间能量,用美食秀大厨端出预制菜般的自嘲风格化解抽象概念的沉重感。讲解过程既有遇到嵌套流报错时的即兴搞怪,也有改用 `flatMap` 跑通代码后的豁然开朗,整体氛围像是一位资深技术好友在白板前用最通俗的比喻为你点破玄机,充满务实与求知的乐趣。 ## Bulletin - [00:00](https://www.youtube.com/watch?v=9QveBbn7t_c&t=0s) 单子(Monad)本质上是增强版的函子(Functor),在 Functor 实现 map 的基础上进一步实现了 flatMap,用于解决容器嵌套问题。 - [03:26](https://www.youtube.com/watch?v=9QveBbn7t_c&t=206s) 当映射函数返回一个异步流或容器时,普通 map 会导致生成嵌套流(Stream of Streams),而 flatMap 会在将值传递给下游前回调并压平内层容器。 - [08:10](https://www.youtube.com/watch?v=9QveBbn7t_c&t=490s) JavaScript 中的 Promise 实际上就是一种 Monad,其 .then() 方法承担了 flatMap 的解包与压平职责,在其他函数式语言中该操作也被称为 bind 或 chain。 ## Key takeaways - [00:00](https://www.youtube.com/watch?v=9QveBbn7t_c&t=0s) Monad 是实现了 flatMap 方法的 Functor,学习时应优先建立宏观体感,而非过早陷入严苛的 Haskell 单子公理和范畴论细节。 - [01:42](https://www.youtube.com/watch?v=9QveBbn7t_c&t=102s) Functor 是实现了 map 方法的容器对象,支持对其内部包含的数据执行链式变换。 - [01:42](https://www.youtube.com/watch?v=9QveBbn7t_c&t=102s) 响应式流(如 Bacon.js、RxJS)本身具备 Functor 特性,可通过链式调用 map 对流动的数据应用纯函数变换。 - [03:26](https://www.youtube.com/watch?v=9QveBbn7t_c&t=206s) 在流式处理中调用返回 Promise 或流的异步函数时,若使用普通 map 会产生嵌套容器,导致订阅者接收到的是流对象而非实际业务数据。 - [06:20](https://www.youtube.com/watch?v=9QveBbn7t_c&t=380s) flatMap 的核心作用在于:当映射回调返回相同类型的容器时,会先等待其解析完成并将其“压平”,提取出内部真实值后再传递给后续处理流程。 - [08:10](https://www.youtube.com/watch?v=9QveBbn7t_c&t=490s) JavaScript 原生的 Promise 即为 Monad,其 .then() 方法在底层同时兼具了 map 和 flatMap 的功能特性。 - [08:10](https://www.youtube.com/watch?v=9QveBbn7t_c&t=490s) flatMap 在不同语言和库中有多种别名,常见的包括 bind、chain 以及 Promise 中的 then,底层机制完全一致。 - [08:30](https://www.youtube.com/watch?v=9QveBbn7t_c&t=510s) 在 fetch API 请求中,response.json() 返回的是 Promise,紧随其后的 .then() 正是通过解包压平将其转换为实际的 JavaScript 对象字面量。 - [08:55](https://www.youtube.com/watch?v=9QveBbn7t_c&t=535s) 经 flatMap 压平解包后的数据依然处于最外层 Monad 管道内,开发者可以继续链式调用普通 map 执行后续的数据变换操作。 ## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=9QveBbn7t_c&t=0s) 单子(Monad)的直观定义与学习视角 主持人 MPJ 在周一早晨向观众问好,并切入当期主题:单子(Monad)。Monad 本质上是一种函子(Functor),因此在理解 Monad 之前,观众必须先了解 Functor(FunFunFunction 之前专门做过一期关于 Functor 的视频)。MPJ 针对网上的教学资料提出了一个免责声明:互联网上大部分关于 Monad 的资料都过于纠结细节,通篇在讲严苛的“单子公理(Monad laws)”以及 Haskell 中的具体实现机制。MPJ 批评这种教法: > “And I think that doing it with that level of detail and correctness is going to cause you to not see the forest for all the trees so we're not going to do that in this video.” 他强调本期视频的目标是让大家形成对 Monad 的直观体感,明白它是做什么的、如何使用,而不是死抠每一个细节;细节完全可以在掌握核心概念之后再去深入。那么 Monad 到底是什么?Functor 是实现了 `map` 的对象,而 Monad 则是一个更强大的 Functor,它在此基础上还实现了 `flatMap`。这就自然引出了核心疑问:`flatMap` 到底是个什么东西?它有什么用?它到底“扁平(flat)”在哪里? - Functor 的核心特征:实现了 `map` 方法。 - Monad 的核心特征:不仅是 Functor,还实现了 `flatMap`。 - 教学目标:建立对 Monad 的宏观感知,避免陷入 Haskell 范畴论细节中“只见树木,不见森林”。 --- ### [01:42](https://www.youtube.com/watch?v=9QveBbn7t_c&t=102s) 用 Bacon.js 流演示 Functor 与 map 为了解释 `flatMap`,MPJ 引入了响应式流(Streams)的代码。虽然这里使用的是 Bacon.js 库,但他指出流的概念是通用的,不管是 RxJS、Highland 还是未来出现的任何流式库,其背后的原理都完全相通。代码一开始创建了一个 `Bacon.Bus`(Bacon.js 中最基础的流),然后通过 `.push()` 推送了 `"cat"`、`"meal"` 和 `"trumpet"` 三个词。接着给流挂载了 `.onValue()` 监听器,每当有单词被推送并发出时,传入的回调函数就会执行 `console.log`,终端随即打印出这三个词。 因为流本身就是 Functor,所以可以直接使用 `map`。MPJ 演示了在流上调用 `.map(word => word.toUpperCase())`,数据经过变换后,终端输出变成了全大写的 `"CAT"`、`"MEAL"`、`"TRUMPET"`。 ```mermaid flowchart LR accTitle: Stream Map Flow accDescr: 展示单词如何通过流中的 map 方法被转为全大写 A["'cat'"] --> B["stream.map(toUpperCase)"] --> C["'CAT'"] ``` - Bacon.js 的 `Bus`:最基础的流输入机制,可通过 `push` 手动发射值。 - `.onValue(callback)`:流的订阅消费方法,流每次发射值时触发回调。 - 流的 Functor 特性:支持链式调用 `.map()` 处理流中流动的值。 --- ### [03:26](https://www.youtube.com/watch?v=9QveBbn7t_c&t=206s) 异步 API 与 map 带来的嵌套流问题 MPJ 提出了一个更贴近实际业务的场景:假设需要调用 Google Translate API 把这些英文单词翻译成葡萄牙语(他还顺带插播了一句自己 8 月份要去全球最大的 JavaScript 大会 BrazilJS 演讲的趣事)。他像美食节目的电视大厨一样拿出了预先写好的代码:函数 `getInPortuguese(word)` 使用 `fetch` 发起 GET 请求去调用 Google Translate API。由于 `fetch` 返回的是原生 ES6 Promise,而应用使用的是 Bacon.js,因此代码使用 `Bacon.fromPromise()` 把 Promise 转换成了 Bacon 流并返回。 当 MPJ 试图将翻译函数和之前的流结合起来时,问题暴露了。他写下了如下调用: ```javascript stream .map(word => getInPortuguese(word)) .onValue(word => console.log(word)) ``` 运行这段代码时,控制台没有打印出期望的葡萄牙语单词,而是打印出了一连串内部流对象(Stream 实例)。MPJ 惊呼: > “Oops, seems like map is just giving us the individual stream items... Functor and map, you have failed us.” 出现这个问题的根本原因在于,`getInPortuguese` 本身返回的是一个流,而普通的 `map` 只是原封不动地把回调的返回值包装到外层流中,结果导致外层流里流动的是一个个“内层流”(即流的流,发生了类型嵌套),而不是翻译后的文本内容。 ```mermaid flowchart TD accTitle: Map vs Nested Streams accDescr: 说明普通 map 导致流嵌套成流中的流,从而拿不到最终值 subgraph MapIssue["普通的 map 处理异步流"] A["'cat'"] -->|"map(getInPortuguese)"| B["Stream(Stream('gato'))"] B -->|".onValue()"| C["打印输出: Stream 实例对象(失败)"] end ``` - `fetch`:返回 ES6 Promise。 - `Bacon.fromPromise(promise)`:将 Promise 转化为可观察的 Bacon.js 流。 - 普通 `map` 的局限:当映射函数返回一个容器(如 Stream)时,`map` 只会产生嵌套容器(Stream of Streams),无法直接获取内层数据。 --- ### [06:20](https://www.youtube.com/watch?v=9QveBbn7t_c&t=380s) flatMap 登场:压平并解包 Monad 正是为了解决这种嵌套容器问题,Monad 登场了。Monad 是一个更强大的 Functor,它提供了 `flatMap`。`flatMap` 的工作原理与 `map` 类似,但它多做了一步:如果回调函数返回的是一个流,它会把这个流“压平(flatten)”,等待内层的流解析出真实数据,并将内层流包含的值提取出来传递给后续的处理逻辑。 MPJ 把代码中的 `.map()` 改为 `.flatMap()`: ```javascript stream .flatMap(word => getInPortuguese(word)) .onValue(word => console.log(word)) ``` 重新运行代码后,控制台顺利输出了翻译结果葡萄牙语单词(输出了 `"treta"` 等内容)。为了加深印象,MPJ 又现场把代码切回 `map`,向观众展示控制台再次打印出 Stream 对象;随后又改回 `flatMap`,再次证明流被顺利压平。正如 MPJ 所总结的那样: > “What flatMap does is that it will take the return stream here... and it will wait for that to resolve, you know, flatten it into its actual word value. It will take the stream, flatten it into the contained word value, and then it will pass it on to the onValue callback here.” ```mermaid flowchart LR accTitle: FlatMap Unwrapping Flow accDescr: 说明 flatMap 如何压平流并获取真实解析值 A["'cat'"] -->|"flatMap(getInPortuguese)"| B["等待内层 Stream 解析"] B -->|"压平展开 (flatten)"| C["'gato' (真实字符串)"] C -->|".onValue()"| D["打印输出: gato(成功)"] ``` - `flatMap` 的本质:先映射(map),若返回值是同类 Monad 容器,则将其解包压平(flatten)。 - 区别于普通 `map`:普通 `map` 盲目传递容器本身,`flatMap` 则会穿透容器,取出其内部包含的具体值。 --- ### [08:10](https://www.youtube.com/watch?v=9QveBbn7t_c&t=490s) Promise 就是 Monad:别名与全流程回顾 有人常说“Promise 是 Monad”,但初学者常会困惑:Promise 上根本没有叫 `flatMap` 的方法啊?MPJ 解释道,Promise 实际上实现了 `flatMap`,只是在 JavaScript 中它不叫 `flatMap`,而是叫 `.then()`。这也是 Monad 经常让人感到比实际更复杂的原因之一——不同的语言和库给 `flatMap` 起了一堆不同的名字,比如 `bind`、`chain`,但它们的底层思想与实现模式是完全一致的。 MPJ 进一步通过刚才编写的 `fetch` 代码来证明这一点。在获取翻译结果时,代码写了 `response.json()`。很多人以为 `response.json()` 直接返回解析好的 JavaScript 对象字面量,但实际上它返回的是一个 Promise。紧接着调用的 `.then()` 负责将这个 Promise 压平为其解析后的对象字面量(`parsedResponse`),然后才将该对象传给下一个 `.then()` 回调去提取具体的字段。这完全就是 Monad 的行为。 回到 Bacon.js 流的示例中,既然 `flatMap` 吐出的是压平后的普通值,流本身依然是 Functor,因此可以在后面继续无缝串联普通操作,例如接上 `.map(word => word.toUpperCase())`,直接把翻译出的葡萄牙语单词变成大写输出。 MPJ 对整期内容做了精炼总结: > “So in summary, a monad is a functor—stream, for instance—that implements flatMap in addition to map, which all functors implement. FlatMap has the same principle as map, with the exception that if the callback passed to flatMap returns a monad of the same type... that stream monad will be flattened into its containing value before it's passed on.” 视频最后,MPJ 询问大家是否通过这一解释真正理解了 Monad,并表示后续很有可能会制作答疑视频,号召大家在评论区留下疑惑。他提醒大家关注每周一早晨 GMT 08:00 更新的节目,并用标志性的结语收尾:“Until next Monday morning, stay curious.” - `flatMap` 的常见别名: - Promise 体系中被称为 `.then()`; - 函数式编程语言/库中常被称为 `bind`、`chain`。 - `fetch` 响应链中的 Monad 机制:`response.json()` 返回 Promise,后续的 `.then()` 自动压平解包出对象字面量,允许继续访问其属性。 - 链式组合能力:经 `flatMap` 压平后的值依然包裹在最外层的 Monad/Functor 管道中,可以继续调用 `.map()` 执行纯同步变换。 ## Mentioned MPJ, drboolean, Bacon.js, RxJS, Highland, Google Translate API, Fetch API, BrazilJS, CircleCI, Quokka, Twitch, YouTube ## Video description 💖 Support the show by becoming a Patreon https://www.patreon.com/funfunfunction Monads is a type of Functor, so you need to know what a Functor is in order for this episode to make sense. Thankfully, there there is a FunFunFunction episode for that, and you can watch it here: So, what is a monad? Remember that a Functor is an object that implements *map*? Well, a monad is a more powerful functor that also implements *flatMap*. Code from the episode https://gist.github.com/mpj/ca167aad371c67372f3b Mad props to @drboolean for helping me to understand Monads. Besides following him on Twitter, you should watch his awesome “Classroom Coding with Prof. Frisby Pt1” https://www.youtube.com/watch?v=h_tkIpwbsxY&list=PLK_hdtAJ4KqX0JOs_KMAmUNTNMRYhWEaC Quick intro to Fetch API http://www.sitepoint.com/introduction-to-the-fetch-api/ BaconJS https://baconjs.github.io/ Google Translate API https://cloud.google.com/translate/v2/getting_started 💛 Follow on Twitch and support by becoming a Subscriber We record the show live Mondays 7 AM PT https://twitch.tv/funfunfunction 💛 Fun Fun Forum Private discussion forum with other viewers in between shows. https://www.funfunforum.com. Available to patron members, become one at https://www.patreon.com/funfunfunction 💛 mpj on Twitter https://twitter.com/mpjme 💛 CircleCI (Show sponsor) Robust and sleek Docker-based Continuous Integration as a service. I used CircleCI prior to them becoming a sponsor and I love that their free tier is powerful enough for small personal projects, even if they are private. Use this link when you sign up to let them know you came from here: https://circleci.funfunfunction.com 💛 Quokka (Show sponsor) Wonder how MPJ evaluates JavaScript inline his editor. Quokka is the answer - use this link when you buy to let them know you came from here: http://quokka.funfunfunction.com 💛 FUN FUN FUNCTION Since 2015, Fun Fun Function (FFF) is one of the longest running weekly YouTube shows on programming 🏅 thanks to its consistency and quality reaching 200,000+ developers. 🤦‍♂️ The Failing Together concept is what makes FFF unique. Most coding content out there focus on step-by-step tutorials. We think tutorials are too far removed from what everyday development is like. Instead, FFF has created a completely new learning environment where we grow from failure, by solving problems while intensively interacting with a live audience. Tutorials try to solve a problem. Failing Together makes you grow as a developer and coworker. 📹 Each show is recorded live on Twitch in a 2-hour livestream on Mondays. The host, assisted by the audience, is tasked to complete a programming challenge by an expert guest. Like in the real world, we often fail, and learn from it. This, of course, reflects what the audience identifies with, and is one of the most praised aspects of the show. ⏯ On Fridays, an edited version of the show is adapted for and published on YouTube. Content Topics revolve around: JavaScript, Functional Programming, Software Architecture, Quality Processes, Developer Career and Health, Team Collaboration, Software Development, Project Management [Source](https://www.youtube.com/watch?v=9QveBbn7t_c)

Original transcript

[0:00] good Monday morning I'm mpj and this is [0:03] fun fun function today we are going to [0:06] explore monads monads is a type of [0:09] functor so you need to know what a [0:12] funter is before watching this episode [0:15] or it's not going to make sense [0:18] thankfully there is a fun fun function [0:20] episode for that which you can watch [0:23] here a disclaimer a lot of the material [0:27] out there on the into webs talking about [0:29] MO ad is very picky on the details [0:33] talking about the exact monad laws and [0:36] uh how the monad is implemented in hasle [0:40] and I think that doing it with that [0:42] level of detail and correctness is going [0:46] to cause you to not see the forest for [0:49] all the trees so we're not going to do [0:52] that in this video instead I'm going to [0:54] try to make you get a sense of what the [0:58] monut is and how it's used so our [1:01] objective here is to make you get the [1:04] monad not try to get every detail right [1:07] because you can learn that later once [1:10] you've gotten the monad so what is a [1:16] monad do you remember that funter is [1:20] something that implements map a monad is [1:24] a more powerful functor that also [1:27] implements flat map that's the main [1:30] thing about the monad which begs the [1:33] question what the hell is flatmap what [1:36] is flat map good for what is flat about [1:39] flat map before we get to that I'm going [1:42] to show you a little bit of code that [1:44] uses streams in case you're not familiar [1:47] with streams there is a handy episode [1:50] that you can watch here we are using [1:52] bacon JS as our streaming library in [1:55] this example but streams are streams all [1:58] streaming libraries have similar [2:00] functionality what you see in this video [2:02] will for example apply to rxjs or [2:05] Highland or really any other streaming [2:08] library that will be written let me just [2:11] run this to show you what it outputs and [2:13] we start from there so it outputs cat [2:17] meal and trumpet on line two here we are [2:20] creating a bus which is uh the most [2:25] basic type of stream in bacon jazz on [2:28] line 78 and9 and we push a couple of [2:31] words onto the stream these words are [2:34] printed here by this [2:36] console.log which is inside this uh [2:40] function which is passed as a uh [2:42] callback function to the onv value [2:45] function of the stream and this function [2:47] is called every time a word is emitted [2:51] on the stream every time we call Push [2:54] absolutely nothing strange going on here [2:56] in the Stream episode we learned that [2:59] streams are functors and that means that [3:02] we can map them let's do that and [3:04] uppercase each word before we print [3:08] it [3:10] map [3:11] [Music] [3:12] word word. to upper case bam and we run [3:19] that cat meal trumpet great that is very [3:24] useful in the Enterprise world but let's [3:26] say that we uh want to use the Google [3:29] Translate API to translate these words [3:32] to Portuguese side note I will be [3:35] speaking at braziljs the world's largest [3:38] JavaScript conference in August let's uh [3:41] delete this code for now and let's write [3:44] a function that translates a single word [3:48] to Portuguese using Google translate API [3:51] like a good TV chef I have prepared some [3:54] code if you look at the last two lines [3:57] here you see that we call get Portuguese [4:00] with a word and then that will return a [4:04] stream that we just call on value on and [4:07] pass it a call back which will print [4:10] each word let's run [4:13] that he my Portuguese is shite the code [4:19] uses fetch to do a uh get request to the [4:23] uh Google translate API if you're not [4:26] jet familiar with how fetch works I have [4:29] linked linked a good article in the [4:31] description that will get you up to [4:33] speed fetch will return an es6 promise [4:37] but we are working with bacon JS in our [4:40] app so we want our uh function to return [4:42] a bacon stream we can accomplish this by [4:46] passing our promise to bacon from [4:49] promise which will return a bacon stream [4:52] which we subsequently return as the [4:56] return value of the function so the [4:58] stream will be returned from this [4:59] function now and we call on value on it [5:02] with a call back word and it gets [5:05] printed now let's try to tie this [5:07] together with the bus that we had before [5:10] I want to uh delete [5:14] this going to add a few lines here let's [5:18] create a [5:22] stream [5:24] Li Let's uh push some words and we want [5:28] to print the these values out so I'm [5:31] going to go stream and then we're going [5:35] to call [5:37] map and for every map we want to do take [5:42] each word map over it and we want to [5:45] call get in Portuguese for that [5:48] word and when we get the value back from [5:53] that we want to [5:57] console.log it out word let's run that [6:01] and see what [6:04] happens whoops seems like map is just [6:08] giving us the individual stream items [6:11] the map is giving us these and that's [6:14] not what we wanted we wanted the [6:16] translated words funter and map you have [6:20] failed us this is where the monad enters [6:24] our story again monad is a more powerful [6:28] funter that old also has flat map flat [6:32] map works just like map but it will also [6:35] flatten the streams to return the values [6:39] contained within the streams so if we [6:42] change the code here to use flat map [6:47] instead of map see what [6:50] happens we [6:53] get he and treta tra tra ain't this cool [7:00] what flat map does is that it will uh [7:03] take the return stream here from P that [7:06] comes from getting Portuguese and it [7:09] will wait for that to resolve you know [7:13] flatten it into its actual word value it [7:17] will take the stream flatten it into the [7:21] contained word value and then it will [7:25] pass it on to the onv value callback [7:29] here let me change back to map for a [7:31] second just to give you like a rehearsal [7:35] and we run with the known test Jazz that [7:38] gives us the uh the the streams again [7:42] and this is because now [7:44] here getting Portuguese that will return [7:47] streams and map will just take them and [7:51] pass them on to uh on value directly it [7:54] will not perform the flattening of the [7:58] streams then we [8:00] go back to flat [8:01] map and run it [8:04] again it will flatten the streams into [8:07] the values contained within the streams [8:10] you might have heard that some people [8:12] say that promises are monads but here I [8:15] am saying that uh monads are things that [8:19] Implement flat map and Promises they [8:21] don't have flat map do they well they do [8:24] actually it's just not called flat map [8:27] it is called then [8:30] and this is one of those things that [8:32] make monads seem a bit more complicated [8:35] than they actually are they the flat map [8:39] has many names other names that you [8:41] might have heard are bind and chain but [8:44] it is the same idea it's the same [8:46] implementation it's just different names [8:48] you can actually see this in action in [8:51] the uh fetch implementation here [8:53] response. Json here this does not [8:56] actually return a uh an object literal [9:00] that you might expect no this will [9:04] return a promise which then will flatten [9:08] into its value the parest object literal [9:11] and it will pass it onto the Callback of [9:15] the the next then which is this so par [9:19] response here this will is not a promise [9:22] anymore this has now been flattened into [9:25] the object literal just the way we want [9:27] it and then we can call it individual [9:30] properties and extract the uh translated [9:33] word here now back to the stream [9:36] remember that uh streams are funs so we [9:40] can map them let's do that and we just [9:43] call uh word. to upper case boom let's [9:49] run [9:53] that so in summary a monad is a functor [9:59] stream for instance that implements flat [10:02] map in addition to uh map which all funs [10:06] implements flat map has the same [10:08] Principle as map with the exception that [10:11] if the uh callback pass to flat map [10:15] returns a monad of the same type you [10:18] know if if stream if you're calling flat [10:20] map on stream it returns a stream monad [10:23] that uh stream monad will be flattened [10:28] into its contain aining value before [10:31] it's it's passed on and that is why we [10:33] can call map directly on this and and [10:36] get a plain word that we can uppercase [10:39] here that's all I got did this video [10:41] make sense to you did this video make [10:44] you get what a monad is I will most [10:47] likely do a follow-up to this video so [10:49] please post your questions and [10:52] confusions down below uh and I will try [10:55] to address them in the next video that [10:58] would that would really help me if you [11:00] did that speaking of which do not miss [11:03] that episode Monday morning 0800 GMT if [11:07] you don't feel like waiting that long [11:10] you can check out this playlist here [11:12] with all the fun fun function episodes [11:14] and see if something tickles your fancy [11:17] in the meantime I am mpj this is fun fun [11:20] function until next Monday morning stay [11:23] curious