Javascript 中解释了 Monad 变换器?
Monad transformers explained in Javascript?
我很难理解 monad 转换器,部分原因是大多数示例和解释都使用 Haskell。
谁能举例说明如何创建一个转换器来合并 Javascript 中的 Future 和 Either monad 以及如何使用它。
如果您可以使用这些 monad 的 ramda-fantasy
实现,那就更好了。
规则优先
首先我们有自然变换法则
a
的一些函子 F
,用函数 f
映射,产生 b
的 F
,然后自然转换,产生一些函子 G
共 b
。
a
的某些函子 F
,自然转换产生 a
的某些函子 G
,然后映射到某些函数 f
,产生 G
共 b
选择任一路径(先映射,再变换,或先变换,再映射)将导致相同的最终结果,G
of 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
函数,该函数 return 是在我们的数据库中查找用户的任务。这类似于 return 承诺的任何数据库库。
// 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
函数 return 是 Either
(Left
或 Right
)的 Task
。这主要是为了演示目的,但也可以说是一种很好的做法。即,我们可能不会将未找到用户的情况视为错误,因此我们不想 reject
任务 - 相反,我们稍后会通过 解决 优雅地处理它Left
个 'not found'
。我们可能会在出现不同的错误时使用 reject
,例如连接数据库失败或其他错误。
制定目标
我们程序的目标是获取给定的用户 ID,并查找该用户的好友。
我们雄心勃勃,但天真,所以我们首先尝试这样的事情
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)))
哎呀! a 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(<b>eitherToTask</b>) // <b>???</b>
...
那么???
是什么?好吧 Task#chain
期望您的函数 return 一个 Task
然后它将当前任务和新 returned 任务压缩在一起。所以在这种情况下,我们去:
// Db.find // eitherToTask // chain
Task(Right(User)) -> Task(Task(User)) -> Task(User)
哇。这已经是一个巨大的改进,因为它在我们进行计算时使我们的数据更加平坦。让我们继续前进...
const main = id =>
Db.find(id) // Task(Right(User))
.chain(eitherToTask) // <b>Task(User)</b>
<b>.chain(user => Db.find(user.bff))</b> // ???
...
那么这一步的???
是什么?我们知道 Db.find
returns Task(Right(User)
但我们正在 chain
ing,所以我们知道我们至少会将两个 Task
s 压在一起。这意味着我们去:
// 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)) // <b>Task(Right(User))</b>
<b>.chain(eitherToTask)</b> // Task(User) !!!
烫手山芋!好的,那么我们将如何处理它呢?那么 main
需要一个 Int
而 return 需要一个 Task(User)
,所以 ...
// main :: Int -> Task(User)
main(1).fork(console.error, console.log)
真的就这么简单。如果 Db.find
解决了一个 Right,它将被转换为一个 Task.of
(一个解决的任务),这意味着结果将转到 console.log
– 否则,如果 Db.find
解决了一个 Left ,它将被转换为 Task.rejected
(被拒绝的任务),这意味着结果将转到 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). He has a fantastic series on Egghead called 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,它的功能与我们实现的任务非常相似。当然有很多不同之处,但就本次演示而言,使用 Promise 而不是 Task 将导致代码看起来与原始示例几乎相同
主要区别是:
- Task 期望您的
fork
函数参数按 (reject, resolve)
排序 - Promise 执行程序函数参数按 (resolve, reject)
(倒序) 排序
- 我们调用
promise.then
而不是task.chain
- Promise 会自动压缩嵌套的 Promise,因此您不必担心手动扁平化 Promise 的 Promise
Promise.rejected
和 Promise.resolve
不能先调用 class – 每个的上下文需要绑定到 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)
我很难理解 monad 转换器,部分原因是大多数示例和解释都使用 Haskell。
谁能举例说明如何创建一个转换器来合并 Javascript 中的 Future 和 Either monad 以及如何使用它。
如果您可以使用这些 monad 的 ramda-fantasy
实现,那就更好了。
规则优先
首先我们有自然变换法则
a
的一些函子F
,用函数f
映射,产生b
的F
,然后自然转换,产生一些函子G
共b
。a
的某些函子F
,自然转换产生a
的某些函子G
,然后映射到某些函数f
,产生G
共b
选择任一路径(先映射,再变换,或先变换,再映射)将导致相同的最终结果,G
of 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
函数,该函数 return 是在我们的数据库中查找用户的任务。这类似于 return 承诺的任何数据库库。
// 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
函数 return 是 Either
(Left
或 Right
)的 Task
。这主要是为了演示目的,但也可以说是一种很好的做法。即,我们可能不会将未找到用户的情况视为错误,因此我们不想 reject
任务 - 相反,我们稍后会通过 解决 优雅地处理它Left
个 'not found'
。我们可能会在出现不同的错误时使用 reject
,例如连接数据库失败或其他错误。
制定目标
我们程序的目标是获取给定的用户 ID,并查找该用户的好友。
我们雄心勃勃,但天真,所以我们首先尝试这样的事情
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)))
哎呀! a 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(<b>eitherToTask</b>) // <b>???</b>
...
那么???
是什么?好吧 Task#chain
期望您的函数 return 一个 Task
然后它将当前任务和新 returned 任务压缩在一起。所以在这种情况下,我们去:
// Db.find // eitherToTask // chain
Task(Right(User)) -> Task(Task(User)) -> Task(User)
哇。这已经是一个巨大的改进,因为它在我们进行计算时使我们的数据更加平坦。让我们继续前进...
const main = id =>
Db.find(id) // Task(Right(User))
.chain(eitherToTask) // <b>Task(User)</b>
<b>.chain(user => Db.find(user.bff))</b> // ???
...
那么这一步的???
是什么?我们知道 Db.find
returns Task(Right(User)
但我们正在 chain
ing,所以我们知道我们至少会将两个 Task
s 压在一起。这意味着我们去:
// 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)) // <b>Task(Right(User))</b>
<b>.chain(eitherToTask)</b> // Task(User) !!!
烫手山芋!好的,那么我们将如何处理它呢?那么 main
需要一个 Int
而 return 需要一个 Task(User)
,所以 ...
// main :: Int -> Task(User)
main(1).fork(console.error, console.log)
真的就这么简单。如果 Db.find
解决了一个 Right,它将被转换为一个 Task.of
(一个解决的任务),这意味着结果将转到 console.log
– 否则,如果 Db.find
解决了一个 Left ,它将被转换为 Task.rejected
(被拒绝的任务),这意味着结果将转到 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). He has a fantastic series on Egghead called 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,它的功能与我们实现的任务非常相似。当然有很多不同之处,但就本次演示而言,使用 Promise 而不是 Task 将导致代码看起来与原始示例几乎相同
主要区别是:
- Task 期望您的
fork
函数参数按(reject, resolve)
排序 - Promise 执行程序函数参数按(resolve, reject)
(倒序) 排序
- 我们调用
promise.then
而不是task.chain
- Promise 会自动压缩嵌套的 Promise,因此您不必担心手动扁平化 Promise 的 Promise
Promise.rejected
和Promise.resolve
不能先调用 class – 每个的上下文需要绑定到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)