Haskell 提高 I/O 效率的技巧及资源控制。
二进制 I/O
Data.ByteString
:定义严格求值的 ByteString 类型,将一串二进制数据或文本数据用一个数组表示。适合不在意内存限制并要求随机存取的情况。
Data.ByteString.Lazy
:提供了 ByteString
的惰性类型,将一串数据分块组成列表,每块大小 64KB 。该方式惰性执行,对于体积较大的数据,惰性的 ByteString 类型会更好,其块大小针对现代 CPU L1缓存调整过,已处理过的、不会再被使用的流数据会被垃圾处理器快速回收。
- 以上两种类型均提供了和
String
类型兼容的接口函数,但元素类型为字节 Word8
,该类型在 Data.Word
模块中声明。
- 可使用
pack
函数将字节数组装载为 ByteString
:L.pack :: [Word.Word8] -> L.ByteString
。
ByteString
库提供了两个功能有限的 I/O 功能模块:Data.ByteString.Char8
和 Data.ByteString.Lazy.Char8
,其中的函数仅适用于单字节大小的 Char 值(ASCII和某些欧洲字符集,大于 255 会被截断)。这两个模块提供了较多方便的函数,如 readInt
、split
等。
正则
- Haskell 的正则通过
Text.Regex.Posix
模块提供,其中 =~
操作符是正则表达式匹配函数。其参数和返回值都使用了类型类,第一个参数是要被匹配的文本,第二个参数是正则表达式,每个参数都可以用为 String
或者 ByteString
类型。其返回值是多态的,但文本匹配的结果必须和被匹配的字符串一致,我们可以将 String
和 ByteString
组合,但结果类型必须和被匹配字符串一样。正则表达式可以使 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
提供,如 doesDirectoryExist
、doesFileExist
、getCurrentDirectory
、getDirectoryContents
等。
System.FilePath
主要处理文件路径,由两个模块构成:System.FilePath.Posix
和 System.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)和本声明。