【发布时间】:2018-01-02 06:00:31
【问题描述】:
在 Haskell 中创建 monad 时,何时使用 continuation-passing style vs codensity vs reflection without remorse 有什么经验法则吗?
作为一个例子,我将使用一个简单的协程 monad。如果您以前从未见过这个,您可能想查看Monad.Reader Issue 19 或pipes 库中的“协程管道”文章。以下示例的完整代码可以在this repository 中找到。
-
正常
这只是一个定义为数据类型的普通 monad:
data FooM i o a = Await (i -> FooM i o a) | Yield o (FooM i o a) | Done a这种风格在 Haskell 生态系统中被广泛使用。这种风格的一个例子是来自
pipes的Proxy数据类型。 -
持续传递风格 (CPS)
这类似于普通风格,但每个数据构造函数都变成了一个延续的参数:
newtype FooCPS i o a = FooCPS { runFooCPS :: forall r. ((i -> FooCPS i o a) -> r) -> (o -> FooCPS i o a -> r) -> (a -> r) -> r }attoparsec 和parsec 都使用这种样式。
-
共密度
这种风格使用 codensity monad transformer 包裹在 normal 风格中定义的 monad。这给出了 O(1) 左关联绑定。
codensity monad 转换器如下所示:
newtype Codensity m a = Codensity { runCodensity :: forall b. (a -> m b) -> m b }我们的实际 monad 可以使用
Codensity转换器定义为新类型。请注意FooCodensity如何在内部使用FooM。newtype FooCodensity i o a = FooCodensity { runFooCodensity :: Codensity (FooM i o) a } -
无悔的反思
这是Reflection without Remorse论文中讨论的风格。
这类似于 normal 样式,但递归调用已成为具有 O(1) append 和 amortized O(1) uncons 的数据结构。这为
FooRWRmonad 提供了 O(1) 左关联绑定和 monadic-reflection:data FooRWR i o a = AwaitRWR (forall x. i -> FooExplicit i o x a) | YieldRWR o (forall x. FooExplicit i o x a) | DoneRWR aFooExplicit类型定义如下:type FooExplicit i o = FTCQueue (FooRWR i o)FTCQueue是一个数据结构,具有 O(1) 附加和摊销 O(1) uncons。freer-effects 和 extensible 包使用此样式。它在monad-skeleton 中作为独立库提供。
什么时候应该使用normal vs CPS vs codensity vs reflection without remorse?我想一个硬而快速的答案需要对给定的 monad 和应用程序进行基准测试,但是是否有任何经验法则?
根据我自己的研究,我遇到了以下想法/cmets:
-
CPS 可能比 正常 样式更快,因为您可能不需要进行案例分析。尽管实际加速可能会根据 GHC 编译代码的方式而有所不同。 Codensity 和 reflection without remorse 有一些开销。
Gabriel Gonzalez(
pipes的作者)在 Github 上的 this reddit thread 和 issue 上写了他如何坚持pipes的 normal 风格。Bryan O'Sullivan(
attoparsec的作者)写道,将attoparsec从 normal 样式更改为 CPS 会产生 factor of 8 speedup。该帖子中的一些 cmets 还谈到了 normal 风格与 CPS。 -
如果您需要深度左关联绑定,normal 样式和 CPS 以二次运行时结束。
这是“无悔的反思”论文中的一个例子,它展示了二次运行时间。
data It i a = Get (i -> It i a) | Done a sumInput :: Int -> It Int Int sumInput n = Get (foldl (>=>) return (replicate (n - 1) f)) where f x = get >>= return . (+ x)如果
sumInput用codensity 或reflection without remorse 重写,它将运行得非常快。如果您的应用程序具有深度左关联绑定,您可能应该使用 codensity 或 reflection without remorse。
Michael Snoyman(
conduit的作者)在一篇关于 speeding upconduit的博文中谈到了这一点。pipes库 used to provide 一个共密度转换器。 -
CPS 和 codensity 不支持 O(1) 反射。
这是一个需要一元反射的函数。这个例子改编自“Reflection without Remorse”论文:
data It i a = Get (i -> It i a) | Done a par :: It i a -> It i b -> It i (It i a, It i b) par l r | Done <- l = Done (l, r) | Done <- r = Done (l, r) | Get f <- l, Get g <- r = Get Done >>= \x -> par (f x) (g x)如果不先转换回普通样式,则无法以CPS或codensity样式编写此方法。 无悔的反思风格就没有这个问题。
如果您需要一元反射,您可能应该使用正常样式或无悔的反射。
Reflection without remorse 增加了一些开销,但它是唯一同时提供 O(1) 左关联绑定和反射的样式。
额外问题:可以从free 包。什么时候应该使用Free?什么时候应该使用F?
【问题讨论】:
-
我把这个问题发到了reddit的/r/haskell:reddit.com/r/haskell/comments/6qn4y0/…
-
需要注意的是,在 CPS 中,您通常将所有出现的
FooCPS i o a替换为r(取决于您想要的性能)。
标签: haskell reflection monads continuations free-monad