跳轉到內容

Haskell/庫/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)

這個程式做什麼?首先,它發出一個簡短的指令字串並讀取一個命令。然後,它執行一個情況在命令上切換並首先檢查第一個字元是否為“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!
華夏公益教科書