Haskell: 如何记数

Haskell: how to keep count

我正在以传统方式递归地遍历目录。这是一个工作原型:

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

到目前为止我看到的所有解决方案都丑得超乎想象,如果适用的话。有什么好的管理方法吗?

不知从何而来?当然不是。

不过,您可以用数字压缩 files,然后 mapM 覆盖它们:

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

您可以通过向您的函数添加额外的效果来完成此操作;即状态效应。

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

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

但是,我不建议这样写这个函数。从语义上讲,您的函数正在收集一堆文件路径。与其打印它们,不如从函数中 return 它们;您以后可以随时打印它们!修改后的逻辑其实很简单:

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

您可以使用此函数打印带编号的文件,而无需明确跟踪某些状态,因为您可以访问所有文件 'at once':

> 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'

我可能会使用像 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.each, S.zip, S.mapM_ 来自 streaming.

最终解决方案,借鉴自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)

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

此解决方案不需要额外的库,在找到每个文件时立即处理它,并且为了分离问题,不需要 traverseFlatDst 知道生成的文件正在做什么.

最后一个功能是通过将一个小型有效状态机(实际上是一个阶跃函数)作为参数传递给 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