函数式 JavaScript - 函子、单子和承诺 为什么没人能解释单子? 我们准备好理解函子了吗? 范畴、对象和映射(态射) 回到函子 回到单子 承诺 总结

2025-05-25

函数式 JavaScript - 函子、单子和承诺

为什么没有人能解释 Monad?

我们准备好理解函子了吗?

范畴、对象和映射(态射)

回到函子

回到 Monad

承诺

概括

拿着裹着丝带的盒子的人

有人说 aPromise是 a Monad。也有人说 aPromise不是 a Monad。他们都错了……但他们都对了。

当您读完本文时,您将了解什么是FunctorMonad以及它们与有何异同Promise

为什么没有人能解释 Monad?

如果没有理解 Monad 所需的先决词汇,就很难解释 Monad 是什么。

我喜欢理查德费曼的这个视频,他被要求描述两个磁铁之间“发生了什么”。

整个视频令人惊叹和震撼,但如果您对学习有些厌恶,可以直接跳到 6:09。

我无法用任何你熟悉的语言来解释这种吸引力 - Richard Feynman @ 6:09

因此,让我们回顾几个步骤并学习所需的词汇以了解什么Monad是。

我们准备好理解函子了吗?

定义:AFunctor是某个事物Mappable,或者某个类别中的对象之间可以映射的事物。

好的……还没有。不过别害怕,Functors如果你用过Arraymap功能,你已经很熟悉了。

[1, 2, 3].map(x => x * 2) //=> [2, 4, 6]
Enter fullscreen mode Exit fullscreen mode

在我们完全理解 a 之前Functor,我们还必须理解 be 的含义Mappable,而要理解 be 的含义,我们还必须理解 aCategory是什么。那就从这里开始吧。

范畴、对象和映射(态射)

范畴论三角形

Acategory由一组节点(对象)和态射(函数)组成。对象可以是数字、字符串、URL、客户,或者任何你希望组织类似事物的方式。(图中的 X、Y 和 Z 就是对象。)

Amap是将某物从一个对象转换为另一个对象的函数。(f、g 和 fog 是映射)。🔍 Google 提示:map对象之间的 A 称为Morphism

例:可以使用方法将对象中的对象Number Type转换为对象String TypetoString()

// A map of Number -> String
const numberToString = num => num.toString()
Enter fullscreen mode Exit fullscreen mode

您还可以创建maps回自己的对象或更复杂的对象类型。

// A map of Number -> Number
const double = num => num * 2

// A map of Array -> Number
const arrayToLength = array => array.length

// A map of URL -> Promise (JSON)
const urlToJson = url =>
  fetch(url)
    .then(response => response.json())
Enter fullscreen mode Exit fullscreen mode

因此,对象可以像数字或字符串一样简单。对象也可以更抽象,例如用户名、用户 API URL、用户 API HTTP 请求、用户 API 响应、用户 API 响应 JSON。然后,我们可以在每个对象之间创建映射或态射来获取所需的数据。

态射的例子:

  • 用户名 -> 用户 API 网址
  • 用户 API URL -> 用户 API HTTP 请求
  • 用户 API HTTP 请求 -> 用户 API 响应
  • 用户 API 响应 -> 用户 API 响应 JSON

🔍 Google 提示:Function Composition这是一种组合多个mapmorphisms创建新的方法maps。使用我们可以创建一个从到 的Function Composition地图UsernameUser API Response JSON

回到函子

现在我们理解了 be 的含义Mappable,我们最终可以理解 aFunctor是什么。

AFunctor是某种东西Mappable,或者可以在类别中的对象之间映射的某种东西。

AnArrayMappable,所以它是 a Functor。在这个例子中,我将 anArray of Numbers变形为Array of Strings

const numberToString = num => num.toString()

const array = [1, 2, 3]
array.map(numberToString)
//=> ["1", "2", "3"]
Enter fullscreen mode Exit fullscreen mode

注意: 的属性之一Functor是它们始终保持相同类型的。您可以将包含 的Functor变形或任何其他对象,但会确保它始终是。您不能将 的变形ArrayStringsNumbersmapArraymapArrayNumberNumber

我们也可以将此Mappable实用性扩展到其他对象!让我们以这个简单的例子为例Thing

const Thing = value => ({
  value
})
Enter fullscreen mode Exit fullscreen mode

如果我们想Thing以与可映射相同的方式使其Array可映射,我们所要做的就是赋予它一个map功能。

const Thing = value => ({
  value,
  map: morphism => Thing(morphism(value))
//                 ----- -------- -----
//                /        |            \
// always a Thing          |             value to be morphed
//                         |
//             Morphism passed into map
})

const thing1 = Thing(1)               // { value: 1 }
const thing2 = thing1.map(x => x + 1) // { value: 2 }
Enter fullscreen mode Exit fullscreen mode

就是这样Functor!真的就这么简单。

Dr Seuse 的 Thing 1 和 Thing 2

🔍 Google 提示:"Thing" Functor我们创建的被称为Identity

回到 Monad

有时函数会返回一个已经被包装的值。这在使用 时可能会不方便,Functor因为它会将 重新包装到Functor另一个 中Functor

const getThing = () => Thing(2)

const thing1 = Thing(1)

thing1.map(getThing) //=> Thing (Thing ("Thing 2"))
Enter fullscreen mode Exit fullscreen mode

此行为与的行为相同Array

const doSomething = x => [x, x + 100]
const list = [1, 2, 3]

list.map(doSomething) //=> [[1, 101], [2, 102], [3, 103]]
Enter fullscreen mode Exit fullscreen mode

这时就flatMap派上用场了。它和 类似map,只是态射也需要执行包装值的工作。

const Thing = value => ({
  value,
  map: morphism => Thing(morphism(value)),
  flatMap: morphism => morphism(value)
})

const thing1 = Thing(1)                          //=> Thing (1)
const thing2 = thing1.flatMap(x => Thing(x + 1)) //=> Thing (2)
Enter fullscreen mode Exit fullscreen mode

看起来好多了!

Maybe当你需要从 切换Just到 时Nothing(例如缺少道具时),这可能会派上用场。

import Just from 'mojiscript/type/Just'
import Nothing from 'mojiscript/type/Nothing'

const prop = (prop, obj) =>
  prop in obj
    ? Just(obj[prop])
    : Nothing

Just({ name: 'Moji' }).flatMap(x => prop('name', x)) //=> Just ("Moji")
Just({}).flatMap(x => prop('name', x))               //=> Nothing
Enter fullscreen mode Exit fullscreen mode

该代码可以缩短为:

const Just = require('mojiscript/type/Just')
const Nothing = require('mojiscript/type/Nothing')
const { fromNullable } = require('mojiscript/type/Maybe')

const prop = prop => obj => fromNullable(obj[prop])

Just({ name: 'Moji' }).flatMap(prop('name')) //=> Just ("Moji")
Just({}).flatMap(prop('name'))               //=> Nothing
Enter fullscreen mode Exit fullscreen mode

🔍 Google 提示:此代码缩短可通过curryingpartial application和 实现point-free style

也许你期待更多,但这就是 Monad 的全部!Monad 既可以映射,也可以平面映射。

希望此刻,你会觉得这段旅程比你最初想象的要轻松。我们已经讲完了Functors……Monads接下来的内容Promise

承诺

如果任何代码看起来很熟悉,那是因为的Promise行为与map和相似flatMap

const double = num => num * 2

const thing1 = Thing(1)             //=> Thing (1)
const promise1 = Promise.resolve(1) //=> Promise (1)

thing1.map(double)    //=> Thing (2)
promise1.then(double) //=> Promise (2)

thing1.flatMap(x => Thing(double(x)))          //=> Thing (2)
promise1.then(x => Promise.resolve(double(x))) //=> Promise (2)
Enter fullscreen mode Exit fullscreen mode

如您所见,该Promise方法的then工作方式类似于map返回未包装的值时的工作方式flatMap,而当将其包装在 a 中时的工作方式类似于Promise。这样, aPromise类似于 aFunctor和 a Monad

这也是其不同之处。

thing1.map(x => Thing(x + 1))              // Thing (Thing (2))
promise1.then(x => Promise.resolve(x + 1)) // Promise (2)

thing1.flatMap(x => x + 1) //=> 2
promise1.then(x => x + 1)  //=> Promise (2)
Enter fullscreen mode Exit fullscreen mode

如果我想将一个值包装两次(比如嵌套Arrays),或者控制返回类型,我无法用 来实现Promise。这样一来,既违反了Functor法律,也违反了Monad法律。

概括

  • AFunctor是某种东西Mappable,或者可以在类别中的对象之间映射的某种东西。
  • AMonad与 a 相似Functor,但介于Flat Mappable类别之间。
  • flatMap类似于map,但将返回类型的包装控制权交给映射函数。
  • Promise 打破了法律FunctorMonad规则,但仍然有很多相似之处。相同之处,却又不同。

继续阅读:NULL、“十亿美元的错误”、也许什么都没有

我的文章展现了我对函数式 JavaScript 的热爱。如果你想了解更多函数式编程知识,可以在这里或 Twitter 上关注我@joelnet

并感谢我的朋友 Joon 对此进行校对 :)

干杯!

文章来源:https://dev.to/joelnet/featured-javascript---functors-monads-and-promises-1pol
PREV
React:“我真希望我能用这种方式编写组件。” DevTwitter Chrome 扩展程序 🍻 🍻 🍻
NEXT
您使用哪些函数/方法...