11

Haskellを使用してディレクトリ構造の再帰下降を行おうとしています。必要に応じて(怠惰に)子ディレクトリとファイルのみを取得したいと思います。

次のコードを記述しましたが、実行すると、最初のファイルの前にすべてのディレクトリにアクセスしたことがトレースに示されています。

module Main where

import Control.Monad ( forM, forM_, liftM )
import Debug.Trace ( trace )
import System.Directory ( doesDirectoryExist, getDirectoryContents )
import System.Environment ( getArgs )
import System.FilePath ( (</>) )

-- From Real World Haskell, p. 214
getRecursiveContents :: FilePath -> IO [FilePath]
getRecursiveContents topPath = do
  names <- getDirectoryContents topPath
  let
    properNames =
      filter (`notElem` [".", ".."]) $
      trace ("Processing " ++ topPath) names
  paths <- forM properNames $ \name -> do
    let path = topPath </> name
    isDirectory <- doesDirectoryExist path
    if isDirectory
      then getRecursiveContents path
      else return [path]
  return (concat paths)

main :: IO ()
main = do
  [path] <- getArgs
  files <- getRecursiveContents path
  forM_ files $ \file -> putStrLn $ "Found file " ++ file

ファイル処理とディセントをインターリーブするにはどうすればよいですか?次のfiles <- getRecursiveContents path前にアクションが実行されるという問題はありますか?forM_main

4

4 に答える 4

9

これはまさに、反復/コルーチンが解決するように設計された種類の問題です。

で簡単にこれを行うことができますpipes。私があなたに加えた唯一の変更はgetRecursiveContents、それを返すのではなく、 sとファイル名で行うことProducerでした。これにより、ダウンストリームは完了を待つのではなく、ファイル名をすぐに処理できます。FilePathrespondgetRecursiveContents

module Main where

import Control.Monad ( forM_, liftM )
import Control.Proxy
import System.Directory ( doesDirectoryExist, getDirectoryContents )
import System.Environment ( getArgs )
import System.FilePath ( (</>) )

getRecursiveContents :: (Proxy p) => FilePath -> () -> Producer p FilePath IO ()
getRecursiveContents topPath () = runIdentityP $ do
  names <- lift $ getDirectoryContents topPath
  let properNames = filter (`notElem` [".", ".."]) names
  forM_ properNames $ \name -> do
    let path = topPath </> name
    isDirectory <- lift $ doesDirectoryExist path
    if isDirectory
      then getRecursiveContents path ()
      else respond path

main :: IO ()
main = do
    [path] <- getArgs
    runProxy $
            getRecursiveContents path
        >-> useD (\file -> putStrLn $ "Found file " ++ file)

これにより、各ファイルがツリーを通過するとすぐに出力され、レイジーは必要ありませんIOuseDまた、実際のファイル処理ロジックを使用してステージを切り替えるだけなので、ファイル名の操作を変更するのも非常に簡単です。

詳細については、 Control.Proxy.Tutorialpipesをお読みになることを強くお勧めします。

于 2013-01-10T15:46:49.657 に答える
7

怠惰なIO/を使用するのは良い方法でunsafe...はありません。レイジーIOは、閉じられていないリソースや純粋なコード内での不純なアクションの実行など、多くの問題を引き起こします。HaskellWikiのレイジーI/ Oの問題も参照してください。)

安全な方法は、いくつかのiteratee/enumeratorライブラリを使用することです。(問題のある怠惰なIOを置き換えることが、これらの概念を開発する動機でした。)getRecursiveContentsデータのソース(別名列挙子)になります。そして、データはいくつかのイテレータによって消費されます。(Haskell wikiの列挙子とiterateeも参照してください。)

列挙子ライブラリに関するチュートリアルがあり、ディレクトリツリーのトラバースとフィルタリングの例を示し、簡単な検索ユーティリティを実装しています。メソッドを実装します

enumDir :: FilePath -> Enumerator FilePath IO b

これは基本的にあなたが必要とするものです。おもしろいと思います。

また、 The Monad Reader、Issue 16Iteratee:Teaching a Old Fold New Tricks by John W. Lato、iterateelibraryの作者による反復を説明する素晴らしい記事があります。

今日、多くの人がパイプなどの新しいライブラリを好みます。あなたは比較に興味があるかもしれません:列挙子対コンジット対パイプの長所と短所は何ですか?

于 2013-01-10T14:49:58.697 に答える
2

Niklas B.のコメントのおかげで、私が持っている解決策は次のとおりです。

module Main where

import Control.Monad ( forM, forM_, liftM )
import Debug.Trace ( trace )
import System.Directory ( doesDirectoryExist, getDirectoryContents )
import System.Environment ( getArgs )
import System.FilePath ( (</>) )
import System.IO.Unsafe ( unsafeInterleaveIO )

-- From Real World Haskell, p. 214
getRecursiveContents :: FilePath -> IO [FilePath]
getRecursiveContents topPath = do
  names <- unsafeInterleaveIO $ getDirectoryContents topPath
  let
    properNames =
      filter (`notElem` [".", ".."]) $
      trace ("Processing " ++ topPath) names
  paths <- forM properNames $ \name -> do
    let path = topPath </> name
    isDirectory <- doesDirectoryExist path
    if isDirectory
      then unsafeInterleaveIO $ getRecursiveContents path
      else return [path]
  return (concat paths)

main :: IO ()
main = do
  [path] <- getArgs
  files <- unsafeInterleaveIO $ getRecursiveContents path
  forM_ files $ \file -> putStrLn $ "Found file " ++ file

もっと良い方法はありますか?

于 2013-01-10T14:17:50.330 に答える
0

私は最近、非常によく似た問題を調べていました。IOモナドを使用してやや複雑な検索を行おうとしていて、興味のあるファイルが見つかったら停止しました。列挙子、コンジットなどのライブラリを使用したソリューションはそれらの回答が投稿されたときにできる最善のことをするために、私はちょうど約1年前にGHCのベースライブラリのIOインスタンスになり、Alternativeいくつかの新しい可能性を開くことを学びました。これが私がそれを試すために書いたコードです:

import Control.Applicative (empty)
import Data.Foldable (asum)
import Data.List (isSuffixOf)
import System.Directory (doesDirectoryExist, listDirectory)
import System.FilePath ((</>))

searchFiles :: (FilePath -> IO a) -> FilePath -> IO a
searchFiles f fp = do
    isDir <- doesDirectoryExist fp
    if isDir
        then do
            entries <- listDirectory fp
            asum $ map (searchFiles f . (fp </>)) entries
        else f fp

matchFile :: String -> FilePath -> IO ()
matchFile name fp
    | name `isSuffixOf` fp = putStrLn $ "Found " ++ fp
    | otherwise = empty

このsearchFiles関数は、ディレクトリツリーの深さ優先探索を実行し、最初の引数として渡された関数によって決定されるように、探しているものが見つかると停止します。この関数は、 ;matchFileの最初の引数として使用する適切な関数を作成する方法を示すためのものです。searchFiles実生活では、おそらくもっと複雑なことをするでしょう。

ここで興味深いのは、結果を返さずに計算を「あきらめる」ために使用できることです。また、計算を(これは)と連鎖emptyさせて、いずれかが成功するまで計算を試行し続けることができます。IOasumfoldr (<|>) empty

アクションの型アノテーションが、IO意図的に結果を生成しない可能性があるという事実を反映しなくなったことは少し不安ですが、コードが単純化されることは確かです。以前はのようなタイプを使おうとしIO (Maybe a)ていましたが、そうするとアクションの作成が非常に難しくなりました。

IMHOのような型を使用する理由はもうあまりありませんIO (Maybe a)が、そのような型を使用するコードとインターフェイスする必要がある場合は、2つの型の間で簡単に変換できます。に変換するIO aIO (Maybe a)は、を使用できますControl.Applicative.optional。逆に、次のようなものを使用できます。

maybeEmpty :: IO (Maybe a) -> IO a
maybeEmpty m = m >>= maybe empty pure
于 2017-10-20T00:00:28.183 に答える