Haskell 中的高效 I/O

Haskell 提高 I/O 效率的技巧及资源控制。

二进制 I/O

  • Data.ByteString :定义严格求值的 ByteString 类型,将一串二进制数据或文本数据用一个数组表示。适合不在意内存限制并要求随机存取的情况。
  • Data.ByteString.Lazy:提供了 ByteString 的惰性类型,将一串数据分块组成列表,每块大小 64KB 。该方式惰性执行,对于体积较大的数据,惰性的 ByteString 类型会更好,其块大小针对现代 CPU L1缓存调整过,已处理过的、不会再被使用的流数据会被垃圾处理器快速回收。
  • 以上两种类型均提供了和 String 类型兼容的接口函数,但元素类型为字节 Word8,该类型在 Data.Word 模块中声明。
  • 可使用 pack 函数将字节数组装载为 ByteStringL.pack :: [Word.Word8] -> L.ByteString
  • ByteString 库提供了两个功能有限的 I/O 功能模块:Data.ByteString.Char8Data.ByteString.Lazy.Char8,其中的函数仅适用于单字节大小的 Char 值(ASCII和某些欧洲字符集,大于 255 会被截断)。这两个模块提供了较多方便的函数,如 readIntsplit 等。

正则

  • Haskell 的正则通过 Text.Regex.Posix 模块提供,其中 =~ 操作符是正则表达式匹配函数。其参数和返回值都使用了类型类,第一个参数是要被匹配的文本,第二个参数是正则表达式,每个参数都可以用为 String 或者 ByteString 类型。其返回值是多态的,但文本匹配的结果必须和被匹配的字符串一致,我们可以将 StringByteString 组合,但结果类型必须和被匹配字符串一样。正则表达式可以使 String 或者 ByteString,没有限制。根据返回类型签名不同,返回结果也有区别:
    • Bool:字符串和正则式是否匹配
    • Int:正则式在字符串中成功匹配的次数
    • (Int, Int):格式为(首次匹配在字符串中的偏移量,首次匹配结果的长度),偏移量为 -1 时表示字符串和正则式不匹配
    • [(Int, Int)]:得到所有匹配子串的(偏移量,匹配长度),列表为空代表无匹配
    • String:得到第一个匹配的子串,或者无匹配的空字符串
    • [[String]]:返回由所有匹配的字符串组成的列表
    • (String, String, String):匹配成功时为(首次匹配之前的部分,首次匹配的子串,首次匹配之后的部分),匹配失败时为(整个字符串,””,””)
    • (String, String, String, [String]):前三个元素和三元组相同,第四个元素是包含了模式中所有分组的列表
  • 正则函数可配合其他函数如 getAllTextMatchs 来获取更多结果:
1
2
ghci> ("foo buot" =~ "(oo|uo)") :: [String]
["oo", "uo"]

文件系统路径

  • Haskell 的文件系统处理函数主要由 System.Directory 提供,如 doesDirectoryExistdoesFileExistgetCurrentDirectorygetDirectoryContents 等。
  • System.FilePath 主要处理文件路径,由两个模块构成:System.FilePath.PosixSystem.FilePath.Windows,二者接口完全相同,适配平台不同。包含函数如:
    • getSearchPath:获得 $PATH 环境变量内容
    • </>:将两个字符串用 / 合为一个路径
    • <.>:将后缀名结合,等价于 addExtension
    • -<.>:去掉后缀名并添加一个新的后缀名,等价于 replaceExtension
    • dropTrailingPathSeparator:去掉文件路径后的分隔符,如 /
    • splitFileName:返回将路径切割为父级目录和文件名的二元组

常见 I/O 异常处理

  • 异常处理的几个常用函数包含在 Control.Exception 中。
  • handle :: (Exception -> IO a) -> IO a -> IO a 接收的第一个参数是一个函数,该函数接受一个异常值并且返回 IO Monad,第二个参数是可能抛出异常的 IO Monad。当第二个 IO Monad 执行出现异常时,作为 handle 第一个参数的函数会接收产生的异常值,并返回自己的 IO Monad;当第二个参数执行无异常时, handle 返回值与第二个参数相同。
  • handle 的使用中可使用 const 忽略传入的异常。const 接收两个参数,无论第二个参数是什么都返回第一个参数:
1
2
handle (const (return [])) (code_may_cause_exception)
const (return []) :: Monad m => b -> m [t]
  • 也可以使用 finally 捕获异常,其类型签名为 finally:: IO a-> IO b -> IO a,无论第一个 IO Monad 成功或失败,第二个 IO Monad 都会执行。
  • bracket 可以看作 Haskell 中的 defer:如果你试图获取一个资源,对该资源做一些操作,并想在操作结束后释放这个资源,则可以使用 bracket 来保证最终资源的释放。
    bracket 接收三个参数,第一个参数用于资源的获取,它的返回值会传给第二、三个参数,而第二个参数对应资源的释放,第三个参数为对资源的操作,它的返回值也是整个 bracket 函数的返回值。例如:
1
2
3
4
5
bracket :: IO a -> (a -> IO b) -> (a -> IO c) -> IO c
bracket
(openFile "filename" ReadMode)
(hClose)
(\fileHandle -> do { ... })
  • 当不需要获取第一个参数的返回值时,或这几个操作之间并无关系,使用 bracket_ :: IO a -> IO b -> IO c -> IO c 替代 bracket
  • 如果仅希望释放操作在执行操作出现异常时调用,则使用 bracketOnError:: IO a -> (a -> IO b) -> (a -> IO c) -> IO c

原创作品,允许转载,转载时无需告知,但请务必以超链接形式标明文章原始出处(http://blog.forec.cn/2016/11/30/efficient-haskell-io/) 、作者信息(Forec)和本声明。

分享到