目录
文件操作
导入 Z.IO.Filesystem
以使用 Z-IO 的文件系统模块:
import qualified Z.IO.FileSystem as FS
在需要将文件直接导入内存时,可以使用以下函数:
readFile :: HasCallStack => CBytes -> IO Bytes
readTextFile :: HasCallStack => CBytes -> IO Text
writeFile :: HasCallStack => CBytes -> Bytes -> IO ()
writeTextFile :: HasCallStack => CBytes -> Text -> IO ()
此处的 CBytes
是 Z.Haskell 所使用的路径类型,Bytes
和 Text
则分别代表内容的二进制和文本编码。在 Z-Data 一节可以查阅它们的相关文档。函数 readTextFile
和 writeTextFile
都假设读写的内容为 UTF-8 编码。
> FS.writeTextFile "./test_file" "hello world!"
> FS.readFile "./test_file"
[104,101,108,108,111,32,119,111,114,108,100,33]
> FS.readTextFile "./test_file"
"hello world!" -- “你好,世界!”
资源管理
现在让我们看一个更复杂的函数:
initFile :: CBytes
-> FileFlag -- ^开启文件时传入的标志参数,例如:'O_CREAT' 和 'O_RDWR'
-> FileMode -- ^ 设置文件模式(权限和粘滞位)
-- 但只有在文件被已被创建的情况下作用,参考 'DEFAULT_FILE_MODE'
-> Resource File
类型 FileFlag
和 FileMode
是控制打开文件时行为的位常数,如我们是否有读或写的权限、或者当没有新文件时是否会创建新文件。要查阅更多这样的常量可以参考 Hackage 文档的本节。特别地,函数 initFile
返回类型为 Resource File
而不是 IO File
。类型 Resource
在模块 Z.IO.Resource
被定义,连同一些使用它的函数:
withResource :: HasCallStack
=> Resource a -- ^ 用于管理资源的记录(Record)类型
-> (a -> IO b) -- ^ 使用给定资源的工作函数
-> IO b
withResource' :: HasCallStack
=> Resource a -- ^ 用于管理资源的记录类型
-> (a -> IO () -> IO b)
-- ^ 第二个参数用于在过早的关闭行为中关闭资源
-> IO b
函数 withResource
会自动处理好资源的开启与使用完毕或抛出异常后的清理。因此你只需要传递需要作用在资源上的工作函数。现在利用它们再读一次先前创建的文件:
import Z.IO -- 本模块重导出了模块 Z.IO.Resource 和其它的常用定义
import qualified Z.IO.FileSystem as FS
withResource (FS.initFile "./test_file" FS.O_RDWR FS.DEFAULT_FILE_MODE) $ \ file -> do
bi <- newBufferedInput file
printStd =<< readLine bi
函数 initFile
本身不会开启目标文件,它只记录下了如何去开启和关闭目标文件。要处理文件或使用 Z 中的其它资源(Resource a
),使用 withResource
函数来开启并关闭它们。这是利用 Z 进行资源管理的一贯风格。
类型 Resource
是 Monad
的一个实例,因此安全地组合资源的操作得到了简化。例如,比起写下面的样版代码:
withResource initRes1 $ \ res1 ->
withResource initRes2 $ \ res2 ->
withResource initRes3 $ \ res3 ->
... res1 ... res2 ... res3
复用和组合性更强的写法是可以定义一个组合的 Resource
:
initRes123 :: Resource (Res1, Res2, Res3)
initRes123 = do
res1 <- initRes1
res2 <- initRes2
res3 <- initRes3
return (res1, res2, res3)
于是使用 withResource initRes123 $ \ (res1, res2, res3) -> ...
将会依次开启资源 res1
、res2
和 res3
,在使用完后以相反顺序关闭它们。由于类型 Resource
是类型类 MonadIO
的实例,可以用函数 liftIO
来叠加 Resource
内的 IO
操作(注意到 withResource :: (MonadMask m, MonadIO m, HasCallStack) => Resource a -> (a -> m b) -> m b
)。
initRes123 :: Resource (Res1, Res2)
initRes123 = do
res1 <- initRes1
res2Param <- liftIO $ ... res1 ...
res2 <- initRes2 res2Param
return (res1, res2)
被提升后的 IO
操作将在资源开启过程中进行。
缓冲式输入输出(Buffered I/O)
上文出现过的 newBufferedInput
与 readLine
函数都是模块 Z.IO.Buffered
的一部分(这些定义在模块 Z.IO
被重导出了)在 Z-IO 中,许多输入-输出设备(IO Devices),包括上文中的类型 File
在内,都是类型类 Input
与 Output
的实例。如果一个类型同时是它们的实例,它自动也是 IODev
的实例。
class Input i where
readInput :: HasCallStack => i -> Ptr Word8 -> Int -> IO Int
class Output o where
writeOutput :: HasCallStack => o -> Ptr Word8 -> Int -> IO ()
type IODev io = (Input io, Output io)
函数 readInput
和 writeOutput
都直接作用在指针上,因此不便于直接使用。一般用类型为 BufferedInput
与 BufferedOutput
的元素一起来使用带自动管理的缓冲式输入输出。
newBufferedInput :: Input i => i -> IO BufferedInput
newBufferedOutput :: Output o => o -> IO BufferedOutput
newBufferedIO :: IODev dev => dev -> IO (BufferedInput, BufferedOutput)
在模块 Z.IO.Buffered
中有许多函数作用于 BufferedInput
或 BufferedOutput
。例如,可以用它们实现一个文件计词器:
import Z.IO
import qualified Z.IO.FileSystem as FS
import qualified Z.Data.Vector as V
main :: IO ()
main = do
-- 从命令行读取需要处理文件的路径
(_:path:_) <- getArgs
withResource (FS.initFile path FS.O_RDWR FS.DEFAULT_FILE_MODE) $ \ file -> do
bi <- newBufferedInput file
printStd =<< loop bi 0
where
loop :: BufferedInput -> Int -> IO Int
loop input !wc = do
-- 读取单行并丢弃换行符
line <- readLine input
case line of
Just line' ->
loop input (wc + length (V.words line'))
_ -> return wc
下面是一张有关 Z-IO 中缓冲式输入输出的速查表,首先介绍 BufferedInput
:
-- | 从输入设备中获取一个分块。
readBuffer :: HasCallStack => BufferedInput -> IO Bytes
-- | 回推一个未被消耗的分快。
unReadBuffer :: HasCallStack => Bytes -> BufferedInput -> IO ()
-- | 读恰好 n 个字节,如果在读完即少于 n 字节出遇到了文件终止符(EOF)则抛出异常。
readExactly :: HasCallStack => Int -> BufferedInput -> IO Bytes
-- | 读至魔法字(Magic Bytes)停止,连同魔法字本身一起返回所读的内容。
-- 注:魔法字又称为文件签名
-- /----- readToMagic ------ \ /----- readToMagic ------\ ...
-- +------------------+--------+-----------------+--------+
-- | ... | 魔法字 | ... | 魔法字 | ...
-- +------------------+--------+-----------------+--------+
readToMagic :: HasCallStack => Word8 -> BufferedInput -> IO Bytes
-- | 读至换行符('\n' 或 '\r\n'),不连同换行符本身返回所读的内容。
-- /--- readLine ----\ 丢弃 /--- readLine ---\ 丢弃 / ...
-- +------------------+---------+------------------+---------+
-- | ... | \r\n/\n | ... | \r\n/\n | ...
-- +------------------+---------+------------------+---------+
readLine :: HasCallStack => BufferedInput -> IO (Maybe Bytes)
-- | 读取输入中的所有分块。
readAll :: HasCallStack => BufferedInput -> IO [Bytes]
readAll' :: HasCallStack => BufferedInput -> IO Bytes
有关以下函数的说明可参考 Z-Data 部分中 Parser 与 Builder 一节。
-- | 用给定的 Parser 并处理获取的输入。
readParser :: HasCallStack => Parser a -> BufferedInput -> IO a
-- | 用 'ParseChunks' 处理获取的输入。参见 Z-Data 部分中 Parser 与 Builder 一节。
readParseChunks :: (Print e, HasCallStack) => ParseChunks IO Bytes e a -> BufferedInput -> IO a
有关 BufferedOutput
的内容则相对简单:
-- | 向输出设备写入一个分块。
writeBuffer :: HasCallStack => BufferedOutput -> Bytes -> IO ()
-- | 向输出设备写入一个用给定 Builder 处理好的分块。
writeBuilder :: HasCallStack => BufferedOutput -> Builder a -> IO ()
-- | 将缓冲区冲入输出设备。
flushBuffer :: HasCallStack => BufferedOutput -> IO ()
-- | 向输出设备写入一个分块并将缓冲区冲入该输出设备。
writeBuffer' :: HasCallStack => BufferedOutput -> Bytes -> IO ()
有关文件路径的注解
模块 Z.IO.FileSystem
中的许多操作函数,例如 seek
、mkdtemp
和 rmdir
等,都可以被视作 UNIX 系统调用的移植,C 或 C++ 用户可能会对它们感到熟悉。在 Z 中基本的文件路径类型是 CBytes
。它由 GHC 的堆管理,是一个以 \NUL
中止的字节数组。
我们总是假设 CBytes
元素的内容是 UTF-8 编码的,但事实上并是不总如愿。不同平台间文件路径的处理方式也有一些差异,例如 Windows 与 UNIX 的分隔符就不相同。模块 Z.IO.FileSystem.FilePath
(在模块 Z.IO.FileSystem
中被重导出)处理了这些问题。例如,比起像这样手动拼接文件路径(错误的做法):
let p = "foo" <> "/" <> "bar"
应该使用函数:
import qualified Z.IO.FileSystem as FS
let p = "foo" `FS.join` "bar"
-- "foo" `FS.join` "../bar" 将会得到 "bar" 而不是不符合预期的 "foo/../bar"