Javascript 中解释了 Monad 变换器?

Monad transformers explained in Javascript?

我很难理解 monad 转换器,部分原因是大多数示例和解释都使用 Haskell。

谁能举例说明如何创建一个转换器来合并 Javascript 中的 Future 和 Either monad 以及如何使用它。

如果您可以使用这些 monad 的 ramda-fantasy 实现,那就更好了。

规则优先

首先我们有自然变换法则

  • a 的一些函子 F,用函数 f 映射,产生 bF,然后自然转换,产生一些函子 Gb
  • a 的某些函子 F,自然转换产生 a 的某些函子 G,然后映射到某些函数 f,产生 Gb

选择任一路径(先映射,再变换,先变换,再映射)将导致相同的最终结果,G of b.

nt(x.map(f)) == nt(x).map(f)

变得真实

好的,现在我们来做一个实际的例子。我将逐位解释代码,然后在最后给出一个完整的可运行示例。

首先我们将实现 Either(使用 LeftRight

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 是 EitherLeftRight)的 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) 但我们正在 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)) // <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)与他的视频和我的回答中的这段代码中使用的示例相同这里。

关于自然变换的两个是

  1. Principled type conversions with natural transformations
  2. Applying natural transformations in everyday work

任务的替代实现

Task#chain 有一些魔法正在发生,但不是很明显

task.chain(f) == task.map(f).join()

我将此作为旁注提及,因为它对于考虑上述 Either 到 Task 的自然转换并不是特别重要。 Task#chain用来演示已经够用了,但如果真的要拆开看看一切如何,可能会觉得有点难以接近。

下面,我使用 mapjoin 导出 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.rejectedPromise.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)