这是一篇关于 Haskell 中字符串类型的指南,旨在帮助 Haskellers 提高对字符串类型的理解,包括初学者和经验丰富的开发者。它还提供了一个快速参考/备忘单,用于决定在给定情况下使用哪种字符串类型。
一、动机
2022 年作者实现了Abstract FilePath 提案,导致了诸如OsString
等新的字符串类型的出现。同时,作者还在Core Libraries Committee中,参与了基础 API 的监督工作,其中经常讨论字符串类型的问题。由于缺乏全面的文档,作者希望通过这篇博客文章来填补一些文档空白,并解释新引入的类型以及为什么作者认为我们没有太多的字符串类型。
二、Unicode 相关
- Unicode 标准:是一个识别和编码可见“字符”的标准,支持世界上所有主要的书写系统。术语“字符”含义模糊,重点关注核心概念,如 Unicode Code Point、UTF-32、UTF-16、UTF-8 等。
- Unicode Code Point:通过数值编码单个字符,范围从 0 到 10FFFF,形式为
U+0000
到U+10FFFF
,是 ASCII 字符集的扩展,能表达中文和其他非拉丁字符,有些字符由多个 Code Point 表示。 - UTF-32:固定长度为 32 位,能表示所有 Unicode 值,但浪费空间,不与 ASCII 兼容。
- UTF-16:可变宽度字符编码,常用在 Windows 上,U+0000 到 U+FFFF 直接用 2 字节表示,U+10000 到 U+10FFFF 用 surrogate 对表示。
- Unicode Scalar Values:Haskell 的
Char
类型本质上是一个 Code Point,能表示 UTF-16 中的 surrogate。Unicode 标准还定义了 Unicode Scalar Values 的概念,即除了 high-surrogate 和 low-surrogate 之外的任何 Unicode 代码点。 - UTF-8:可变宽度字符编码,常用于 Web APIs 和 Unix 系统,根据代码点范围用 1 到 4 个字节表示,与 ASCII 兼容,UTF-8 中 surrogate 范围的代码点被视为无效字节序列,只表达 Unicode Scalar Values。
三、Haskell 中的字符串类型
- String(不推荐):在 Haskell 标准中定义,是一个字符列表,作为中间表示在编码转换时有用,但对于大型文本效率低下,容易让用户困惑,不适合某些转换,只应用于小项目和原型。
- Text:用于人类可读的 Unicode 文本,内部使用 UTF-8 或 UTF-16 编码,有严格和懒惰两种变体。严格
Text
使用A.Array
存储字节数组,允许高效切片;懒惰Text
结构类似列表,可用于流式处理和增量处理。Text
不允许表示 surrogate,会将无效值转换为U+FFFD
。 - ShortText:用于大量小文本序列,是
text-short
包的一部分,定义为newtype ShortText = ShortText ShortByteString
,数据保证为有效 UTF-8,但不允许与text-icu
包一起使用,也不适合高效切片等操作。 - ByteString:低级别字节序列类型,从
bytestring
包中引入,使用固定内存,与 FFI 交互时更方便,效率高但缺乏文本处理功能,有严格和懒惰两种变体,严格ByteString
使用ForeignPtr Word8
存储字节数组,切片操作高效,懒惰ByteString
结构类似懒惰Text
。 - ShortByteString:
bytestring
包中的类型,与ByteString
API 相似,通常使用非固定内存,避免堆碎片,可用于 Unix 文件路径等,但不支持切片,没有懒惰变体。 - Bytes:
byteslice
包中的类型,类似于ShortByteString
,具有 0 拷贝切片功能,可构造为固定或非固定字节序列,有Chunks
变体,用于避免ByteArray
追加的开销。 - OsString、PosixString 和 WindowsString:相对较新的类型,用于抽象平台差异和操作系统 API 的编码,在处理文件路径等操作时更安全、类型更安全,内部使用
Word8
或Word16
数组表示。 - OsPath、PosixPath 和 WindowsPath:与
OsString
等对应的文件路径类型,是filepath
包的一部分,是类型同义词。 - CString 和 CStringLen:
base
中的低级 FFI 类型,用于与 C 代码交互。
四、Lazy 与 Strict
- Lazy:可以流式处理和增量处理,可能在常量空间中运行,允许垃圾回收器在切片后清理未使用的块,可表示无限数据流,时间复杂度稍高,可与懒惰 IO 一起使用。
- Strict:在时间复杂度上最有效,始终强制进入内存,开销比懒惰类型小。
五、Slicable 与 non-slicable
- Slicable:如
Text
,切片时不需要复制数据,节省memcpy
操作,但会增加内存开销(携带两个Int
字段),适合处理大型字符串或需要大量切片的情况。 - non-slicable:如
ShortText
,切片时会创建新的字节数组并复制数据,节省内存开销和一些运行时间接性,但不适合处理大型字符串或需要大量切片的情况。
六、Pinned vs unpinned
- Pinned memory:不能被垃圾回收器移动,用于与外国代码交互时避免复制数据,但会导致内存碎片,特别是对于大量小的固定内存数据。
- Unpinned memory:可以被垃圾回收器移动,避免内存碎片,但在与外国代码交互时需要复制数据。
七、Construction(构造字符串的方式)
- String literals:在 Haskell 文件中写入的字符串字面量将被编译器转换为
[Char]
。 - String Classes:如
IsString
类,允许将String
转换为其他兼容类型,但存在一些问题,如Text
的pack
函数不处理 surrogate,ByteString
/ShortByteString
的实例会截断为 8 位。 - OverloadedStrings:语言扩展,允许字符串字面量用于所有具有
IsString
实例的类型,但会使类型推断更困难,并且存在IsString
类的问题。 - QuasiQuoters:使用 Template Haskell 在编译时运行的表达式,用于更严格地验证字符串字面量,避免无效的 Unicode 序列,但会增加编译时间,并且可能与工具链存在问题。
八、Conversions(转换字符串类型)
- 从
String
到其他类型:需要指定编码,如toText
将String
转换为Text
,toByteString
将String
转换为ByteString
等。 - 从
Text
到其他类型:可复用处理String
的 API,如toString
将Text
转换为String
等。 - 从
ByteString
到其他类型:需要提供编码进行解码,如toString
将ByteString
转换为String
等,将ByteString
转换为OsString
较复杂,取决于字节序列的来源和用途。 - 从
ShortByteString
到其他类型:与ByteString
的转换类似,需要指定编码等。 - 从
OsString
到其他类型:OsString
提供了多种解码和编码函数,如encodeUtf
/decodeUtf
等,用于在不同平台和编码之间进行转换。
九、To JSON
Text
和String
已有ToJSON
实例,因为它们是 Unicode 且 JSON 要求 UTF-8。ByteString
、ShortByteString
和OsString
转换为 JSON 较复杂,取决于具体用途,如可转换为String
(假设 UTF-8 或 UTF-16)、base64 编码的String
或[Word8]
等。
十、A word on lazy IO
- 一些命名包的懒惰变体 API 用于读取和写入文件,但懒惰 IO 存在问题,如文件可能被锁定、操作系统文件描述符限制等,最好使用适当的流式库。
十一、Streaming(流式处理)
- 有许多流行的流式库,如
conduit
、streaming
、streamly
、pipes
等,可解决懒惰 IO 问题和[Char]
类型的效率问题,提供更高效的处理方式。 - 以
streamly
为例,提供了decodeUtf8
和encodeUtf8
等函数用于处理 Unicode 字符流,示例程序展示了如何使用streamly
读取文件的最后一个 Unicode 字符。
十二、Reflection(反思)
- What we should know:理解了 Unicode 的相关概念,包括字符编码、文本编码、不同的转换格式及其优缺点、
Char
类型的问题、grapheme clusters 的概念等,还了解了不同字符串类型的总结和各种字符串操作。 - Too many Strings:虽然有人认为 Haskell 有太多字符串类型,但实际上不同类型都有其特性和权衡,懒惰变体可能被流式库替代,但它们仍有其用途,新的字符串类型的创建具有挑战性,但可以通过 newtype 来实现。
- What are we missing:缺少 Unicode Scalar Values 和 Grapheme Clusters 类型,
text-icu
包有相关 API 但不直观,也没有良好的基于 base 的流式解决方案,作者的下一个项目可能是强类型文件路径。
十三、Special thanks to(特别感谢)
感谢了 Andrew Lelechenko、Jonathan Knowles、Mike Pilgrem、John Ericson 以及 streamly 维护者和其他相关包的维护者。
十四、Links and relevant stuff(链接和相关内容)
- String type blog posts:一系列关于 Haskell 字符串类型的博客文章,如Fixing Haskell filepaths等。
- Other blog posts:其他相关博客文章,如From conduit to streamly等。
- Interesting issues:一些有趣的问题,如Quit using ForeignPtr in favor of ByteArray#。
- String types not discussed here:一些未在本文中讨论的字符串类型,如monoid-subclasses等。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。