如果您查看 Haskell 中如何定义 continuation monad,它看起来像这样:
data Cont r a = Cont { runCont :: (a -> r) -> r }
就其本身而言,这是完全纯粹的,并不代表真实世界的效果或时间。即使在其当前形式中,它也可以用于表示时间/IO 效果,只需选择r 作为涉及IO 的类型。然而,出于我们的目的,我们将做一些稍微不同的事情。我们将用类型参数替换具体的->:
data Cont p r a = Cont { runCont :: p (p a r) r }
这个有什么用?在 Haskell 中,我们只有将某些输入域映射到输出域的纯函数。在其他语言中,我们可以有这样的函数,但我们可以另外定义不纯的“函数”,它(除了为给定的输入产生一些任意输出之外)可能隐含地执行副作用。 p>
以下是 JS 中两者的示例:
// :: Int -> Int -> Int
const add = x => y => x + y
// :: String -!-> ()
const log = msg => { console.log(msg); }
请注意,log 不是一个纯函数,它会产生一个表示效果的值,这就是用 Haskell 等纯语言对此类事物进行编码的方式。相反,该效果仅与log 的调用相关联。为了捕捉到这一点,当我们谈论纯函数和不纯“函数”(分别为-> 和-!->)时,我们可以使用不同的箭头。
所以,回到你关于 continuation monad 如何解决回调地狱的问题,事实证明(至少在 JavaScript 中),大多数引起回调地狱的 API 都可以很容易地转换为 @ 形式的值987654331@,以后我将其称为Cont! a。
一旦你有了一个单子 API,剩下的就很简单了;您可以遍历充满延续的结构进入结构的延续,使用 do 表示法编写类似于 async/await 的多步计算,使用 monad 转换器为延续配备附加行为(例如错误处理)等。
monad 实例看起来与 Haskell 中的非常相似:
// :: type Cont p r a = p (p a r) r
// :: type Cont! = Cont (-!->) ()
// :: type Monad m = { pure: x -> m x, bind: (a -> m b) -> m a -> m b }
// :: Monad Cont!
const Cont = (() => {
// :: x -> Cont! x
const pure = x => cb => cb(x)
// :: (a -> Cont! b) -> Cont! a -> Cont! b
const bind = amb => ma => cb => ma(a => amb(a)(cb))
return { pure, bind }
})()
以下是几个将 Node JS 中可用的 setTimeout 和 readFile API 建模为不纯延续的示例:
// :: FilePath -> Cont! (Either ReadFileError Buffer)
const readFile = path => cb => fs.readFile(path, (e, b) => cb(e ? Left(e) : Right(b)))
// :: Int -> v -> Cont! v
const setTimeout = delay => v => cb => setTimeout(() => cb(v), delay)
作为一个人为的例子,这是我们使用标准 API 读取文件时进入的“回调地狱”,等待五秒钟,然后读取另一个文件:
fs.readFile("foo.txt", (e, b1) => {
if (e) { throw e }
setTimeout(() => {
fs.readFile("bar.txt", (e, b2) => {
if (e) { throw e }
console.log(b1.toString("utf8") + b2.toString("utf8"))
})
}, 5000)
})
这是使用延续单子的等效程序:
const ECont = EitherT(Cont)
const decode = buf => buf.toString("utf8")
const foo = readFile("foo.txt") |> ECont.map(decode)
const bar = readFile("bar.txt") |> ECont.map(decode)
// Imaginary do notation for JS for purposes of clarity
const result = do(ECont)([
[s1, foo],
ECont.lift(delay_(5000)),
[s2, bar],
ECont.pure(s1 + s2)
])
const panic = e => { throw e }
const log = v => { console.log(v) }
// No side effects actually happen until the next line is invoked
result(Either.match({ Left: panic, Right: log }))