Link Search Menu Expand Document

目录

  1. 目录
  2. 文件操作
  3. 资源管理
  4. 缓冲式输入输出(Buffered I/O)
  5. 有关文件路径的注解

文件操作

导入 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 所使用的路径类型,BytesText 则分别代表内容的二进制和文本编码。在 Z-Data 一节可以查阅它们的相关文档。函数 readTextFilewriteTextFile 都假设读写的内容为 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

类型 FileFlagFileMode 是控制打开文件时行为的位常数,如我们是否有读或写的权限、或者当没有新文件时是否会创建新文件。要查阅更多这样的常量可以参考 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 进行资源管理的一贯风格。

类型 ResourceMonad 的一个实例,因此安全地组合资源的操作得到了简化。例如,比起写下面的样版代码:

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) -> ... 将会依次开启资源 res1res2res3,在使用完后以相反顺序关闭它们。由于类型 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)

上文出现过的 newBufferedInputreadLine 函数都是模块 Z.IO.Buffered 的一部分(这些定义在模块 Z.IO 被重导出了)在 Z-IO 中,许多输入-输出设备(IO Devices),包括上文中的类型 File 在内,都是类型类 InputOutput 的实例。如果一个类型同时是它们的实例,它自动也是 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)

函数 readInputwriteOutput 都直接作用在指针上,因此不便于直接使用。一般用类型为 BufferedInputBufferedOutput 的元素一起来使用带自动管理的缓冲式输入输出。

newBufferedInput :: Input i => i -> IO BufferedInput
newBufferedOutput :: Output o => o -> IO BufferedOutput
newBufferedIO :: IODev dev => dev -> IO (BufferedInput, BufferedOutput)

在模块 Z.IO.Buffered 中有许多函数作用于 BufferedInputBufferedOutput。例如,可以用它们实现一个文件计词器:

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 中的许多操作函数,例如 seekmkdtemprmdir 等,都可以被视作 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"