跳转到内容

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