۲۷ - ۹تایپهای نوشتاری
عنوان این بخش یه کم اشتباهه، چون راجع به دوتا تایپِ نوشتاری و یک تایپ برای ارائهی تسلسل ِ بایتها صحبت میکنیم. اول 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
غاطی نشن.