Строгие и ленивые
Байтовые строки бывают двух видов: строгие и ленивые. Строгие байтовые строки объявлены в модуле Data.ByteString, и они полностью не ленивые. Не используется никаких «обещаний», строгая строка байтов представляет собой последовательность байтов в массиве. Подобная строка не может быть бесконечной. Если вы вычисляете первый байт из строгой строки, вы должны вычислить её целиком. Положительный момент – меньше накладных расходов, поскольку не используются «обещания». Отрицательный момент – такие строки заполнят память быстрее, так как они считываются целиком.
Второй вид байтовых строк определён в модуле Data.ByteString. Lazy. Они ленивы – но не настолько, как списки. Как мы говорили ранее, в списке столько же «обещаний», сколько элементов. Вот почему это может сделать его медленным для некоторых целей. Ленивые строки байтов применяют другой подход: они хранятся блоками размером 64 Кб. Если вы вычисляете байт в ленивой байтовой строке (печатая или другим способом), то будут вычислены первые 64 Кб. После этого будет возращено обещание вычислить остальные блоки. Ленивые байтовые строки похожи на список строгих байтовых строк размером 64 Кб. При обработке файла ленивыми байтовыми строками файл будет считываться блок за блоком. Это удобно, потому что не вызывает резкого увеличения потребления памяти, и 64 Кб, вероятно, влезет в L2 – кэш вашего процессора.
Если вы посмотрите документацию на модуль Data.ByteString. Lazy, то увидите множество функций с такими же именами, как и в модуле Data.List, только в сигнатурах функций будет указан тип ByteString вместо [a] и Word8 вместо a. Функции в этом модуле работают со значениями типа ByteString так же, как одноимённые функции – со списками. Поскольку имена совпадают, нам придётся сделать уточнённый импорт в скрипте и затем загрузить этот скрипт в интерпретатор GHCi для того, чтобы поэкспериментировать с типом ByteString.
import qualified Data.ByteString.Lazy as B
import qualified Data.ByteString as S
Модуль B содержит ленивые строки байтов и функции, модуль S – строгие. Главным образом мы будем использовать ленивую версию.
Функция pack имеет сигнатуру pack :: [Word8] –> ByteString. Это означает, что она принимает список байтов типа Word8 и возвращает значение типа ByteString. Можно думать, будто функция принимает ленивый список и делает его менее ленивым, так что он ленив только блоками по 64 Кб.
Что за тип Word8? Он похож на Int, но имеет значительно меньший диапазон, а именно 0 – 255. Тип представляет собой восьми битовое число. Так же как и Int, он имеет экземпляр класса Num. Например, мы знаем, что число 5 полиморфно, а значит, оно может вести себя как любой числовой тип. В том числе – принимать тип Word8.
ghci> B.pack [99,97,110]
Chunk "can" Empty
ghci> B.pack [98..120]
Chunk "bcdefghijklmnopqrstuvwx" Empty
Как можно видеть, Word8 не доставляет много хлопот, поскольку система типов определяет, что числа должны быть преобразованы к нему. Если вы попытаетесь использовать большое число, например 336, в качестве значения типа Word8, число будет взято по модулю 256, то есть сохранится 80.
Мы упаковали всего несколько значений в тип ByteString; они уместились в один блок. Значение Empty – это нечто вроде [] для списков.
Если нужно просмотреть байтовую строку байт за байтом, её нужно распаковать. Функция unpack обратна функции pack. Она принимает строку байтов и возвращает список байтов. Вот пример:
ghci> let by = B.pack [98,111,114,116]
ghci> by
Chunk "bort" Empty
ghci> B.unpack by
[98,111,114,116]
Вы также можете преобразовывать байтовые строки из строгих в ленивые и наоборот. Функция fromChunks принимает список строгих строк и преобразует их в ленивую строку. Соответственно, функция toChunks принимает ленивую строку байтов и преобразует её в список строгих строк.
ghci> B.fromChunks [S.pack [40,41,42], S.pack [43,44,45], S.pack [46,47,48]]
Chunk "()*" (Chunk "+,–" (Chunk "./0" Empty))
Это полезно, если у вас есть множество маленьких строгих строк байтов и вы хотите эффективно обработать их, не объединяя их в памяти в одну большую строгую строку.
Аналог конструктора : для строк байтов называется cons. Он принимает байт и строку байтов и помещает байт в начало строки.
ghci> B.cons 85 $ B.pack [80,81,82,84]
Chunk "U" (Chunk "PQRT" Empty)
Модули для работы со строками байтов содержат большое количество функций, аналогичных функциям в модуле Data.List, включая следующие (но не ограничиваясь ими): head, tail, init, null, length, map, reverse, foldl, foldr, concat, takeWhile, filter и др.
Есть и функции, имя которых совпадает с именем функций из модуля System.IO, и работают они аналогично, только строки заменены значениями типа ByteString. Например, функция readFile в модуле System.IO имеет тип
readFile :: FilePath –> IO String
а функция readFile из модулей для строк байтов имеет тип
readFile :: FilePath –> IO ByteString
ПРИМЕЧАНИЕ. Обратите внимание, что если вы используете строгие строки и выполняете чтение файла, он будет считан в память целиком! При использовании ленивых байтовых строк файл будет читаться аккуратными порциями.