规则优先
首先我们有自然变换定律
-
a 的某个函子 F 与函数 f 映射,产生 b 的 F,然后自然转换,产生一些函子 G 的 b。
-
a 中的某个函子 F,自然转换后产生 a 中的某个函子 G,然后映射到 f 中的某个函子,产生 b 中的 G
选择任一路径(首先映射,然后变换,或先变换,然后映射)将导致相同的最终结果,G 和 b。
nt(x.map(f)) == nt(x).map(f)
变得真实
好的,现在让我们做一个实际的例子。我会一点一点地解释代码,然后我会在最后给出一个完整的可运行示例。
首先我们将实现 Either(使用 Left 和 Right)
const Left = x => ({
map: f => Left(x),
fold: (f,_) => f(x)
})
const Right = x => ({
map: f => Right(f(x)),
fold: (_,f) => f(x),
})
然后我们实现Task
const Task = fork => ({
fork,
// "chain" could be called "bind" or "flatMap", name doesn't matter
chain: f =>
Task((reject, resolve) =>
fork(reject,
x => f(x).fork(reject, resolve)))
})
Task.of = x => Task((reject, resolve) => resolve(x))
Task.rejected = x => Task((reject, resolve) => reject(x))
现在让我们开始定义一些理论程序。我们将有一个用户数据库,其中每个用户都有一个 bff(永远最好的朋友)。我们还将定义一个简单的Db.find 函数,它返回一个在我们的数据库中查找用户的任务。这类似于任何返回 Promise 的数据库库。
// fake database
const data = {
"1": {id: 1, name: 'bob', bff: 2},
"2": {id: 2, name: 'alice', bff: 1}
}
// fake db api
const Db = {
find: id =>
Task((reject, resolve) =>
resolve((id in data) ? Right(data[id]) : Left('not found')))
}
好的,所以有一点点转折。我们的Db.find 函数返回Task 的Either(Left 或Right)。这主要是出于演示目的,但也可以说是一种良好的做法。即,我们可能不会认为 user-not-found 场景是一个错误,因此我们不想 reject 任务 - 相反,我们稍后通过 resolving Left of @ 优雅地处理它987654359@。我们可能会在出现其他错误时使用reject,例如无法连接到数据库等。
制定目标
我们程序的目标是获取给定的用户 id,并查找该用户的 bff。
我们雄心勃勃,但天真,所以我们首先尝试这样的事情
const main = id =>
Db.find(1) // Task(Right(User))
.map(either => // Right(User)
either.map(user => // User
Db.find(user.bff))) // Right(Task(Right(user)))
耶! Task(Right(Task(Right(User)))) ...这很快就失控了。处理这个结果将是一场彻头彻尾的噩梦......
自然转化
这是我们的第一个自然变换eitherToTask:
const eitherToTask = e =>
e.fold(Task.rejected, Task.of)
// eitherToTask(Left(x)) == Task.rejected(x)
// eitherToTask(Right(x)) == Task.of(x)
让我们看看当我们将chain 转换为我们的Db.find 结果时会发生什么
const main = id =>
Db.find(id) // Task(Right(User))
.chain(eitherToTask) // ???
...
那么??? 是什么?那么Task#chain 期望你的函数返回一个Task,然后它将当前任务和新返回的任务压缩在一起。所以在这种情况下,我们去:
// Db.find // eitherToTask // chain
Task(Right(User)) -> Task(Task(User)) -> Task(User)
哇。这已经是一个巨大的改进,因为它在我们进行计算时使我们的数据更加平坦。让我们继续……
const main = id =>
Db.find(id) // Task(Right(User))
.chain(eitherToTask) // Task(User)
.chain(user => Db.find(user.bff)) // ???
...
那么这一步中的??? 是什么?我们知道Db.find 返回Task(Right(User),但我们是chaining,所以我们知道我们至少会将两个Tasks 挤在一起。这意味着我们走了:
// Task of Db.find // chain
Task(Task(Right(User))) -> Task(Right(User))
看一下,我们还有另一个Task(Right(User)),我们已经知道如何将其展平。 eitherToTask!
const main = id =>
Db.find(id) // Task(Right(User))
.chain(eitherToTask) // Task(User)
.chain(user => Db.find(user.bff)) // Task(Right(User))
.chain(eitherToTask) // Task(User) !!!
热土豆!好的,那么我们将如何使用它呢?那么main 接受Int 并返回Task(User),所以...
// main :: Int -> Task(User)
main(1).fork(console.error, console.log)
真的就这么简单。如果Db.find 解析了一个Right,它将被转换为一个Task.of(一个已解析的任务),这意味着结果将转到console.log - 否则,如果Db.find 解析了一个Left,它将被转换为一个@ 987654390@(被拒绝的任务),意味着结果将转到console.error
可运行代码
// Either
const Left = x => ({
map: f => Left(x),
fold: (f,_) => f(x)
})
const Right = x => ({
map: f => Right(f(x)),
fold: (_,f) => f(x),
})
// Task
const Task = fork => ({
fork,
chain: f =>
Task((reject, resolve) =>
fork(reject,
x => f(x).fork(reject, resolve)))
})
Task.of = x => Task((reject, resolve) => resolve(x))
Task.rejected = x => Task((reject, resolve) => reject(x))
// natural transformation
const eitherToTask = e =>
e.fold(Task.rejected, Task.of)
// fake database
const data = {
"1": {id: 1, name: 'bob', bff: 2},
"2": {id: 2, name: 'alice', bff: 1}
}
// fake db api
const Db = {
find: id =>
Task((reject, resolve) =>
resolve((id in data) ? Right(data[id]) : Left('not found')))
}
// your program
const main = id =>
Db.find(id)
.chain(eitherToTask)
.chain(user => Db.find(user.bff))
.chain(eitherToTask)
// bob's bff
main(1).fork(console.error, console.log)
// alice's bff
main(2).fork(console.error, console.log)
// unknown user's bff
main(3).fork(console.error, console.log)
归因
我几乎把整个答案都归功于 Brian Lonsdorf (@drboolean)。他有一个关于 Egghead 的精彩系列,名为 Professor Frisby Introduces Composable Functional JavaScript。巧合的是,您问题中的示例(转换 Future 和 Either)与他的视频中使用的示例以及我在此处的答案中的此代码中使用的示例相同。
关于自然变换的两个是
- Principled type conversions with natural transformations
- Applying natural transformations in everyday work
任务的替代实现
Task#chain 有一点魔力,但不是很明显
task.chain(f) == task.map(f).join()
我将这一点作为附注提到,因为它对于考虑上述从 Either 到 Task 的自然转换并不是特别重要。 Task#chain 足够演示了,但如果你真的想拆开看看一切如何运作,可能会觉得有点难以接近。
下面,我使用map 和join 推导出chain。我会在下面放几个类型注释,应该会有所帮助
const Task = fork => ({
fork,
// map :: Task a => (a -> b) -> Task b
map (f) {
return Task((reject, resolve) =>
fork(reject, x => resolve(f(x))))
},
// join :: Task (Task a) => () -> Task a
join () {
return Task((reject, resolve) =>
fork(reject,
task => task.fork(reject, resolve)))
},
// chain :: Task a => (a -> Task b) -> Task b
chain (f) {
return this.map(f).join()
}
})
// these stay the same
Task.of = x => Task((reject, resolve) => resolve(x))
Task.rejected = x => Task((reject, resolve) => reject(x))
您可以在上面的示例中用这个新任务替换旧任务的定义,一切仍将照常工作^_^
使用 Promise 实现原生化
ES6 附带 Promises,其功能与我们实现的 Task 非常相似。当然有很多不同,但就本次演示而言,使用 Promise 代替 Task 将导致代码看起来与原始示例几乎相同
主要区别是:
- Task 期望您的
fork 函数参数按 (reject, resolve) 排序 - Promise 执行器函数参数按 (resolve, reject) 排序(相反顺序)
- 我们调用
promise.then 而不是task.chain
- Promise 会自动压缩嵌套的 Promise,因此您不必担心手动扁平化 Promise 的 Promise
-
Promise.rejected 和 Promise.resolve 不能被称为第一类 - 每个的上下文都需要绑定到 Promise - 例如 x => Promise.resolve(x) 或 Promise.resolve.bind(Promise) 而不是 Promise.resolve(Promise.reject 相同)
// Either
const Left = x => ({
map: f => Left(x),
fold: (f,_) => f(x)
})
const Right = x => ({
map: f => Right(f(x)),
fold: (_,f) => f(x),
})
// natural transformation
const eitherToPromise = e =>
e.fold(x => Promise.reject(x),
x => Promise.resolve(x))
// fake database
const data = {
"1": {id: 1, name: 'bob', bff: 2},
"2": {id: 2, name: 'alice', bff: 1}
}
// fake db api
const Db = {
find: id =>
new Promise((resolve, reject) =>
resolve((id in data) ? Right(data[id]) : Left('not found')))
}
// your program
const main = id =>
Db.find(id)
.then(eitherToPromise)
.then(user => Db.find(user.bff))
.then(eitherToPromise)
// bob's bff
main(1).then(console.log, console.error)
// alice's bff
main(2).then(console.log, console.error)
// unknown user's bff
main(3).then(console.log, console.error)