您正在寻找的术语是免费的单子变压器。了解这些工作原理的最佳地点是阅读The Monad Reader 第 19期中的“协程管道”文章。Mario Blazevic 非常清楚地描述了这种类型的工作原理,除了他称之为“协程”类型。
我在包里写了他的类型,transformers-free
然后它被合并到free
包中,这是它的新官方主页。
您的Callback
类型同构于:
type Callback a = forall r . FreeT ((->) a) IO r
要了解免费的 monad 转换器,您需要首先了解免费的 monad,它们只是抽象的语法树。你给自由的 monad 一个函子,它在语法树中定义了一个步骤,然后它Monad
从中创建一个Functor
基本上是这些类型步骤的列表的函数。所以如果你有:
Free ((->) a) r
那将是一个语法树,它接受零个或多个a
s 作为输入,然后返回一个值r
。
但是,通常我们希望嵌入效果或使语法树的下一步依赖于某些效果。为此,我们只需将我们的自由 monad 提升为一个自由 monad 转换器,它在语法树步骤之间交错基本 monad。对于您的类型,您在每个输入步骤之间Callback
交错,因此您的基本单子是:IO
IO
FreeT ((->) a) IO r
自由 monad 的好处是它们自动成为任何函子的 monad,因此我们可以利用这一点来使用do
符号来组装我们的语法树。例如,我可以定义一个await
在 monad 中绑定输入的命令:
import Control.Monad.Trans.Free
await :: (Monad m) => FreeT ((->) a) m a
await = liftF id
现在我有一个用于编写Callback
s 的 DSL:
import Control.Monad
import Control.Monad.Trans.Free
printer :: (Show a) => FreeT ((->) a) IO r
printer = forever $ do
a <- await
lift $ print a
请注意,我不必定义必要的Monad
实例。对于任何仿函数,两者FreeT f
和Free f
都是自动的,在这种情况下是我们的仿函数,所以它会自动做正确的事情。这就是范畴论的魔力!Monad
f
((->) a)
此外,我们不必MonadTrans
为了使用lift
. FreeT f
给定任何 functor ,它会自动成为一个 monad 转换器,f
所以它也为我们解决了这个问题。
我们的打印机是一个合适的Callback
,所以我们可以通过解构免费的单子变压器来提供它的值:
feed :: [a] -> FreeT ((->) a) IO r -> IO ()
feed as callback = do
x <- runFreeT callback
case x of
Pure _ -> return ()
Free k -> case as of
[] -> return ()
b:bs -> feed bs (k b)
实际打印发生在我们 bind 时runFreeT callback
,这给了我们语法树的下一步,我们提供列表的下一个元素。
让我们尝试一下:
>>> feed [1..5] printer
1
2
3
4
5
但是,您甚至不需要自己编写所有这些内容。正如 Petr 所指出的,我的pipes
库为您抽象了像这样的常见流模式。您的回调只是:
forall r . Consumer a IO r
我们定义printer
using的方式pipes
是:
printer = forever $ do
a <- await
lift $ print a
...我们可以为它提供一个值列表,如下所示:
>>> runEffect $ each [1..5] >-> printer
1
2
3
4
5
我设计pipes
包含非常大范围的像这样的流抽象,这样你总是可以使用do
符号来构建每个流组件。 pipes
还为状态和错误处理以及双向信息流等问题提供了多种优雅的解决方案,因此如果您Callback
根据pipes
.
如果你想了解更多pipes
,我建议你阅读教程。