【问题标题】:Monad transformers explained in Javascript?用 Javascript 解释的 Monad 转换器?
【发布时间】:2017-08-04 15:22:31
【问题描述】:

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

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

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

【问题讨论】:

  • 合并的最终结果是什么? Afaict monad 转换器是接受 monad 并返回满足一些规则的 monad 的函数:en.wikipedia.org/wiki/Monad_transformer#Definition
  • 我正在使用 Either monad 来处理 Future monad 处理异步流中的验证。在另一个中处理一个 monad 不是很干净,链接 Future monad 变得特别棘手。我读过 monad 转换器可以给我一个由这两个 monad 组成的更干净的 API。那正确吗?如果是这样,在 Javascript 中看起来如何?
  • @AlexPánek 这篇维基百科文章是一个解释示例,对于不了解 Haskell 的 JavaScript 开发人员来说几乎无法理解。我正在寻找使用纯 JavaScript 代码的解释。
  • 您能概括一下到目前为止您是如何尝试实现这一点的吗?
  • 这个问题太笼统了。无论如何,AFAIK,monad 转换器确实促进了 monad 的组合。例如,它们可以帮助您避免深度嵌套的链式调用。 Fantasy Land 和这个 SO question 上提供了 Javascript 实现。请注意,并非每个 monad 组合都会产生一个 monad,即您可能会丢失一些 monad 法则。

标签: javascript functional-programming monads either futuretask


【解决方案1】:

规则优先

首先我们有自然变换定律

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

选择任一路径(首先映射,然后变换,先变换,然后映射)将导致相同的最终结果,Gb

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 函数,它返回一个在我们的数据库中查找用户的任务。这类似于任何返回 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 函数返回TaskEitherLeftRight)。这主要是出于演示目的,但也可以说是一种良好的做法。即,我们可能不会认为 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)与他的视频中使用的示例以及我在此处的答案中的此代码中使用的示例相同。

关于自然变换的两个是

  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,其功能与我们实现的 Task 非常相似。当然有很多不同,但就本次演示而言,使用 Promise 代替 Task 将导致代码看起来与原始示例几乎相同

主要区别是:

  • Task 期望您的 fork 函数参数按 (reject, resolve) 排序 - Promise 执行器函数参数按 (resolve, reject) 排序(相反顺序)
  • 我们调用promise.then 而不是task.chain
  • Promise 会自动压缩嵌套的 Promise,因此您不必担心手动扁平化 Promise 的 Promise
  • Promise.rejectedPromise.resolve 不能被称为第一类 - 每个的上下文都需要绑定到 Promise - 例如 x => Promise.resolve(x)Promise.resolve.bind(Promise) 而不是 Promise.resolvePromise.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>>。那将是一个真正的 monad 转换器!
  • @Bergi 这是一个有趣的想法。我想知道视频系列是否按原样使用了 Task,以免让学习者不知所措。我仍然对你的提议如何运作有一些疑问。你认为你可以提供一个编辑来澄清吗?
  • @Bergi,特别是如果失败表示为Task(Left(someErr)),我们甚至还有转换的理由吗?还是我们需要一个完全独立的示例程序?
  • 不是自然转换,不,但您可以明确写出Either monad transformer 并将其应用于Task。然后出现了一个 monad,它的 chain 可以直接应用于 find(id) 结果
  • 所以如果你明白我的意思,我们将使用EitherT<E, Task><A> 而不是Task<Either<E,A>>
猜你喜欢
  • 2015-05-23
  • 1970-01-01
  • 2018-07-08
  • 1970-01-01
  • 1970-01-01
  • 2021-11-27
  • 2013-05-01
  • 1970-01-01
  • 2018-02-06
相关资源
最近更新 更多