首页 文章

用管道的WriterP编写一个简单的累加器

提问于
浏览
4

使用管道库,我想编写一个程序来从某个源读取数据并单独累积它(例如,使用 Sum ) . 最简单的方法是,

import Control.Proxy as 
 import Data.Monoid (Sum)

 main = do
     let source = enumFromToS (0::Int) 5
     a <- runWriterT $ runProxy $ source >-> foldD Sum
     print a

当然,虽然这适用于小型源,但由于 WriterT 的累加器的惰性,大输入将导致可怕的堆栈溢出 .

值得庆幸的是,似乎 pipes 预计会这样,提供一个带有严格累加器的 WriterP 代理 . 不幸的是,围绕这个代理的文档非常稀少 . 经过一番探索(并简化问题而不是为每个下游元素累积1),我来到这个程序,

import Control.Proxy
import Control.Proxy.Trans.Writer
import Data.Monoid (Sum)

main = do
    let source = enumFromToS (0::Int) 5
    a <- runProxy $ runWriterK $ source >-> \x->tell (Sum 1::Sum Int)
    print a

当然,这个程序甚至没有正确地执行简化的任务,因为它累积到1而不是6 . 如果我没有弄错,这个行为可以解释为管道在终止之前只读取一个元素 . 要重复直到输入结束,我想出了以下内容,

import Control.Proxy
import Control.Proxy.Trans.Writer
import Data.Monoid (Sum)

main = do
    let source = enumFromToS (0::Int) 5
    a <- runProxy $ runWriterK $ source >-> fold Sum
    print a

fold :: (Monad m, Proxy p, Monoid w) => (a -> w) -> a' -> WriterP w p a' a a' a m r
fold f = go
  where go x = do a <- request x
                  tell $ f a
                  x' <- respond a
                  go x'

但是,这段代码返回的累加器为0.这是为什么?是否有像 pipes 中提供的 fold 这样的功能?

鉴于 pipes 的许多用例是长时间运行的进程,使用大型数据集, Control.Proxy.Prelude 中的折叠是否有必要围绕严格 WriterP 而不是 WriterT 构建?目前感觉 pipes 中的代理变换器是二等公民,但是缺少许多组合使得 WriterT 如此方便 .

2 回答

  • 6

    我'm adding a new answer because I'已经在 pipes-3.3 修复了这个问题,我刚刚上传到Hackage . 管道背后的理论表明,您期望的全局行为一直是正确的行为,现在 WriterP 全局行为,因此您可以在管道中折叠 .

    我修改了你的例子,表明你将使用 pipes-3.3 实现它:

    import Control.Proxy
    import Control.Proxy.Trans.Writer
    
    main = do
        let source = enumFromToS (0::Int) 5
        a <- runProxy $ execWriterK $ source >-> sumD
        print a
    

    您现在还可以检索管道中折叠的结果 . 例如,这是完全有效的:

    chunksOf :: (Monad m, Proxy p) => Int -> () -> Pipe p a [a] m r
    chunksOf n () = runIdentityP $ forever $ do
        -- The unitU discards the values that 'toListD' reforwards
        as <- execWriterK (takeB_ n >-> toListD >-> unitU) ()
        respond as
    

    这是一个示例用法:

    >>> runProxy $ getLineS >-> takeWhileD (/= "quit") >-> chunksOf 3 >-> printD
    1<Enter>
    2<Enter>
    3<Enter>
    ["1","2","3"]
    4<Enter>
    5<Enter>
    6<Enter>
    ["4","5","6"]
    quit
    

    很抱歉第一次得错答案!

  • 4

    请记住,代理转换器在本地运行,而基本monad在全局运行 . 这意味着 WriterP 让每个代理都维护自己的累加器,并且首先终止的代理确定哪个累加器返回 . 累加器返回 0 的原因是因为你的枚举管道首先返回而且它没有累积任何东西 .

    WriterP fold是严格的,因为我可以控制该类型(而不是 transformers 中的那个) . 我从未打算将它作为代理前奏中的懒惰折叠的严格替代品 . 使用的正确严格替代方案是 foldlD' .

    请注意 foldlD' mappend 基本上是严格的 Writer 折叠,特别是如果使用 mempty 作为初始状态运行基本 State monad .

    在您的情况下,您可以使用以下方法更轻松地完成:

    main = do
        let source = enumFromToS (0::Int) 5
        a <- (`runStateT` 0) $ runProxy $ source >-> foldlD' (+)
        print a
    

    这将严格折叠输入 .

相关问题