【问题标题】:Why and how the Continuation Monad solves the callback hell? In other words : Is RX or FRP = Continuation Monad ? IF not =, what is the difference?Continuation Monad 为什么以及如何解决回调地狱?换句话说:RX 还是 FRP = Continuation Monad?如果不是=,有什么区别?
【发布时间】:2015-12-23 19:36:05
【问题描述】:

Herehere据说Continuation Monad解决了回调地狱。

RX 和 FRP 也解决了回调地狱。

如果所有这三个工具都解决了回调地狱,那么就会出现以下问题:

在 Erik 的视频中,据说 RX=Continuation Monad。这是真的吗?如果是,你能显示映射吗?

如果 RX 不是 = 继续。 Monad 那么 RX 和 Continuation Monad 有什么区别呢?

同样,FRP 和 Continuation Monad 有什么区别?

也就是说,假设读者知道 FRP 或 RX 是什么,那么读者如何容易理解 Continuation Monad 是什么?

通过将 Continuation Monad 与 RX 或 FRP 进行比较,是否可能/容易理解它是什么?

【问题讨论】:

  • 首先什么是callback-hell?第二:您是在谈论 FRP 还是像 RX 这样的框架(那些不是真正的 FRP 实现)- cont-monad 的某些部分(例如类型)看起来像一个事件(您提供事件处理程序)但例如没有取消订阅(您通常在 RX 之类的框架上找到)并且通常您不会添加多个延续;)...所以 IMO 它不是“相同的”...但是好吧,这是我的意见,也许这不是讨论这样的地方(?)
  • 这里有一张关于回调地狱的好图seajones.co.uk/content/images/2014/12/callback-hell.png
  • 在 Erik 的视频中,据说 RX 是 Cont. Monad,最好知道“用简单的术语”/“向凡人解释”为什么以及如何是真的。

标签: haskell system.reactive monads continuations frp


【解决方案1】:

我不熟悉 RX,但关于 FRP 和 continuation monad,它们是根本不同的概念。

Functional reactive programming 是处理与时间相关的值和事件的问题的解决方案。使用事件,您可以通过对计算进行排序来解决回调问题,即当一个事件完成时,发送一个事件并触发下一个事件。但是对于回调,你真的不关心时间,你只想以特定的方式对计算进行排序,所以除非你的整个程序基于 FRP,否则它不是最佳解决方案。

continuation monad 与时间完全无关。这是一个抽象的计算概念,可以(粗略地说)获取当前评估序列的“快照”,并使用它们任意“跳转”到这些快照。这允许创建复杂的控制结构,例如循环和coroutines。现在看看延续单子的定义:

newtype Cont r a = Cont { runCont :: (a -> r) -> r}

这本质上是一个带有回调的函数!它是一个接受延续(回调)的函数,它将在产生a 之后继续计算。因此,如果您有多个需要延续的函数,

f1 :: (Int -> r) -> r

f2 :: Int -> (Char -> c) -> c

f3 :: Char -> (String -> d) -> d

他们的组成变得有些混乱:

comp :: String
comp = f1 (\a -> f2 a (\b -> f3 b id))

使用延续,它变得非常简单,我们只需要将它们包装在cont 中,然后使用单子绑定操作>>= 对它们进行排序:

import Control.Monad.Cont

comp' :: String
comp' = runCont (cont f1 >>= cont . f2 >>= cont . f3) id

所以延续是回调问题的直接解决方案。

【讨论】:

  • 非常有趣。所以基本上 - 松散地说 - 延续单子类似于 RX。
  • FRP 适合建模状态机,不知道 Continuation Monad 是否也适合?
  • @jhegedus 重要的是要注意延续单子与异步计算无关,不涉及线程。可以在ContT r IO 中显式包含线程,或者使用Haskell 的并行性,但这与Cont/ContT 是分开的。关于 FRP,我认为除非状态机以某种方式涉及时间,否则 FRP 不是适合它们的工具,尽管我不是该领域的专家。我建议研究一下Automaton
  • 我们在 FRP 上举行了几次聚会,这就是我知道 FRP 非常适合建模状态机 meetup.com/Helsinki-Functional-Reactive-Programming-Meetup 的原因,这甚至在在线的讲座视频中提到,由其中一位提供Haskell FRP 作者,在我们的计算机上,时间在实践中总是离散的,所以在实践中时间演化只是描述状态机的另一种方式。在实践中,如果你想求解一个微分方程,你可以以某种方式离散它,或者你找到一个解析解。后者一般来说并不那么容易:)
  • @jhegedus 很好,我不知道。学习一些东西总是好的:)。您是否有更多关于 FRP 和状态机主题的链接(最好是文本形式)?
【解决方案2】:

如果您查看 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 中可用的 setTimeoutreadFile 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 }))

【讨论】:

    猜你喜欢
    • 2013-10-03
    • 2011-09-29
    • 2015-03-01
    • 2020-03-26
    • 1970-01-01
    • 2013-07-21
    • 2017-12-28
    • 2018-01-24
    • 2011-07-01
    相关资源
    最近更新 更多