۲۷ - ۹تایپهای نوشتاری
عنوان این بخش یه کم اشتباهه، چون راجع به دوتا تایپِ نوشتاری و یک تایپ برای ارائهی تسلسل ِ بایتها صحبت میکنیم. اول String، Text، و ByteString رو اجمالاً توضیح میدیم:
String
String رو میشناسین. تایپِ مستعار برای یه لیست از Char ِه، اما در پشتِ پرده به همون سادگیِ یه لیستِ Char نیست.
یکی از مزایای استفاده از String سادگیه، توضیح دادنشون هم آسونه. برای بیشترِ مثالها و برنامههای بازیچهای مناسباند.
اما مثل خودِ لیست، Stringها میتونن بینهایت باشن. حتی برای Stringهای خیلی بزرگ هم ممکنه سریع مصرفِ حافظه از کنترل خارج بشه. دیگه اینکه اندیسگیری ِ حرفبهحرف توی String اصلاً بهینه نیست. زمان لازم برای یک جستجو متناسب با طولِ لیست زیاد میشه.
Text
این تایپ از کتابخونه ِ text (روی Hackage) میاد. بهترین کاربردش برای وقتهاییه که متن ساده دارین، و میخواین دادهتون رو بهینهتر ذخیره کنین – بخصوص وقتی مصرفِ حافظه مهمه.
در فصلهای قبل یه کم از این تایپ استفاده کردیم (اون موقع که با OverloadedStrings بازی میکردیم). مزایای این تایپ اینها هستن:
ارائهی فشرده در حافظه؛ و
اندیسگیری ِ بهینه در نوشته.
اما Text با UTF-16 کدبندی شده، و با توجه به محبوبیتِ UTF-8، چیزی نیست که اکثرِ مردم انتظارش رو داشته باشن. در دفاع از Text، معمولاً UTF-16 به خاطر رابطهی بهتری که با حافظهی نهان داره (یا cache-friendly ِه) سرعتِ بیشتری داره.
شکمی پیش نرین، اندازه بگیرین
گفتیم Text ارائهی فشردهتری توی حافظه داره، ولی فکر میکنین پروفایلِ حافظه ِ برنامهی زیر چطور بشه؟ اول زیاد، بعد کم، یا اول کم بعد زیاد؟
module Main where
import Control.Monad.Primitive
import qualified Data.Text as T
import qualified Data.Text.IO as TIO
import qualified System.IO as SIO
-- رو متناسب "/usr/share/dict/words" آدرس
-- .با دستگاه خودتون تغییر بدین
dictWords :: IO String
dictWords =
SIO.readFile "/usr/share/dict/words"
dictWordsT :: IO T.Text
dictWordsT =
TIO.readFile "/usr/share/dict/words"
main :: IO ()
main = do
replicateM_ 1000 (dictWords >>= print)
replicateM_ 1000
(dictWordsT >>= TIO.putStrLn)مشکل اینجاست که هربار اجراییه ِ IO اجبار میشه، Text کلِ فایل رو توی حافظه بارگذاری میکنه. اما عملیات ِ readFile برای String تنبل ِه و فقط همون مقداری از فایل رو که برای چاپ به stdout لازمه میبره توی حافظه. راهِ صحیح برای پردازشِ تدریجیِ دادهها، استفاده از جریان ِه،* البته چیزی نیست که با جزئیات بهش بپردازیم. ولی اینطوری میشه کدِمون رو تنبلتر کنیم:
-- این رو به ماژولی که برای پروفایلینگ
-- درست کردین اضافه کنین. Text و String
import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.IO as TLIO
dictWordsTL :: IO TL.Text
dictWordsTL =
TLIO.readFile "/usr/share/dict/words"
main :: IO ()
main = do
replicateM_ 1000 (dictWords >>= print)
replicateM_ 1000
(dictWordsT >>= TIO.putStrLn)
replicateM_ 1000
(dictWordsTL >>= TLIO.putStrLn)حالا مصرف حافظه بعد از یه رشد زیاد اون وسط، دوباره افت میکنه؛ دلیلش اینه که فقط مقداری که برای چاپِ متن لازمه میره توی حافظه، و در طول مسیر هم حافظه رو آزاد میکنه. این چندتا (فقط چندتا، نه همه) از مزایای جریاندهی ِه، ما به شدت توصیه میکنیم بجای اتکا به یه API برای IO ِ تنبل، از جریاندهی استفاده کنین.
ByteString
این یه نوشته یا متن نیست. حداقل، "الزاماً" این دوتا نیست. ByteString ها تسلسلی از بایتها هستن که (بطور غیرمستقیم) با یه بردار از مقادیرِ Word8 ارائه میشن. متن توی کامپیوتر متشکل از بایتهاست، اما باید به نحوِ بخصوصی کدبندی شده باشه تا متن بشن. کدبندی ِ متن میتونه ASCII، UTF-8، UTF-16، یا UTF-32 باشه (معمولاً UTF-8 یا UTF-16). تایپِ Text داده رو با UTF-16 کدبندی میکنه، یکی از دلایلش عملکرد ِ بهتره. معمولاً اگه با هر بار خوندن از حافظه، تیکههای بزرگتری از داده خونده بشن، سرعت بالا میره، در نتیجه این ۱۶ بیت برای هر کاراکتر در کدبندی ِ UTF-16 در اکثرِ موارد بهتر عمل میکنه.
مزیتِ اصلیِ ByteString اینه که خیلی راحت میشه ازش استفاده کرد (به واسطهی OverloadedStrings)، ولی بجای اینکه فقط متن باشه، بایت ِه. همین باعث میشه فضای مسئلهی خیلی بزرگتری رو نسبت به متن ِ خالی پوشش بده.
البته جنبهی دیگهش اینه که دادههای بایتیای که متن ِ درستی نیستن هم شامل میشه؛ که اگه نخواین بایتهای غیرِمتنی ِ داخلِ دادهتون بیان، این مورد جزء معایبِ ByteString محسوب میشه.
مثالهای ByteString
این مثال نشون میده که مقادیرِ ByteString همیشه متن نیستن:
{-# LANGUAGE OverloadedStrings #-}
module BS where
import qualified Data.Text.IO as TIO
import qualified Data.Text.Encoding as TE
import qualified Data.ByteString.Lazy as BL
-- https://hackage.haskell.org/package/zlib
import qualified
Code.Compression.GZip as GZipاینجا از فرمتِ فشردهسازی ِ gzip استفاده میکنیم تا دادهای درست کنیم که حاویِ بایتهایی با کدبندی ِ نامعتبر برای متن باشه.
input :: BL.ByteString
input = "123"
compressed :: BL.ByteString
compressed = BL.compress inputماژول ِ GZip انتظارِ ByteString ِ تنبل داره (احتمالاً به خاطر کارکردِ بهتر برای جریاندهی).
main :: IO ()
main = do
TIO.putStrLn $ TE.decodeUtf8 (s input)
TIO.putStrLn $
TE.decodeUtf8 (s compressed)
where s = BL.toStrictماژول ِ کدبندی از کتابخونه ِ text (م. Data.Text.Encoding) انتظارِ ByteString ِ اکید داره، پس قبل از کدگشایی، اول باید اکیدِش کنیم. اینجا کدگشایی ِ دوم شکست میخوره، چون بایتی که بهش داده شده با کدبندی ِ متنی سازگاری نداره.
تلههای ByteString
ممکنه پیش خودتون فکر کنین: "میخوام یه String رو به ByteString تبدیل کنم!" کاملاً منطقیه، اما خیلی از هسکلنویسها به اشتباه از ماژول ِ Char8 از کتابخونه ِ bytestring استفاده میکنن؛ در واقع چیزی که میخوان، اون نیست. ماژول ِ Char8 فقط برای راحتی و کار با دادههای بایتی و ASCII طراحی شده.* برای یونیکد کار نمیکنه، و اگه کوچکترین احتمالِ وجودِ دادهی یونیکد هست، نباید ازش استفاده بشه. برای مثال:
module Char8ProllyNotWhatYouWant where
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import qualified Data.ByteString as B
import qualified Data.ByteString.Char8 as B8
-- utf8-string
import qualified Data.ByteString.UTF8 as UTF8
-- کدبندی دستی یونیکد برای متن ژاپنی.
-- در UTF8 اجازهی GHC Haskell
-- فایلهای منبع رو میده.
s :: String
s = "\12371\12435\12395\12385\12399\12289\
\20803\27671\12391\12377\12363\65311"
utf8ThenPrint :: B.ByteString -> IO ()
utf8ThenPrint =
putStrLn . T.unpack . TE.decodeUtf8
throwsException :: IO ()
throwsException =
utf8ThenPrint (B8.pack s)
bytesByWayOfText :: B.ByteString
bytesByWayOfText = TE.encodeUtf8 (T.pack s)
-- برامون انجام بده utf8-string بذاریم
libraryDoesTheWork :: B.ByteString
libraryDoesTheWork = UTF8.fromString s
thisWorks :: IO ()
thisWorks = utf8ThenPrint bytesByWayOfText
alsoWorks :: IO ()
alsoWorks = utf8ThenPrint libraryDoesTheWorkاز اونجا که ASCII با ۷ بیت، و Char8 با ۸ بیت کار میکنه، میشه از بیتِ هشتم برای کاراکترهای لاتین-۱ استفاده کرد. ولی به خاطر اینکه معمولاً دادهها ِ Char8 به کدبندیهایی مثل UTF-8 و UTF-16 تبدیل میشن (که از هشتمین بیت به نحوِ متفاوتی استفاده میکنن)، پس خیلی عاقلانه نیست.
اول کدی که شاملِ یونیکد ِه و سعی میکنه با استفاده از ماژول ِ Char8 یه ByteString چاپ کنه رو اجرا میکنیم:
Prelude> throwsException
*** Exception: Cannot decode byte '\x93':
Data.Text.Internal.Encoding.decodeUtf8:
Invalid UTF-8 streamبا استفاده از تابع ord از Data.Char، میشه مقدار Int ِ بایتِ یه کاراکتر رو پیدا کنین:
Prelude> import Data.Char (ord)
Prelude> :t ord
ord :: Char -> Int
Prelude> ord 'A'
65
Prelude> ord '\12435'
12435مثال دوم ('\12435') به نظر واضح میاد، این تابع برای دادههایی که ارائهی مخصوص دارن کاربردیتره. میتونین از سایتهایی که زبانشون انگلیسی نیست داده ِ نمونه برای تست پیدا کنین.
م.
اگه سیستم عاملتون تایپِ فارسی داره، میتونین ارائهی عددیِ حروف فارسی هم پیدا کنین!
Prelude> ord 'م'
1605
Prelude> ord '\1587'
1587
Prelude> ord 'س'
1587میتونیم به کمک ترتیبِ کاراکترها، اولین کاراکتری که Char8 رو شکست میده پیدا کنیم:
Prelude> let xs = ['A'..'\12435']
Prelude> let cs = map (:[]) xs
Prelude> mapM_ (utf8ThenPrint . B8.pack) cs
... یه مشت خروجی ...پس باید بفهمیم چه چیزی بعد از تیلدا و کاراکترِ \DEL میاد:
... بعد از یه کم سعی و خطا ...
Prelude> let f = take 3 . drop 60
Prelude> mapM_ putStrLn (f cs)
}
~
خیلی خوب، ولی کجای جدولِ ASCII میشه؟ میتونیم از تابعِ متضادِ ord از Data.Char به اسمِ chr استفاده کنیم تا بفهمیم:
Prelude> import Data.Char (chr)
Prelude> :t chr
chr :: Int -> Char
Prelude> map chr [0..128]
... ۱۲۹ کاراکتر اول رو چاپ میکنه ...چیزی که چاپ شد با جدولِ ASCII همخونی داره، و UTF-8 هم اون کاراکترها رو همونطوری ارائه میده. حالا میشه با استفاده از این تابع ببینیم دقیقاً کجا کدِمون شکست میخوره:
-- درست کار میکنه
Prelude> utf8ThenPrint (B8.pack [chr 127])
-- شکست میخوره
Prelude> utf8ThenPrint (B8.pack [chr 128])
*** Exception: Cannot decode byte '\x80':
Data.Text.Internal.Encoding.decodeUtf8:
Invalid UTF-8 streamبا ماژول ِ Char8 از کاراکترهای یونیکد استفاده نکنین! این مشکل فقط مختصِ هسکل نیست – همهی زبانهای برنامهنویسی باید به وجود کدبندیهای مختلفِ متن واقف باشن.
Char8 بَده، قبوووووله
ماژول ِ Char8 برای یونیکد نیست؛ یا بطور کلیتر، برای متن نیست! تابعِ pack که داره فقط برای داده ِ ASCII ِه! چیزی که برنامهنویسها رو گمراه میکنه اینه که کدبندی ِ UTF-8 برای الفبای انگلیسی و چندتا کاراکترِ لاتین، عمداً با همون بایتهایی که ASCII برای کدبندی ِ همون کاراکترها استفاده میکنه، دقیقاً یکساناند. پس این کد کار میکنه، اما از لحاظِ اصولی غلطه:
Prelude> utf8ThenPrint (B8.pack "blah")
blahاگه به نتیجهی thisWorks و alsoWorks نگاه کنین، میبینین که اگه با کتابخونه ِ text یا utf8-string یه ByteString ِ UTF-8 بگیرین، به خوبی کار میکنه.
کِی باید برای دادهی نوشتاری از ByteString بجای Text استفاده کرد؟
معمولاً زمانهایی این کار لازمه که میخواین دادهای که با کدبندی UTF-8 از راه رسیده رو UTF-8 نگه دارین. معمولاً داده ِ UTF-8 رو از یه فایل یا سوکتِ شبکه میخونین، و برای جلوگیری از سرباری، نمیخواین مدام بینِ Text و UTF-8 نوسان کنین. اما اگه چنین کاری لازمه، شاید بهتر باشه از newtype استفاده کنین تا اشتباهاً با ByteStringهای غیرِ UTF-8 غاطی نشن.