在 Haskell 中编码和高效 IO



您好,我对将数据从String编码到ByteString所需的所有Haskell模块有点困惑,以实现高效写入。

我不明白您如何将Data.ByteString.Lazy转换为Data.ByteString.Char8,反之亦然。

我需要知道什么?因为我无法获得所有这些可能的用法组合....Data.ByteStringData.ByteString.LazyData.ByteString.Char8,然后是Data.Text.....我需要什么才能轻松有效地将字符串写入文件,反之亦然?(使用正确的编码)

P.S目前正在阅读现实世界的 Haskell 和我对所有这些模块都感到非常困惑。

这是路线图的一个镜头。

字符串和文本

你可能知道,HaskellString类型只是[Char]的类型同义词,其中Char是可以表示单个Unicode码位的数据类型。 这使得String成为表示文本数据的完美数据类型,除了一个小问题 - 作为盒装Char值的链接列表 - 它有可能非常低效。

text包中的Text数据类型解决了此问题。Text也像String一样,是Char值列表的表示,但它不是使用实际的Haskell列表,而是使用时间和空间有效的表示。 当您需要高效处理文本(Unicode)数据时,它应该是String的首选替代品。

与标准Haskell库中的许多其他数据类型一样,它有惰性和严格的变体。 两个变体具有相同的名称Text,但它们包含在单独的模块中,因此您可以执行以下操作:

import qualified Data.Text as TS
import qualified Data.Text.Lazy as TL

如果您需要在同一程序中同时使用TS.TextTL.Text变体。

变体之间的确切区别在 Data.Text 的文档中进行了描述。 简而言之,您应该默认使用严格版本。 您仅在两种情况下使用惰性版本。 首先,如果您打算一次处理一点点大的Text值,将其视为文本"流"而不是"字符串",那么惰性版本是一个不错的选择。 (例如,读取大型 CSV 数字文件的程序可能会将该文件读取为长延迟Text流,并将结果存储在有效的数字类型中,例如未装箱的DoubleVector,以避免将整个输入文本保留在内存中。 其次,如果你要从许多小块构建一个大的Text字符串,那么你不想使用严格的版本,因为它们的不变性意味着当你添加东西时,它们都需要被复制。 相反,您希望将惰性变体与Data.Text.Lazy.Builder中的函数一起使用。

字节字符串

另一方面,bytestring包中的ByteString数据类型是字节列表的有效表示形式。 就像Text[Char]的有效版本一样,你应该把ByteString看作是[Word8]的有效版本,其中Word8是Haskell类型,表示值为0-255的单个无符号数据字节。 等效地,您可以将ByteString视为表示要从文件中读取或写入文件的内存块或数据块,精确地按原样逐个字节。 它还具有懒惰和严格的口味:

import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as BL

并且使用这些变体的注意事项与Text的注意事项相似。

读取和写入文件

在Haskell程序中,通常在内部将Unicode字符串表示为StringText值。 但是,要从文件中读入或写出文件,需要将它们编码为字节序列并从字节序列中解码。

处理此问题的最简单方法是使用自动处理编码和解码的Haskell函数。 您可能知道,Prelude中已经有两个函数可以直接读取和写入字符串:

readFile :: FilePath -> IO String
writeFile :: FilePath -> String -> IO ()

此外,text中还有执行此操作的readFilewriteFile函数。 您可以在Data.Text.IOData.Text.Lazy.IO中找到版本。 它们看起来具有相同的签名,但一个在严格Text类型上运行,另一个在惰Text类型上运行:

readFile :: FilePath -> IO Text
writeFile :: FilePath -> Text -> IO ()

您可以判断这些函数正在自动执行编码和解码,因为它们返回并接受Text值,而不是ByteString值。 使用的默认编码将取决于操作系统及其配置。 在典型的现代Linux发行版上,它将是UTF-8。

或者,您可以使用bytestring包中的函数从文件中读取或写入原始字节(同样,延迟或严格版本,具体取决于模块):

readFile :: FilePath -> IO ByteString
writeFile :: FilePath -> ByteString -> IO ()

它们与text版本具有相同的名称,但您可以看到它们正在处理原始字节,因为它们返回并接受ByteString参数。 在这种情况下,如果要将这些ByteString用作文本数据,则需要自己解码或编码它们。 例如,如果ByteString表示文本的 UTF-8 编码版本,则Data.Text.Encoding(对于严格版本)或Data.Text.Lazy.Encoding(对于惰性版本)中的这些函数就是您要查找的:

decodeUtf8 :: ByteString -> Text
encodeUtf8 :: Text -> ByteString

字符 8 模块

现在,Data.ByteString.Char8Data.ByteString.Lazy.Char8中的模块是一个特例。 当纯 ASCII 文本使用几种"ASCII 保留"编码方案(包括 ASCII 本身、Latin-1 和其他 Latin-x 编码以及 UTF-8)之一进行编码时,事实证明,编码ByteString只是 Unicode 码位 0 到 127 的简单每字符一个字节的编码。 稍微一般地说,当文本以 Latin-1 编码时,编码ByteString只是 Unicode 代码点 0 到 255 的简单每字符一个字节的编码。 在这些情况下,并且仅在这些情况下,这些模块中的函数可以安全地用于绕过显式编码和解码步骤,只需将字节字符串直接视为 ASCII 和/或拉丁语 1 文本,即可自动将单个字节转换为 unicodeChar值并返回。

由于这些函数仅在特殊情况下有效,因此除在专用应用程序中外,通常应避免使用它们。

此外,正如评论中提到的,这些Char8模块中的ByteString变体与普通的严格和懒惰的ByteString变体没有任何不同;这些模块中的函数只是将它们视为Char值的字符串,而不是Word8值 - 数据类型是相同的, 只是功能界面不同。

总体战略

因此,如果您使用的是纯文本和操作系统的默认编码,只需使用Data.Text中的严格Text数据类型和Data.Text.IO中的(高效)IO 函数。 您可以使用惰性变体进行流处理或从小片段构建大字符串,也可以使用Data.Text.Read进行一些简单的解析。

在大多数情况下,您应该能够避免使用String,但是如果您发现需要来回转换,那么Data.Text(或Data.Text.Lazy)中的这些转换函数将很有用:

pack :: String -> Text
unpack :: Text -> String

如果您需要对编码进行更多控制,您仍然希望在整个程序中使用Text,除了在读取或写入文件的"边缘"处。 在这些边缘,使用Data.ByteString(或Data.ByteString.Lazy)中的 I/O 函数以及来自Data.Text.EncodingData.Text.Lazy.Encoding的编码/解码函数。

如果您发现需要混合使用严格变体和懒惰变体,请注意Data.Text.Lazy包含:

toStrict :: TL.Text -> TS.Text     -- convert lazy to strict
fromStrict :: TS.Text -> TL.Text   -- vice versa

Data.ByteString.Lazy包含ByteString值的相应函数:

toStrict :: BL.ByteString -> BS.ByteString
fromStrict :: BS.ByteString -> BL.ByteString

这取决于您正在处理的数据类型以及您计划如何处理这些数据。

如果要处理 Unicode 字符串,请使用 Text 包中的Text

如果不需要一次将所有数据读入内存,请使用该模块的Lazy版本。否则,整个数据将加载到单个数据结构中。

何时使用Data.ByteStringData.ByteString.Char8取决于您希望该字节串表示的内容:字节序列或 8 位字符序列。ByteString是一种数据结构,可用于存储字节序列(每种类型:Word8)或 8 位字符序列(每种类型:Char)。只有一种ByteString类型。

因为通常我们可能会处理二进制数据与基于字符的数据混合,所以如果我们在不同的模块中分别保留字节和字符的操作会很方便;这样,当我们需要处理基于字符的数据时,只需使用 Char8 模块中的操作。

最新更新