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
最终解决方案,借鉴自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
我正在以传统方式递归地遍历目录。这是一个工作原型:
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
最终解决方案,借鉴自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