Monad变形金刚用Javascript解释?

Mar*_*oni 2 javascript monads functional-programming futuretask either

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

任何人都可以举一个例子来创建一个变换器来合并Javascript中的Future和Either monad以及如何使用它.

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

Tha*_*you 15

规则第一

首先,我们有自然转型法

  • 一些仿函数Fa,具有功能映射f,产率Fb话,自然转化,产生一些函子Gb.
  • 一些函子Fa,自然转化产生一些函子Ga,然后用一些函数映射f,产率Gb

选择任一路径(第一映射,变换第二,第一变换,映射第二)会导致相同的最终结果,Gb.

自然转化法

nt(x.map(f)) == nt(x).map(f)
Run Code Online (Sandbox Code Playgroud)

变得真实

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

首先我们将实现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),
})
Run Code Online (Sandbox Code Playgroud)

然后我们将实施 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))
Run Code Online (Sandbox Code Playgroud)

现在让我们开始定义一些理论程序.我们将拥有一个用户数据库,每个用户都拥有一个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')))
}
Run Code Online (Sandbox Code Playgroud)

好的,所以有一点点扭曲.我们的Db.find函数返回TaskEither(LeftRight).这主要是出于演示目的,但也可以说是一种好的做法.即,我们可能不认为用户未找到的场景是错误,因此我们不想要reject任务 - 相反,我们稍后通过解析 a Left来优雅地处理它'not found'.我们可能会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)))
Run Code Online (Sandbox Code Playgroud)

Yeck!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)
Run Code Online (Sandbox Code Playgroud)

让我们看看当我们chainDb.find结果进行转换时会发生什么

const main = id =>
  Db.find(id) // Task(Right(User))
    .chain(eitherToTask) // ???
    ...
Run Code Online (Sandbox Code Playgroud)

那是什么????好的,Task#chain期望你的函数返回a Task,然后它会搜索当前的Task,以及新返回的Task.所以在这种情况下,我们去:

// Db.find           // eitherToTask     // chain
Task(Right(User)) -> Task(Task(User)) -> Task(User)
Run Code Online (Sandbox Code Playgroud)

哇.这已经是一个巨大的进步,因为当我们进行计算时,它会使我们的数据更加平坦.我们继续吧 ...

const main = id =>
  Db.find(id) // Task(Right(User))
    .chain(eitherToTask) // Task(User)
    .chain(user => Db.find(user.bff)) // ???
    ...
Run Code Online (Sandbox Code Playgroud)

那么???这一步又是什么呢?我们知道Db.find回报Task(Right(User)但是我们chain正在进行,所以我们知道我们将至少两次挤压Task.这意味着我们去:

// Task of Db.find         // chain
Task(Task(Right(User))) -> Task(Right(User))
Run Code Online (Sandbox Code Playgroud)

看看那个,我们还有另一个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) !!!
Run Code Online (Sandbox Code Playgroud)

热土豆!好的,那我们如何处理呢?好吧main拿一个Int并返回一个Task(User),所以......

// main :: Int -> Task(User)
main(1).fork(console.error, console.log)
Run Code Online (Sandbox Code Playgroud)

这真的很简单.如果Db.find解析一个Right,它将被转换为Task.of(一个已解析的Task),这意味着结果将转到console.log- 否则,如果Db.find解析一个Left,它将被转换为一个Task.rejected(被拒绝的Task),这意味着结果将转到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)
Run Code Online (Sandbox Code Playgroud)


归因

我几乎完全答应了Brian Lonsdorf(@drboolean).他有一个关于Egghead的精彩系列,名为Frisby教授,介绍可组合功能JavaScript.很巧合的是,你的问题中的例子(转换Future和Either)与他的视频中使用的示例相同,并且在我的答案中使用此代码.

关于自然变换的两个是

  1. 具有自然变换的原则类型转换
  2. 在日常工作中应用自然变换

任务的替代实施

Task#chain 有一点神奇的事情并没有立即显现出来

task.chain(f) == task.map(f).join()
Run Code Online (Sandbox Code Playgroud)

我提到这是一个旁注,因为它对于考虑上面的Either到Task的自然转换并不是特别重要.Task#chain足以进行演示,但是如果你真的想把它拆开来看看一切是如何工作的,那么它可能会感觉有点无法接近.

下面,我推导chain使用mapjoin.我会在下面放一些应该有帮助的类型注释

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))
Run Code Online (Sandbox Code Playgroud)

您可以在上面的示例中使用这个新任务替换旧任务的定义,并且所有内容仍然可以使用相同的^ _ ^


跟着原住民 Promise

ES6附带Promises,其功能与我们实施的任务非常相似.当然有很多不同之处,但是就本演示而言,使用Promise而不是Task将导致代码几乎与原始示例相同

主要区别是:

  • 任务期望您的fork函数参数按顺序排序(reject, resolve)- Promise执行程序函数参数按(resolve, reject)顺序排序(反向顺序)
  • 我们打电话promise.then而不是task.chain
  • Promises会自动压缩嵌套的Promises,因此您不必担心手动扁平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)
Run Code Online (Sandbox Code Playgroud)