Haskell 导管-分派到多个输出文件中

Haskell 导管-分派到多个输出文件中,haskell,conduit,Haskell,Conduit,我试图将项目从一个管道分派到多个输出文件中,问题非常类似,但有一些不同: 在前面的解决方案中,每个接收器都有一个过滤器,用于确定元素是否属于该接收器。在我的例子中,来自上游的每个元素都精确地到达一个文件,在有大量文件的情况下,最好只进行一次操作来决定它要到达哪个文件 这些文件是按需创建的。“选择器”函数决定下一个元素将进入哪个接收器,如果它还不存在,则使用“创建新接收器”函数创建它 例如,如果源产生:84715 而接收器选择器是模块3,则操作顺序为: Create file 2 Add 8

我试图将项目从一个管道分派到多个输出文件中,问题非常类似,但有一些不同:

  • 在前面的解决方案中,每个接收器都有一个过滤器,用于确定元素是否属于该接收器。在我的例子中,来自上游的每个元素都精确地到达一个文件,在有大量文件的情况下,最好只进行一次操作来决定它要到达哪个文件

  • 这些文件是按需创建的。“选择器”函数决定下一个元素将进入哪个接收器,如果它还不存在,则使用“创建新接收器”函数创建它

例如,如果源产生:84715
而接收器选择器是模块3,则操作顺序为:

Create file 2
Add 8 to file 2
Create file 1
Add 4 to file 1
Add 7 to file 1
Add 1 to file 1
Add 5 to file 2
我正在为这个调度器考虑一种类型,如下所示:

        dispatcherSink_ :: (Monad m) => 
                           (a -> k) ->               -- sink selector
                           (k -> Sink a m ()) ->     -- new sink
                           Sink a m ()
我曾尝试使用evalStateC编写函数,其中一个内部StateT包含一个汇映射,但我无法绑定这些类型。我不确定你是否能用同一个水槽两次

我想做的事可能吗

我在Haskell还是个新手,所以任何帮助都将不胜感激

编辑 我想我可以创建一个可恢复墨水的地图,有一个用于此的库,但它取决于一个旧的非常特殊的导管版本,所以阴谋集团无法安装它。 最后,我没有找到使用前一种类型编写函数的方法,可以处理任何接收器,因此我提出了一个直接处理文件的函数:

import System.IO (hClose,openFile,IOMode(WriteMode))
import Conduit
import Data.IOData
import qualified Data.Foldable as F
import qualified Data.Map.Strict as M
import Control.Monad.State.Strict
import Data.ByteString.Char8 (pack)


fileDispatcherSink ::
  (MonadIO m, IOData c,Ord k) =>
  (a -> k) ->
  (a -> c) ->
  (k -> FilePath) ->
  Sink a m ()
fileDispatcherSink selector toChunked path =
  evalStateC M.empty $ dispatcher 
  where 
    dispatcher = do
      next  <- await
      m <- get
      case next of
        Nothing -> liftIO $ F.traverse_ hClose m
        Just a -> do
          let k = selector a
          h <- case M.lookup k m of
                Nothing -> do
                  nh <- liftIO $ openFile (path k) WriteMode
                  put $ M.insert k nh m
                  return nh
                Just h -> return h
          yield (toChunked a) $$ sinkHandle h
          dispatcher

testSource :: (Monad m) => Source m Int
testSource = yieldMany [8, 4, 7, 1, 5]

main :: IO ()
main = testSource
       $$ fileDispatcherSink (`mod` 3) (pack . show) ((++ ".txt") . show)
导入System.IO(hClose、openFile、IOMode(WriteMode)) 导入导管 导入数据.IOData 导入符合条件的数据。可折叠为F 导入符合条件的Data.Map.Strict作为M 进口控制。单子。状态。严格 导入Data.ByteString.Char8(包) fileDispatcherSink:: (MonadIO m,IOData c,Ord k)=> (a->k)-> (a->c)-> (k->FilePath)-> 下沉一米() fileDispatcherSink选择器到缓存路径= evalStateC M.empty$dispatcher 哪里 调度程序=do 下一步 设k=选择器a h do nh返回h 收益率(toChunked a)$$h 调度员 testSource::(Monad m)=>Source m Int testSource=yieldMany[8,4,7,1,5] main::IO() main=testSource $$fileDispatcherSink(`mod`3)(pack.show)((++“.txt”).show)
有没有办法编写_dispatcherSink uuu函数?

实现时存在概念问题

dispatcherSink_ :: (Monad m) => 
                   (a -> k) ->               -- sink selector
                   (k -> Sink a m ()) ->     -- new sink
                   Sink a m ()
。在管道中,数据从上游拉到下游,而不是推送。因此,
Sink
决定是否从其上游管道请求下一个输入值。因此,您不能真正拥有
Sink
s的映射,请读取输入值,然后将其馈送到
Sink
s之一。您选择的
Sink
可能不会决定读取输入值,它可能决定完成,然后您将如何处理输入值?您可以为该键创建一个新接收器,但它也可以决定不接受输入

因此,你很可能需要一些不同的概念,你可以将一个值和你可以最终确定的东西推到它上面,而不是一个
Sink
。一个想法(未经测试):

写入文件的实现将打开一个文件,保留句柄,
psPush
将只向文件中写入一个块,返回相同的对象,而
psFinalize
将关闭文件

然后你可以实现这样一个变体

dispatcherSink_ :: (Monad m) => 
                   (a -> k) ->                 -- sink selector
                   (k -> m (PushSink a m)) ->  -- new sink
                   Sink a m ()

它将值推送到
PushSink
s,并在没有输入的情况下完成所有值。

您能介绍您迄今为止所做的尝试吗?谢谢@PetrPudlák,这是一种有趣的方法。我有一种感觉,如果有类似的情况,我正在尝试做的将更适合一种“反应管道”。在这种情况下,zipSinks函数是如何工作的?恐怕我还拿不到它的源代码。
dispatcherSink_ :: (Monad m) => 
                   (a -> k) ->                 -- sink selector
                   (k -> m (PushSink a m)) ->  -- new sink
                   Sink a m ()