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 ()
注意
FilePath
是String
的类型别名。因此,例如,readFile
函数接受一个String
(要读取的文件)并返回一个动作,该动作在运行时会生成该文件的内容。有关类型别名的更多信息,请参见类型声明章节。
大多数IO函数是不言自明的。openFile
和hClose
函数分别打开和关闭文件。IOMode
参数确定打开文件的模式。hIsEOF
测试文件结束。hGetChar
和hGetLine
分别从文件中读取一个字符或一行。hGetContents
读取整个文件。getChar
、getLine
和getContents
变体从标准输入读取。hPutChar
将一个字符打印到文件;hPutStr
打印一个字符串;hPutStrLn
在末尾打印一个带有换行符的字符串。没有h
前缀的变体作用于标准输出。readFile
和writeFile
函数读取和写入整个文件,无需首先打开它。
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
函数请求一些文本,从键盘读取它,然后将其写入指定的文件。
注意
doRead
和doWrite
都可以通过使用readFile
和writeFile
来简化,但它们是用扩展的方式编写的,以展示如何使用更复杂的函数。
该程序有一个主要问题:如果你尝试读取一个不存在的文件或指定一些错误的文件名,例如*\bs^#_@
,它将死亡。你可能会认为doRead
和doWrite
中的bracket
调用应该处理这个问题,但它们没有。它们只捕获主体内部的异常,而不是启动或关闭函数内部的异常(在本例中,分别为openFile
和hClose
)。为了使这完全可靠,我们需要一种方法来捕获由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!
|