16

在之前破解某些东西时,我创建了以下代码:

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使用IORefa 将 a 转换Callback为简单函数。

该类型的一般结构是:

data T m a = T (a -> m (T m a))

看起来这可能与范畴论中一些著名的数学结构同构。

但它是什么?它是一个单子还是什么?一个应用函子?一个转化的单子?甚至是箭?是否有类似 Hoogle 的搜索引擎可以让我搜索这样的一般模式?

4

4 回答 4

14

您正在寻找的术语是免费的单子变压器。了解这些工作原理的最佳地点是阅读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

那将是一个语法树,它接受零个或多个as 作为输入,然后返回一个值r

但是,通常我们希望嵌入效果或使语法树的下一步依赖于某些效果。为此,我们只需将我们的自由 monad 提升为一个自由 monad 转换器,它在语法树步骤之间交错基本 monad。对于您的类型,您在每个输入步骤之间Callback交错,因此您的基本单子是:IOIO

FreeT ((->) a) IO r

自由 monad 的好处是它们自动成为任何函子的 monad,因此我们可以利用这一点来使用do符号来组装我们的语法树。例如,我可以定义一个await在 monad 中绑定输入的命令:

import Control.Monad.Trans.Free

await :: (Monad m) => FreeT ((->) a) m a
await = liftF id

现在我有一个用于编写Callbacks 的 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 fFree f都是自动的,在这种情况下是我们的仿函数,所以它会自动做正确的事情。这就是范畴论的魔力!Monadf((->) 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

我们定义printerusing的方式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,我建议你阅读教程

于 2013-02-06T00:09:53.283 回答
8

类型的一般结构在我看来像

data T (~>) a = T (a ~> T (~>) a)

用你的(~>) = Kleisli m话来说(箭头)。


Callback它本身看起来不像我能想到的任何标准 Haskell 类型类的实例,但它是一个逆变函子(也称为 Cofunctor,事实证明它具有误导性)。由于它不包含在 GHC 附带的任何库中,因此在 Hackage 上存在多个它的定义(使用这个),但它们看起来都像这样:

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?我不知道。

于 2013-02-05T20:21:17.583 回答
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 的相似之处,只是我们还输出了一个单位值。由于单位值无论如何都在一个元组中,这并不能告诉我们太多,所以我会说它们基本上是相同的。

注意,出于某种原因,我使用 ~=~ 来表示类似但不完全等同于。但是它们非常相似,特别注意我们可以将 a 转换Callback a为 a CircuitT (Kleisli IO) a (),反之亦然。

编辑:我也完全同意以下想法:A)一元协同流(一元操作期望无限数量的值,我认为这意味着)和 B)仅消费管道(在许多方面非常类似于没有输出的电路类型,或者更确切地说输出设置为(),因为这样的管道也可能有输出)。

于 2013-02-05T21:40:38.540 回答
3

只是一个观察,您的类型似乎与Consumer p a m出现在管道库(可能还有其他类似的库)中非常相关:

type Consumer p a = p () a () C
-- A Pipe that consumes values
-- Consumers never respond.

whereC是一个空数据类型,pProxy类型类的一个实例。它使用类型的值a并且从不产生任何值(因为它的输出类型为空)。

例如,我们可以将 a 转换Callback为 a 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

请参阅教程

(这应该是一个评论,但它有点太长了。)

于 2013-02-05T21:39:35.847 回答