40

Haskell で読み取り用にファイルを開くと、ファイルを閉じるとその内容を使用できないことがわかりました。たとえば、次のプログラムはファイルの内容を出力します。

main = do inFile <- openFile "foo" ReadMode
          contents <- hGetContents inFile
          putStr contents
          hClose inFile

putStr行と行を入れ替えhCloseても効果はないと思っていましたが、このプログラムは何も出力しません。

main = do inFile <- openFile "foo" ReadMode
          contents <- hGetContents inFile
          hClose inFile
          putStr contents

なぜこれが起こるのですか?遅延評価と関係があるのではないかと思いますが、これらの式は順序付けされるので問題ないと思いました。のような関数をどのように実装しますreadFileか?

4

6 に答える 6

39

他の人が述べているように、それは遅延評価のためです。この操作の後、ハンドルは半分閉じられ、すべてのデータが読み取られると自動的に閉じられます。このように、hGetContents と readFile はどちらも怠惰です。ハンドルを開いたままにしておくことに問題がある場合は、通常、読み取りを強制します。簡単な方法は次のとおりです。

import Control.Parallel.Strategies (rnf)
-- rnf means "reduce to normal form"
main = do inFile <- openFile "foo" 
          contents <- hGetContents inFile
          rnf contents `seq` hClose inFile -- force the whole file to be read, then close
          putStr contents

しかし最近では、ファイル I/O に文字列を使用する人はもういません。新しい方法は、Data.ByteString (ハックで利用可能) と、遅延読み取りが必要な場合に Data.ByteString.Lazy を使用することです。

import qualified Data.ByteString as Str

main = do contents <- Str.readFile "foo"
          -- readFile is strict, so the the entire string is read here
          Str.putStr contents

ByteStrings は、大きな文字列 (ファイルの内容など) を処理する方法です。これらは、文字列 (= [Char]) よりもはるかに高速で、メモリ効率が高くなります。

ノート:

便宜上、Control.Parallel.Strategies から rnf をインポートしました。あなたはそれのようなものを自分でかなり簡単に書くことができます:

  forceList [] = ()
  forceList (x:xs) = forceList xs

これは、リストの背骨 (値ではなく) のトラバーサルを強制するだけで、ファイル全体を読み取る効果があります。

レイジー I/O は、専門家によって悪と見なされるようになっています。当分の間、ほとんどのファイル I/O に厳密なバイト文字列を使用することをお勧めします。オーブンには、構成可能な増分読み取りを取り戻そうとするいくつかのソリューションがあり、その中で最も有望なものは、Oleg による「Iteratee」と呼ばれています。

于 2008-11-18T02:00:22.963 に答える
4

[更新:Prelude.readFileは以下に説明するように問題を引き起こしますが、Data.ByteStringのすべてのバージョンを使用するように切り替えると機能します:例外は発生しなくなりました。]

Haskellの初心者はここにいますが、現在、「readFileは厳密であり、完了するとファイルを閉じる」という主張は購入しません。

go fname = do
   putStrLn "reading"
   body <- readFile fname
   let body' = "foo" ++ body ++ "bar"
   putStrLn body' -- comment this out to get a runtime exception.
   putStrLn "writing"
   writeFile fname body'
   return ()

これは、私がテストしていたファイルでそのまま機能しますが、putStrLnをコメントアウトすると、writeFileが失敗するようです。(Haskellの例外メッセージがいかに不完全であるか、行番号が不足しているなど、興味深いですか?)

Test> go "Foo.hs"
reading
writing
Exception: Foo.hs: openFile: permission denied (Permission denied)
Test> 

?!?!?

于 2008-11-19T01:16:34.753 に答える
2

これは、hGetContents がまだ何もしていないためです。これは遅延 I/O です。結果文字列を使用する場合にのみ、ファイルが実際に読み取られます (または必要な部分)。強制的に読み取らせたい場合は、その長さを計算し、seq 関数を使用して長さを強制的に評価できます。遅延 I/O はクールな場合もありますが、混乱を招く場合もあります。

詳細については、たとえばReal World Haskell の遅延 I/O に関する部分を参照してください。

于 2008-11-17T22:58:06.077 に答える
1

IO を遅延させたいが、このようなエラーが発生しないように安全に行うには、safe-lazy-ioなどのように設計されたパッケージを使用します。(ただし、safe-lazy-io はバイト文字列 I/O をサポートしていません。)

于 2010-11-03T10:42:17.753 に答える
1

前に述べたように、hGetContents怠け者です。readFile厳密であり、完了したらファイルを閉じます。

main = do contents <- readFile "foo"
          putStr contents

Hugs で次の結果が得られます

> main
blahblahblah

どこfooですか

blahblahblah

興味深いことに、すべてではなく、入力の一部が読み取らseqれることのみが保証されます。

main = do inFile <- openFile "foo" ReadMode
          contents <- hGetContents $! inFile
          contents `seq` hClose inFile
          putStr contents

収量

> main
b

良いリソースは次のとおりです: Haskell プログラムをより速く、より小さくする: hGetContents、hClose、readFile

于 2008-11-17T23:55:05.777 に答える
0

説明はここに含めるにはかなり長いです。短いヒントのみを提供することをお許しください。「セミクローズド ファイル ハンドル」と「unsafePerformIO」について読む必要があります。

要するに、この動作は、セマンティックの明確さと遅延評価の間の設計上の妥協です。ファイルの内容に対して何もしないことが確実になるまで hClose を延期するか (エラー ハンドラーで呼び出すなど)、または hGetContents 以外の何かを使用してファイルの内容を遅延なく取得する必要があります。

于 2008-11-17T20:52:42.103 に答える