虽然早些时候出现了问题,但我创建了以下代码:
newtype Callback a = Callback { unCallback :: a -> IO (Callback a) }
liftCallback :: (a -> IO ()) -> Callback a
liftCallback f = let cb = Callback $ \x -> (f x >> return cb) in cb
runCallback :: Callback a -> IO (a -> IO ())
runCallback cb =
do ref <- newIORef cb
return $ \x -> readIORef ref >>= ($ x) . unCallback >>= writeIORef ref
Callback a
表示处理某些数据并返回应该用于下一个通知的新回调的函数。一个回调基本上可以取代自己,可以这么说。 liftCallback
只是将普通函数提升到我的类型,而runCallback
使用IORef
将Callback
转换为简单函数。
该类型的一般结构是:
data T m a = T (a -> m (T m a))
看起来很像这可能与类别理论中的一些众所周知的数学结构同构。
但它是什么?它是monad还是什么?一个应用函子?一个变形的单子?箭头,甚至?是否有类似Hoogle的搜索引擎可以让我搜索这样的一般模式?
答案 0 :(得分:14)
您正在寻找的术语是free monad transformer。了解这些工作原理的最佳位置是阅读issue 19 of The Monad Reader中的“Coroutine Pipelines”文章。 Mario Blazevic对这种类型的工作原理进行了非常清晰的描述,除了他称之为“Coroutine”类型。
我在transformers-free
包中写了他的类型然后它被合并到free
包中,这是它的新官方住所。
您的Callback
类型与:
type Callback a = forall r . FreeT ((->) a) IO r
要了解免费的monad变换器,首先需要understand free monads,它们只是抽象语法树。您为免费monad提供了一个函数,它在语法树中定义了一个步骤,然后从Monad
创建一个Functor
,它基本上是这些类型的步骤的列表。所以如果你有:
Free ((->) a) r
这将是一个语法树,它接受零个或多个a
作为输入,然后返回一个值r
。
但是,通常我们希望嵌入效果或使语法树的下一步依赖于某些效果。为此,我们简单地将我们的免费monad推广到一个免费的monad变换器,它在语法树步骤之间交换基本monad。对于Callback
类型,您在每个输入步骤之间插入IO
,因此您的基本monad为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
的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
,f
自动成为monad变换器,所以它也为我们处理了这个。
我们的打印机是合适的Callback
,因此我们可以通过解构免费的monad变换器来提供它的值:
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)
当我们绑定runFreeT callback
时会发生实际打印,然后我们会在语法树中提供下一步,我们将为列表的下一个元素提供信息。
我们试一试:
>>> feed [1..5] printer
1
2
3
4
5
但是,您甚至不需要自己编写所有这些内容。正如彼得指出的那样,我的pipes
库为你提取了这样的常见流媒体模式。你的回调只是:
forall r . Consumer a IO r
我们使用printer
定义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
的更多信息,建议您read the tutorial。
答案 1 :(得分:8)
该类型的一般结构在我看来像
data T (~>) a = T (a ~> T (~>) a)
你的术语中的(~>) = Kleisli m
(箭头)。
Callback
本身看起来不像我能想到的任何标准Haskell类型类的实例,但它是一个Contravariant Functor(也称为Cofunctor,因为它的结果是错误的)。因为它没有包含在GHC附带的任何库中,所以在Hackage(use this one)上有几个定义,但它们看起来都是这样的:
class Contravariant f where
contramap :: (b -> a) -> f a -> f b
-- c.f. fmap :: (a -> b) -> f a -> f b
然后
instance Contravariant Callback where
contramap f (Callback k) = Callback ((fmap . liftM . contramap) f (f . k))
Callback
拥有的类别理论是否存在一些更奇特的结构?我不知道。
答案 2 :(得分:6)
我认为这种类型非常接近我所听到的称为“电路”的电路,这是一种箭头。暂时忽略IO部分(正如我们可以通过转换Kliesli箭头那样)电路变压器是:
newtype CircuitT a b c = CircuitT { unCircuitT :: a b (c, CircuitT a b c) }
这是一个箭头,它返回一个新箭头,每次用于下一个输入。只要基本箭头支持它们,就可以为此箭头变换器实现所有常见的箭头类(包括循环)。现在,我们所要做的就是让它在理论上与你提到的类型相同,就是摆脱额外的输出。这很容易完成,因此我们发现:
Callback a ~=~ CircuitT (Kleisli IO) a ()
好像我们看右边:
CircuitT (Kleisli IO) a () ~=~
(Kliesli IO) a ((), CircuitT (Kleisli IO) a ()) ~=~
a -> IO ((), CircuitT (Kliesli IO) a ())
从这里开始,您可以看到它与Callback a的相似之处,除了我们还输出一个单位值。由于单位价值与其他东西在元组中,这实际上并没有告诉我们多少,所以我会说它们基本相同。
N.B。出于某种原因,我使用〜=〜表示类似但不完全相同。它们非常相似,特别注意我们可以将Callback a
转换为CircuitT (Kleisli IO) a ()
,反之亦然。
编辑:我也完全赞同这样的想法:A)monadic costream(monadic操作特别指出无数个值,我认为这意味着)和B)一个仅消耗管道(在很多方面)非常类似于没有输出的电路类型,或者输出设置为(),因为这样的管道也可能有输出)。
答案 3 :(得分:3)
只是一个观察,你的类型似乎与出现在Consumer p a m
库中的pipes非常相关(也可能是其他类似的图书馆):
type Consumer p a = p () a () C
-- A Pipe that consumes values
-- Consumers never respond.
其中C
是空数据类型,p
是Proxy
类型类的实例。它使用类型a
的值并且从不生成任何值(因为它的输出类型为空)。
例如,我们可以将Callback
转换为Consumer
:
import Control.Proxy
import Control.Proxy.Synonym
newtype Callback m a = Callback { unCallback :: a -> m (Callback m a) }
-- No values produced, hence the polymorphic return type `r`.
-- We could replace `r` with `C` as well.
consumer :: (Proxy p, Monad m) => Callback m a -> () -> Consumer p a m r
consumer c () = runIdentityP (run c)
where
run (Callback c) = request () >>= lift . c >>= run
请参阅the tutorial。
(这应该是一个评论,但它有点太长了。)