跳轉到內容

Haskell/Libraries/IO

來自華夏公益教科書

這裡,我們將探討 System.IO 模組中最常用的元素。

data IOMode = ReadMode | WriteMode
             | AppendMode | ReadWriteMode

openFile :: FilePath -> IOMode -> IO Handle
hClose :: Handle -> IO ()

hIsEOF :: Handle -> IO Bool

hGetChar :: Handle -> IO Char
hGetLine :: Handle -> IO String
hGetContents :: Handle -> IO String

getChar :: IO Char
getLine :: IO String
getContents :: IO String

hPutChar :: Handle -> Char -> IO ()
hPutStr :: Handle -> String -> IO ()
hPutStrLn :: Handle -> String -> IO ()

putChar :: Char -> IO ()
putStr :: String -> IO ()
putStrLn :: String -> IO ()

readFile :: FilePath -> IO String
writeFile :: FilePath -> String -> IO ()

注意

FilePathString 的一個類型別名。因此,例如,readFile 函式接受一個 String(要讀取的檔案)並返回一個操作,該操作在執行時會生成該檔案的內容。有關類型別名的更多資訊,請參閱 型別宣告 章。


大多數 IO 函式是不言自明的。openFilehClose 函式分別開啟和關閉檔案。IOMode 引數確定開啟檔案的模式。hIsEOF 測試檔案結束。hGetCharhGetLine 分別從檔案讀取一個字元或一行。hGetContents 讀取整個檔案。getChargetLinegetContents 變數從標準輸入讀取。hPutChar 將一個字元列印到檔案;hPutStr 列印一個字串;hPutStrLn 列印一個字串,並在末尾新增一個換行符。沒有 h 字首的變數在標準輸出上工作。readFilewriteFile 函式讀取和寫入整個檔案,無需先開啟它。

bracket 函式來自 Control.Exception 模組。它有助於安全地執行操作。

bracket :: IO a -> (a -> IO b) -> (a -> IO c) -> IO c

考慮一個開啟檔案、向其中寫入一個字元,然後關閉檔案的函式。在編寫此類函式時,需要小心確保如果在某個點發生錯誤,檔案仍然能夠成功關閉。bracket 函式使這變得容易。它接受三個引數:第一個是在開始時要執行的操作。第二個是在結束時要執行的操作,無論是否出錯。第三個是在中間要執行的操作,該操作可能會導致錯誤。例如,我們的字元寫入函式可能看起來像

writeChar :: FilePath -> Char -> IO ()
writeChar fp c =
    bracket
      (openFile fp WriteMode)
      hClose
      (\h -> hPutChar h c)

這將開啟檔案,寫入字元,然後關閉檔案。但是,如果寫入字元失敗,hClose 仍然會執行,並且異常將在之後重新丟擲。這樣,您就不必太擔心捕獲異常和關閉所有控制代碼。

檔案讀取程式

[編輯 | 編輯原始碼]

我們可以編寫一個簡單的程式,允許使用者讀取和寫入檔案。介面誠然很差,並且它沒有捕獲所有錯誤(例如讀取不存在的檔案)。儘管如此,它應該提供一個關於如何使用 IO 的相當完整的示例。將以下程式碼輸入到“FileRead.hs”中,然後編譯/執行

import System.IO
import Control.Exception

main = doLoop

doLoop = do
  putStrLn "Enter a command rFN wFN or q to quit:"
  command <- getLine
  case command of
    'q':_ -> return ()
    'r':filename -> do putStrLn ("Reading " ++ filename)
                       doRead filename
                       doLoop
    'w':filename -> do putStrLn ("Writing " ++ filename)
                       doWrite filename
                       doLoop
    _            -> doLoop

doRead filename =
    bracket (openFile filename ReadMode) hClose
            (\h -> do contents <- hGetContents h
                      putStrLn "The first 100 chars:"
                      putStrLn (take 100 contents))

doWrite filename = do
  putStrLn "Enter text to go into the file:"
  contents <- getLine
  bracket (openFile filename WriteMode) hClose
          (\h -> hPutStrLn h contents)

這個程式做了什麼?首先,它發出簡短的指令字串並讀取命令。然後,它執行一個case在命令上切換,並首先檢查第一個字元是否為 `q.' 如果是,它將返回一個單位型別的返回值。

注意

return 函式是一個接受型別為 a 的值並返回型別為 IO a 的操作的函式。因此,return () 的型別為 IO ()


如果命令的第一個字元不是 `q,' 程式會檢查它是否為 `r' 後跟繫結到變數 filename 的一些字串。然後它會告訴你它正在讀取檔案,執行讀取並再次執行 doLoop。對 `w' 的檢查幾乎相同。否則,它匹配 _(萬用字元)並迴圈到 doLoop

doRead 函式使用 bracket 函式來確保讀取檔案時沒有任何問題。它以 ReadMode 模式開啟一個檔案,讀取其內容並列印前 100 個字元(take 函式接受一個整數 和一個列表,並返回列表的前 個元素)。

doWrite 函式請求一些文字,從鍵盤讀取它,然後將其寫入指定的檔案。

注意

doReaddoWrite 都可以更簡單地使用 readFilewriteFile,但它們以擴充套件的方式編寫,以顯示如何使用更復雜的函式。


該程式有一個主要問題:如果您嘗試讀取一個不存在的檔案或指定一些錯誤的檔名(例如 *\bs^#_@),它將崩潰。您可能認為 doReaddoWrite 中對 bracket 的呼叫應該解決這個問題,但事實並非如此。它們只捕獲主體內部的異常,而不會捕獲啟動或關閉函式(在本例中為 openFilehClose)內部的異常。要使其完全可靠,我們需要一種方法來捕獲由 openFile 丟擲的異常。

練習

編寫我們程式的變體,使其首先詢問使用者是否要從檔案讀取、寫入檔案或退出。如果使用者回覆“退出”,程式應該退出。如果他們回覆“讀取”,程式應該詢問他們檔名,然後將該檔案列印到螢幕上(如果檔案不存在,程式可能會崩潰)。如果他們回覆“寫入”,它應該詢問他們檔名,然後詢問他們要寫入檔案的文字,用“.” 標記完成。除了“.” 之外的所有內容都將寫入檔案。

例如,執行此程式可能會產生

Do you want to [read] a file, [write] a file, or [quit]?
read
Enter a file name to read:
foo
...contents of foo...
Do you want to [read] a file, [write] a file, or [quit]?
write
Enter a file name to write:
foo
Enter text (dot on a line by itself to end):
this is some
text for
foo
.
Do you want to [read] a file, [write] a file, or [quit]?
read
Enter a file name to read:
foo
this is some
text for
foo
Do you want to [read] a file, [write] a file, or [quit]?
blech
I don't understand the command blech.
Do you want to [read] a file, [write] a file, or [quit]?
quit
Goodbye!
華夏公益教科書