哈斯克尔:如何保持计数

时间:2017-12-11 16:45:09

标签: haskell counter io-monad

我以传统的方式递归地走一个目录。这是一个工作原型:

traverseFlatDst :: FilePath -> Int -> Int -> FilePath -> IO ()
traverseFlatDst dstRoot total totw srcDir = do
  (dirs, files) <- listDir srcDir
  mapM_ (\file -> putStrLn (printf "%s" (strp file))) files    -- tracing
  let traverse = traverseFlatDst dstRoot total totw
  mapM_ traverse dirs

我有一个不太常见的请求:每个跟踪线都应该编号(它不是真正用于跟踪)。像这样:

traverseFlatDst :: FilePath -> Int -> Int -> FilePath -> IO ()
traverseFlatDst dstRoot total totw srcDir = do
  (dirs, files) <- listDir srcDir
  mapM_ (\file -> putStrLn (printf "%d: %s" counterFromNowhere (strp file))) files
  let traverse = traverseFlatDst dstRoot total totw
  mapM_ traverse dirs

到目前为止,我所见过的所有解决方案都难以想象,如果适用的话。有没有一种很好的方法来管理它?

5 个答案:

答案 0 :(得分:3)

您可以通过为您的功能添加额外效果来实现此目的;即国家效应。

import Control.Monad.State

printPath :: (PrintfArg t, Show a) => (t, a) -> IO ()
printPath (l, file) = printf "%d : %s\n" l (show file)

traverseFlatDst :: Path Abs Dir -> IO ()
traverseFlatDst =
  let loop srcDir = do
        (dirs, files) <- listDir srcDir
        i <- get
        put (i + length files)
        mapM_ (liftIO . printPath) $ zip [i..] files
        mapM_ loop dirs
  in \s -> evalStateT (loop s) 0

(注意:为了清楚起见,我还删除了未使用的参数)。

但是,我建议不要这样写这个功能。从语义上讲,您的函数正在收集一堆文件路径。你应该从函数中返回它们,而不是打印它们;你可以随时打印出来!修改版本的逻辑实际上非常简单:

traverseFlatDst' :: Path Abs Dir -> IO [Path Abs File]
traverseFlatDst' srcDir = do
  (dirs, files) <- listDir srcDir
  (concat . (files:)) <$> mapM traverseFlatDst' dirs

您可以使用此功能打印带有数字的文件,而无需明确跟踪某些状态,因为您可以立即访问所有文件&#39;:

> traverseFlatDst' somePath >>= mapM_ printPath . zip [0..]

另请注意,第二个版本比第一个版本严格得多;它将在开始打印任何内容之前遍历整个目录树。作为一般规则,严格版本无论如何都更好,但如果你想要懒惰版本,你可以使用unsafeInterleaveIO来编写它:

import System.IO.Unsafe (unsafeInterleaveIO)

traverseFlatDst' :: Path Abs Dir -> IO [Path Abs File]
traverseFlatDst' srcDir = do
  (dirs, files) <- listDir srcDir
  files' <- unsafeInterleaveIO $ mapM traverseFlatDst' dirs
  return $ concat $ files:files'

答案 1 :(得分:3)

我可能会使用像streaming这样的流媒体库来分隔枚举文件,不添加数字和打印装饰条目:

import Streaming
import qualified Streaming.Prelude as S

traverseFlatDst :: FilePath -> Int -> Int -> FilePath -> Stream (Of FilePath) IO ()
traverseFlatDst dstRoot total totw srcDir = do
  (dirs, files) <- liftIO $ listDir srcDir
  S.each files
  mapM_ (traverseFlatDst dstRoot total totw) dirs

decorate :: Stream (Of FilePath) IO r -> Stream (Of (Int,FilePath)) IO r
decorate stream = S.zip (S.enumFrom 1) stream

display:: Stream (Of (Int,FilePath)) IO () -> IO ()
display = S.mapM_ $ \(index,path) ->  putStrLn $ show index ++ " " ++ path

S.eachS.zipS.mapM_来自流媒体

答案 2 :(得分:0)

从哪儿来?当然不是。

您可以使用数字压缩files,然后mapM对其进行压缩:

mapM_ (\(file, counter) -> putStrLn (printf "%d: %s" counter (strp file))) (zip [0..] files)

答案 3 :(得分:0)

最终解决方案,借鉴How to implement a global counter using Monad?

import Data.IORef

type Counter = Int -> IO Int

makeCounter :: IO Counter
makeCounter = do
  r <- newIORef 0
  return (\i -> do modifyIORef r (+i)
                   readIORef r)


printPath :: Counter -> FilePath -> IO ()
printPath counter file = do
  n <- counter 1
  putStrLn (printf "%d : %s" n (strp file))


traverseFlatDst :: FilePath -> Int -> Int -> Counter -> FilePath -> IO ()
traverseFlatDst dstRoot total totw cnt srcDir = do
  (dirs, files) <- listDir srcDir
  let iterate = printPath cnt
  mapM_  iterate files                -- tracing
  let traverse = traverseFlatDst dstRoot total totw cnt
  mapM_ traverse dirs


groom :: FilePath -> FilePath -> Int -> IO ()
groom src dst total = do
  counter <- makeCounter
  let totWidth = length $ show total
  traverseFlatDst dst total totWidth counter src
  putStrLn (printf "total: %d, width: %d" total totWidth)

仍然关在笼子里,不能在任何地方使用,但没关系。不难看。

答案 4 :(得分:0)

此解决方案不需要额外的库,在找到每个文件时处理它们,并且为了分离关注点,不需要traverseFlatDst知道对生成的文件执行了什么操作。< / p>

最后一个功能是通过将一个小型有效状态机(真正的一个步骤函数)作为参数传递给traverseFlatDst,并在机器状态下使traverseFlatDst多态来实现的,所以它不会知道任何事情:

{-# language RankNTypes #-}
import Control.Monad (foldM)

type Source e = forall s. (s -> e -> IO s) -> s -> IO s

traverseFlatDst :: FilePath -> Int -> Int -> FilePath -> Source FilePath
traverseFlatDst dstRoot total totw srcDir step state = do
  (dirs, files) <- listDir srcDir
  state' <- foldM step state files
  foldM (\s path -> traverseFlatDst dstRoot total totw path step s) state' dirs

-- Pass this as the step argument to traverseFlatDst
-- The counter is the state.
step :: Int -> FilePath -> IO Int
step index path = do
    putStrLn $ show index ++ " " ++ path
    return $ succ index