函数式 JavaScript - 函子、单子和承诺
为什么没有人能解释 Monad?
我们准备好理解函子了吗?
范畴、对象和映射(态射)
回到函子
回到 Monad
承诺
概括
有人说 aPromise
是 a Monad
。也有人说 aPromise
不是 a Monad
。他们都错了……但他们都对了。
当您读完本文时,您将了解什么是Functor
和Monad
以及它们与有何异同Promise
。
为什么没有人能解释 Monad?
如果没有理解 Monad 所需的先决词汇,就很难解释 Monad 是什么。
我喜欢理查德费曼的这个视频,他被要求描述两个磁铁之间“发生了什么”。
整个视频令人惊叹和震撼,但如果您对学习有些厌恶,可以直接跳到 6:09。
我无法用任何你熟悉的语言来解释这种吸引力 - Richard Feynman @ 6:09
因此,让我们回顾几个步骤并学习所需的词汇以了解什么Monad
是。
我们准备好理解函子了吗?
定义:AFunctor
是某个事物Mappable
,或者某个类别中的对象之间可以映射的事物。
好的……还没有。不过别害怕,Functors
如果你用过Array
的map
功能,你已经很熟悉了。
[1, 2, 3].map(x => x * 2) //=> [2, 4, 6]
在我们完全理解 a 之前Functor
,我们还必须理解 be 的含义Mappable
,而要理解 be 的含义,我们还必须理解 aCategory
是什么。那就从这里开始吧。
范畴、对象和映射(态射)
Acategory
由一组节点(对象)和态射(函数)组成。对象可以是数字、字符串、URL、客户,或者任何你希望组织类似事物的方式。(图中的 X、Y 和 Z 就是对象。)
Amap
是将某物从一个对象转换为另一个对象的函数。(f、g 和 fog 是映射)。🔍 Google 提示:map
对象之间的 A 称为Morphism
。
例:可以使用方法将对象中的对象Number Type
转换为对象。String Type
toString()
// A map of Number -> String
const numberToString = num => num.toString()
您还可以创建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())
因此,对象可以像数字或字符串一样简单。对象也可以更抽象,例如用户名、用户 API URL、用户 API HTTP 请求、用户 API 响应、用户 API 响应 JSON。然后,我们可以在每个对象之间创建映射或态射来获取所需的数据。
态射的例子:
- 用户名 -> 用户 API 网址
- 用户 API URL -> 用户 API HTTP 请求
- 用户 API HTTP 请求 -> 用户 API 响应
- 用户 API 响应 -> 用户 API 响应 JSON
🔍 Google 提示:Function Composition
这是一种组合多个map
或morphisms
创建新的方法maps
。使用我们可以创建一个从到 的Function Composition
地图Username
User API Response JSON
回到函子
现在我们理解了 be 的含义Mappable
,我们最终可以理解 aFunctor
是什么。
AFunctor
是某种东西Mappable
,或者可以在类别中的对象之间映射的某种东西。
AnArray
是Mappable
,所以它是 a Functor
。在这个例子中,我将 anArray of Numbers
变形为Array of Strings
。
const numberToString = num => num.toString()
const array = [1, 2, 3]
array.map(numberToString)
//=> ["1", "2", "3"]
注意: 的属性之一Functor
是它们始终保持相同类型的。您可以将包含 的Functor
变形为或任何其他对象,但会确保它始终是。您不能将 的变形为。Array
Strings
Numbers
map
Array
map
Array
Number
Number
我们也可以将此Mappable
实用性扩展到其他对象!让我们以这个简单的例子为例Thing
。
const Thing = value => ({
value
})
如果我们想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 }
就是这样Functor
!真的就这么简单。
🔍 Google 提示:"Thing"
Functor
我们创建的被称为Identity
。
回到 Monad
有时函数会返回一个已经被包装的值。这在使用 时可能会不方便,Functor
因为它会将 重新包装到Functor
另一个 中Functor
。
const getThing = () => Thing(2)
const thing1 = Thing(1)
thing1.map(getThing) //=> Thing (Thing ("Thing 2"))
此行为与的行为相同Array
。
const doSomething = x => [x, x + 100]
const list = [1, 2, 3]
list.map(doSomething) //=> [[1, 101], [2, 102], [3, 103]]
这时就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)
看起来好多了!
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
该代码可以缩短为:
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
🔍 Google 提示:此代码缩短可通过currying
、partial 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)
如您所见,该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)
如果我想将一个值包装两次(比如嵌套Arrays
),或者控制返回类型,我无法用 来实现Promise
。这样一来,既违反了Functor
法律,也违反了Monad
法律。
概括
- A
Functor
是某种东西Mappable
,或者可以在类别中的对象之间映射的某种东西。 - A
Monad
与 a 相似Functor
,但介于Flat Mappable
类别之间。 flatMap
类似于map
,但将返回类型的包装控制权交给映射函数。- Promise 打破了法律
Functor
和Monad
规则,但仍然有很多相似之处。相同之处,却又不同。
我的文章展现了我对函数式 JavaScript 的热爱。如果你想了解更多函数式编程知识,可以在这里或 Twitter 上关注我@joelnet!
并感谢我的朋友 Joon 对此进行校对 :)
文章来源:https://dev.to/joelnet/featured-javascript---functors-monads-and-promises-1pol