我很难理解单子转换器,部分原因是大多数例子和解释都使用Haskell。
有人可以给出一个在JavaScript中创建将Future和Either单子合并的转换器的示例以及如何使用它的例子吗?
如果您可以使用ramda-fantasy
实现这些单子,那就更好了。
我很难理解单子转换器,部分原因是大多数例子和解释都使用Haskell。
有人可以给出一个在JavaScript中创建将Future和Either单子合并的转换器的示例以及如何使用它的例子吗?
如果您可以使用ramda-fantasy
实现这些单子,那就更好了。
首先是规则
首先我们有自然变换定律(Natural Transformation Law)
a
映射到 b
的函子 F
,使用函数 f
进行映射后,得到 F
的 b
类型的函子,然后进行自然变换,得到一些从 b
映射到 G
的函子。a
映射到 b
的函子 F
,进行自然变换后得到一些从 a
映射到 G
的函子,然后使用某些函数 f
进行映射,得到 G
的 b
类型的函子。选择任何一条路径(先映射再变换,或者先变换再映射)都会导致相同的最终结果,即 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))
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
)。这主要是为了演示目的,但也可以被认为是一个好的实践。例如,我们可能不认为用户未找到的情况是一个错误,因此我们不想 reject
任务 - 相反,我们通过解决 'not found'
的 Left
来优雅地处理它。在遇到其他错误,比如无法连接到数据库等情况下,我们可能会使用 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)
让我们观察当我们将此转换链接到我们的Db.find
结果时会发生什么
const main = id =>
Db.find(id) // Task(Right(User))
.chain(<b>eitherToTask</b>) // <b>???</b>
...
那么什么是???
?好的,Task#chain
期望你的函数返回一个Task
,然后将当前的Task
和新返回的Task
压缩在一起。所以在这种情况下,我们会这样做:
// 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
返回Task(Right(User)
,但是因为我们正在chain
,所以我们知道至少会将两个Task
组合在一起。这意味着我们会执行以下操作:// 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
并返回一个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)的回答。他在Egghead上有一系列名为Professor Frisby Introduces Composable Functional JavaScript的精彩视频。巧合的是,你问题中的示例(转换Future和Either)与他的视频以及我在此处的代码中使用的相同示例。
关于自然变换的两个内容如下:
任务的替代实现
Task#chain
中有一些不太明显的魔法。
task.chain(f) == task.map(f).join()
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提供了Promise,可以非常类似于我们实现的任务。当然,二者之间有很多差异,但是为了演示的目的,使用Promise而不是Task将导致代码几乎与原始示例完全相同。
主要区别如下:
fork
函数参数按(reject, resolve)
排序 - Promise执行器函数参数按(resolve, reject)
排序(顺序相反)promise.then
而不是task.chain
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)
Task
中删除reject
,然后将失败的可能性表示为Task<Either<A>>
。那将是一个真正的单子变换器! - BergiEither
monad transformer,并将其应用于 Task
。这样就会得到一个Monad,你可以直接将其chain
应用于find(id)
的结果。 - BergiEitherT<E, Task><A>
代替Task<Either<E,A>>
。 - Bergi