跳转到内容

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!
华夏公益教科书