haskell – 惯用双向管道,下游状态无损失

说我有简单的生产者/消费者模式,消费者想把一些状态传回给制片人.例如,让下游流对象是我们要写入文件的对象,上游对象是表示对象在文件中写入的位置(例如,偏移量)的一些令牌.

这两个进程可能看起来像这样(使用管道4.0),

{-# LANGUAGE GeneralizedNewtypeDeriving #-}

import Pipes
import Pipes.Core
import Control.Monad.Trans.State       
import Control.Monad

newtype Object = Obj Int
               deriving (Show)

newtype ObjectId = ObjId Int
                 deriving (Show, Num)

writeObjects :: Proxy ObjectId Object () X IO r
writeObjects = evalStateT (forever go) (ObjId 0)
  where go = do i <- get
                obj <- lift $request i
                lift $lift $putStrLn $"Wrote "++show obj
                modify (+1)

produceObjects :: [Object] -> Proxy X () ObjectId Object IO ()
produceObjects = go
  where go [] = return ()
        go (obj:rest) = do
            lift $putStrLn $"Producing "++show obj
            objId <- respond obj
            lift $putStrLn $"Object "++show obj++" has ID "++show objId
            go rest

objects = [ Obj i | i <- [0..10] ]

简单的,因为这可能是,我有一个很难的推理如何组成它们.理想情况下,我们希望采用基于推送的控制流程,如下所示,

> writeObjects通过在发送初始ObjId 0上游的请求时阻止启动.
> produceObjects发送第一个对象Obj 0,下游
> writeObjects写入对象并递增其状态,并等待请求,此时向上发送ObjId 1
>在generateObjects中使用ObjId 0返回
在第(2)步中,yieldObject继续使用第二个对象Obj 1

我的初步尝试是按照推动式组合如下,

main = void $run $produceObjects objects >>~ const writeObjects

注意使用const来解决其他不兼容的类型(这可能是问题所在).在这种情况下,我们发现ObjId 0被吃掉,

Producing Obj 0
Wrote Obj 0
Object Obj 0 has ID ObjId 1
Producing Obj 1
...

基于拉式的方法,

main = void $run $const (produceObjects objects) +>> writeObjects

遇到类似的问题,这一次放弃Obj 0.

如何以想要的方式组合这些作品?

最佳答案
选择使用哪种组合取决于哪​​个组件应该启动整个过程.如果您希望下游管道启动进程,那么您希望使用基于拉式的组合(即(>>)/(>>)),但如果希望上游管道启动进程,则应使用基于推的组合(即(>〜)/(>〜>)).你得到的类型错误实际上是在你的代码中警告你有一个逻辑错误:你还没有清楚地确定哪个组件首先启动该进程.

从您的描述中,很明显,您希望控制流从produceObjects开始,因此您希望使用基于push的组合.一旦您使用基于推送的构图,组合运算符的类型将告诉您需要了解的有关如何修复代码的所有内容.我会把它的类型和专长到你的构图链:

-- Here I'm using the `Server` and `Client` type synonyms to simplify the types
(>>~) :: Server ObjectId Object IO ()
      -> (Object -> Client ObjectId Object IO ())
      -> Effect IO ()

您已经注意到,当您尝试使用(>>〜)时,您遇到的类型错误告诉您,您的writeObjects函数中缺少Object类型的参数.这是静态强制的,在接收到第一个Object(通过初始参数)之前,您无法在writeObject中运行任何代码.

解决方案是重写你的writeObjects函数,如下所示:

writeObjects :: Object -> Proxy ObjectId Object () X IO r
writeObjects obj0 = evalStateT (go obj0) (ObjId 0)
  where go obj = do i <- get
                    lift $lift $putStrLn $"Wrote "++ show obj
                    modify (+1)
                    obj' <- lift $request i
                    go obj'

这就给出了正确的行为:

>>> run $produceObjects objects >>~ writeObjects
Producing Obj 0
Wrote Obj 0
Object Obj 0 has ID ObjId 0
Producing Obj 1
Wrote Obj 1
Object Obj 1 has ID ObjId 1
Producing Obj 2
Wrote Obj 2
Object Obj 2 has ID ObjId 2
Producing Obj 3
Wrote Obj 3
Object Obj 3 has ID ObjId 3
Producing Obj 4
Wrote Obj 4
Object Obj 4 has ID ObjId 4
Producing Obj 5
Wrote Obj 5
Object Obj 5 has ID ObjId 5
Producing Obj 6
Wrote Obj 6
Object Obj 6 has ID ObjId 6
Producing Obj 7
Wrote Obj 7
Object Obj 7 has ID ObjId 7
Producing Obj 8
Wrote Obj 8
Object Obj 8 has ID ObjId 8
Producing Obj 9
Wrote Obj 9
Object Obj 9 has ID ObjId 9
Producing Obj 10
Wrote Obj 10
Object Obj 10 has ID ObjId 10

您可能会想知道为什么这两个管道中的一个需要初始参数的这个要求是有道理的,除了抽象的理由以外,这是类别法要求的.简单的英语解释是,替代方案是在writeObjects到达其第一个请求语句之前,您需要缓冲第一个传输的对象“之间”两个管道之间.这种方法产生了许多有问题的行为和错误的角落案例,但可能最重要的问题是,管道组合不再是关联的,效果的顺序将根据您组成的事情的顺序而改变.

双向管道组合运算符的好处在于,类型的工作原理是,您可以总是推断出组件是否“激活”(即启动控制)或“被动”(即等待输入),纯粹通过研究类型.如果组合说某个管道(如writeObjects)必须采取一个参数,那么它是被动的.如果没有参数(如generateObjects),那么它是活动的并启动控制.因此,组合将迫使您在管道中至多有一个活动管道(不采用初始参数的管道),这是开始控制的管道.

转载注明原文:haskell – 惯用双向管道,下游状态无损失 - 代码日志