这种类型的一般结构是什么?

时间:2013-02-05 19:54:08

标签: haskell types higher-kinded-types

虽然早些时候出现了问题,但我创建了以下代码:

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使用IORefCallback转换为简单函数。

该类型的一般结构是:

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

看起来很像这可能与类别理论中的一些众所周知的数学结构同构。

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

4 个答案:

答案 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 fFree fMonad都会自动f,在这种情况下,((->) a)是我们的仿函数,因此它会自动执行正确的操作。这就是范畴理论的神奇之处!

此外,我们从来没有必要定义MonadTrans实例才能使用lift。给定任何仿函数FreeT ff自动成为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是空数据类型,pProxy类型类的实例。它使用类型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

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