۲۳ - ۱۰مارشال از یک AST به یک نوعداده
گفته باشیم: این بخش نسبت به بقیهی بخشها یه ذره بیشتر پیشزمینه لازم داره. مطالبی که اینجا گفتیم، بدون یه کم تجربهی برنامهنویسی خیلی مفید نیستن. ممکنه بعضی مفاهیم و لغات هم براتون ناآشنا باشن.
عملِ پارسینگ رو میشه راهی برای کاهشِ کاردینالیتی ِ ورودیها به مجموعهی چیزهاییه که برنامه براشون جواب داره دونست. وقتی ورودیِ برنامه String
، Text
، یا ByteString
باشه، احتمالِ اینکه بشه یه کار اختصاصی و معنیدار باهاشون انجام داد خیلی کمه. ولی اگه بشه یکی از اونها رو به چیزی با ساختار پارس کنین و ورودیهای نامناسب رو رَد کنین، اون موقع میشه برنامهی مناسبی نوشت. از اشتباهاتی که برنامهنویسها در نوشتن برنامههایی که ورودیِ نوشتاری میگیرن انجام میدن اینه که دادهها رو در همون فرمتِ نوشتاری نگه میدارن و کارهای عجیبغریبی میکنن تا با ذاتِ بیساختار ِ اونها کنار بیان.
در بعضی موارد، پارسکردن به تنهایی کافی نیست. ممکنه یه جور AST یا ارائهای ساختاردار از چیزی که پارس کردین داشته باشین، ولی میخواین به یه فرمِ خاصی برسه. یعنی میخوایم کاردینالیتی رو محدودتر کنیم و ظاهر دادهمون رو از اون هم خاصتر کنیم. به این مرحلهی دوم معمولاً آنمارشالکردن ِ داده گفته میشه. مارشالکردن به آمادهکردنِ داده برای سریالسازی گفته میشه، چه از حافظه به خودیِخودِش (مرزِ واسطِ تعاملی با تابع خارجی)، چه از یه واسط تعاملیِ شبکه.
ایدهی کلی اینه که دو مسیر برای دادههاتون دارین:
Text (نوشته) -> Structure (ساختار) -> Meaning (مفهوم)
-- parse -> unmarshall
Meaning -> Structure -> Text
-- marshall -> serialize
برای انجام این کار فقط یک راه وجود نداره، اما ما یک کتابخونه ِ رایج رو نشون میدیم، و میبینیم که چطور این مسیرِ دومرحلهای در APIِش تعبیه شده.
مارشالکردن و آنمارشالکردن ِ داده ِ JSON
در حال حاضر، aeson
محبوبترین کتابخونه ِ JSON* برای هسکله. برنامهنویسهایی که از پایتون، روبی ، کلوژر ، جاواسکریپت، یا زبانهای مشابه میان به هسکل، از اینکه معمولاً هیچ مرحلهی مارشال/آنمارشالی وجود نداره کمی گیج میشن. در عوض AST ِ خامِ JSON مستقیماً به عنوانِ یه تیکه دادهی بیتایپ ارائه میشه. کاربرهای زبانهای تایپی احتمالاً با چنین چیزی مواجه شدن. ما برای مثالهای زیر از aeson
نسخهی 0.10.0.0
استفاده میکنیم.
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
module Marshalling where
import Data.Aeson
import Data.ByteString.Lazy (ByteString)
import Text.RawString.QQ
sectionJson :: ByteString
sectionJson = [r|
{ "section": {"host": "wikipedia.org"},
"whatisit": {"red": "intoothandclaw"}
}
|]
JSON مخففِ JavaScript Object Notation، به معنای "نشانگذاریِ اشیاء جاواسکریپت" هست. خوشبختانه یا بدبختانه، JSON یه فرمت استاندارد و بسیار محبوبه که برای انتقالِ داده، بخصوص بین مرورگرها و سرورها به کار میره. به همین خاطر کار با JSON یکی از کارهای رایج در برنامهنویسیه، پس بهتره از الان بهش عادت کنین.
دقت کنین که اینجا تعیین کردیم تایپِ sectionJson
یه ByteString
ِ تنبل ِه. اگه تایپهای ByteString
ِ اکید و تنبل رو با هم غاطی کنید، خطا میگیرین.
وقتی انتظارِ یه ByteString
ِ تنبل میره، و اکیدِش رو تأمین میکنین:
<interactive>:10:8:
Couldn't match expected type
Data.ByteString.Lazy.Internal.ByteString
with actual type ByteString
NB:
Data.ByteString.Lazy.Internal.ByteString
is defined in
Data.ByteString.Lazy.Internal
ByteString
is defined in
Data.ByteString.Internal
تایپ واقعی تایپیه که ما تأمین کردیم؛ تایپ مورد انتظار تایپیه که میخواسته. اون NB:
در پیغام خطا مخففِ nota bene به معنای "نکته" است. یا کُدِمون اشتباه بوده (پس تایپِ موردِ انتظار باید تغییر کنه)، یا مقادیرِ اشتباه رو تأمین کردیم (تایپ واقعی – تایپ یا مقادیری که تأمین کردیم باید تغییر کنن). با اشتباهِ زیر در ماژول ِ Marshalling
خودتون هم میتونین اون خطا رو بازسازی کنین:
-- :رو از این ByteString وارداتِ نوعسازِ
import Data.ByteString.Lazy (ByteString)
-- :به این تغییر بدین
import Data.ByteString (ByteString)
تأمینِ یه ByteString
ِ تنبل، وقتی اکیدِش انتظار میره:
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
module WantedStrict where
import Data.Aeson
import Data.ByteString.Lazy (ByteString)
import Text.RawString.QQ
sectionJson :: ByteString
sectionJson = [r|
{ "section": {"host": "wikipedia.org"},
"whatisit": {"red": "intoothandclaw"}
}
|]
main = do
let blah :: Maybe Value
blah = decodeStrict sectionJson
print blah
بارگذاریش که کنین، خطای تایپ ِ زیر رو میگیرین:
code/wantedStrictGotLazy.hs:19:27:
Couldn't match expected type
‘Data.ByteString.Internal.ByteString’
with actual type ‘ByteString’
NB:
‘Data.ByteString.Internal.ByteString’
is defined in ‘Data.ByteString.Internal’
‘ByteString’
is defined in ‘Data.ByteString.Lazy.Internal’
In the first argument of ‘decodeStrict’,
namely ‘sectionJson’
In the expression: decodeStrict sectionJson
اطلاعاتِ مفیدتر توی NB:
یا nota bene هستن، که به ماژولهای Internal
اشاره کرده. چیزی که باید به خاطر بسپرین اینه که تایپ واقعی یعنی "کُدِ خودتون،" تایپ مورد انتظار یعنی "تایپی که انتظار دارن،" و اینکه ماژول ِ ByteString
که تو اسمش Lazy
نداره، نسخهی اکیدِشه. میشه کُدِمون رو تغییر بدیم تا خطاهای تایپِ بهتری بگیریم:
-- رو ByteString واردات
-- :با این دوتا عوض کنین
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as LBS
-- :تایپ سیگنچر هم به این تغییر بدین
sectionJson :: LBS.ByteString
بعد خطای تایپِمون اینطوری میشه:
Couldn't match expected type ‘BS.ByteString’
with actual type ‘LBS.ByteString'
NB: ‘BS.ByteString’ is defined in
‘Data.ByteString.Internal’
‘LBS.ByteString is defined in
‘Data.ByteString.Lazy.Internal’
In the first argument of ‘decodeStrict’,
namely ‘sectionJson’
In the expression: decodeStrict sectionJson
چون هر دو نسخه رو به صورت ماژولهای مقید در اختیار داریم، پیغامهای خطا بهتر شدن. شاید همیشه مثلِ اینجا خوششانش نباشین و مجبور باشین یادتون بمونه کدوم کدوم بوده.
برگردیم به JSON
رایجترین توابعی که از aeson
استفاده میشن اینها هستن:
Prelude> import Data.Aeson
Prelude> :t encode
encode :: toJSON a => a -> LBS.ByteString
Prelude> :t decode
decode :: FromJSON a => LBS.ByteString -> Maybe a
این توابع یه جورایی دارن مرحلهی میانیِ گذشتن از تایپِ Value
رو دور میزنن (اون یکی از تایپهای aeson
ِه که یه نوعداده ِ AST ِ JSON ِه) – میگیم "یه جورایی،" چون به هر حال میشه از یه دادهی خام JSON به یه Value
رسید:
Prelude> decode sectionJson :: Maybe Value
Just (Object (fromList [
("whatisit",
Object (fromList [("red",
String "intoothandclaw")])),
("section",
Object (fromList [("host",
String "wikipedia.org")]))]))
خیلی خوشگل نیست. یه راهِ بهتر پیدا میکنیم. دقت کنین که تایپش رو تعیین کنین، وگرنه پیشفرضیسازیِ تایپ ِ GHCi کارهای عجیبی میکنه:
Prelude> decode sectionJson
Nothing
حالا اگه بخوایم این JSON رو بهتر نشون بدیم چطور؟ خب اول نوعدادههای خودمون رو تعریف میکنیم تا ببینیم میشه JSON رو به تایپهای خودمون decode* کنیم یا نه:
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
module Marshalling where
import Data.Aeson
import Data.ByteString.Lazy (ByteString)
import qualified Data.Text as T
import Data.Text (Text)
import Text.RawString.QQ
sectionJson :: ByteString
sectionJson = [r|
{ "section": {"host": "wikipedia.org"},
"whatisit": {"red": "intoothandclaw"}
}
|]
data TestData =
TestData {
section :: Host
, what :: Color
} deriving (Eq, Show)
newtype Host =
Host String
deriving (Eq, Show)
type Annotation = String
newtype Color =
Red Annotation
| Blue Annotation
| Yellow Annotation
deriving (Eq, Show)
main :: IO ()
main = do
let d = decode sectionJson :: Maybe TestData
print d
م. این لغت متضاد encode هست که تو این کتاب کدبندی ترجمه شده. پس شاید کدگشایی معادل مناسبی برای decode باشه.
این کُد بهتون خطای تایپ میده؛ از نبودِ یه نمونه ِ FromJSON
برای TestData
گله میکنه. درست میگه! GHC هیچ ایدهای نداره چطور داده ِ JSON (در فُرمِ یک Value
) به یه مقدارِ TestData
آنمارشال کنه. پس نمونهِش رو اضافه میکنیم:
instance FromJSON TestData where
parseJSON (Object v) =
TestData <$> v .: "section"
<*> v .: "whatisit"
parseJSON _ =
fail "Expected an object for TestData"
instance FromJSON Host where
parseJSON (Object v) =
Host <$> v .: "host"
parseJSON _ =
fail "Expected an object for Host"
instance FromJSON Color where
parseJSON (Object v) =
(Red <$> v .: "red")
<|> (Blue <$> v .: "blue")
<|> (Yellow <$> v .: "yellow")
parseJSON _ =
fail "Expected an object for Color"
دقت کنین که با استفاده از نیمنقلها در REPL، دیگه لازم نیست از backslash برای یه سری کاراکترها استفاده کنین (م. به این کار میگن رَدِ کاراکتر):
λ> :set -XOverloadedString
λ> decode "{\"blue\": \"123\"}" :: Maybe Color
Just (Blue "123")
λ> :set -XQuasiQuotes
λ> decode [r|{"red": "123"}|] :: Maybe Color
Just (Red "123")
نمونه ِ FromJSON
تایپِ Value
میگیره، و ToJSON
یه تایپِ Value
ایجاد میکنه و حلقه ِ زیر بسته میشه (بالاتر رابطهی بین پارسینگ و مارشالکردن رو با یه فرمِ کلیتر از این حلقه نشون دادیم):
-- FromJSON v------v م. تایپ خودتون
ByteString -> Value -> yourType
-- parse -> unmarshall
-- ToJSON
yourType -> Value -> ByteString
-- marshall -> serialize
در زمانِ نوشتارِ این کتاب، Value
اینطور تعریف شده:
-- | JSON ارائهی یک مقدار
-- | .به عنوان یک مقدار هسکل
data Value = Object !Object
| Array !Array
| String !Text
| Number !Scientific
| Bool !Bool
| Null
deriving (Eq, Read, Show,
Typeable, Data)
اگه بخوایم چیزی رو آنمارشال کنیم که ممکنه یه Number
یا String
باشه چطور؟
data NumberOrString =
Numba Integer
| Stringy Text
deriving (Eq, Show)
instance FromJSON NumberOrString where
parseJSON (Nubmer i) = return $ Numba i
parseJSON (String s) = return $ Stringy s
parseJSON _ =
fail "NumberOrString must\
\ be number or string"
این کُد، اولش درست کار نمیکنه. مشکل اینجاست که JSON (و برحسب اتفاق جاواسکریپت) فقط یک تایپِ عددی داره: ممیزِ شناورِ IEEE-754. JSON (و متأسفانه جاواسکریپت) هیچ تایپِ صحیحی ندارن، در نتیجه aeson
باید چیزی انتخاب کنه که بتونه همهی اعدادِ ممکن در JSON رو ارائه بده. دقیقترین گزینه، تایپِ Scientific
ِه که دقتِ نامحدود داره (خیلی وقت پیش تو فصلِ ۴ این رو گفتیم). پس باید از یه Scientific
به یه Integer
تبدیل کنیم:
import Control.Applicative
import Data.Aeson
import Data.ByteString.Lazy (ByteString)
import qualified Data.Text as T
import Data.Text (Text)
import Text.RawString.QQ
import Data.Scientific (floatingOrInteger)
data NumberOrString =
Numba Integer
| Stringy Text
deriving (Eq, Show)
instance FromJSON NumberOrString where
parseJSON (Number i) =
case floatingOrInteger i of
(Left _) ->
fail "Must be integral number"
(Right integer) ->
return $ Numba integer
parseJSON (String s) = return $ Stringy s
parseJSON _ =
fail "NumberOrString must\
\ be number or string"
-- تا بدونه میخوایم به چی پارس کنیم:
dec :: ByteString
-> Maybe NumberOrString
dec = decode
eitherDec :: ByteString
-> Either String NumberOrString
eitherDec = eitherDecode
main = do
print $ dec "blah"
ببینیم چه میکنه:
λ> main
Nothing
چی شد؟ میشه با eitherDecode
امتحان کنیم تا خطای تایپ ِ مفیدتری بگیریم:
main = do
print $ dec "blah"
print $ eitherDecode "blah"
مجدداً در REPL:
λ> main
Nothing
Left "Error in $: Failed reading:
not a valid json value"
پس اینطوری میشه پیغامهای خطای پربارتری از aeson
بگیریم. اگه بخوایم این کُد کار کنه، میتونیم مثالهای زیر رو امتحان کنیم:
λ> dec "123"
Just (Numba 123)
λ> dec "\"blah\""
Just (Stringy "blah")
حتی اگه زیاد قصدِ کار کردن با JSON ندارین، خوبه که با aeson
آشنا بشین چون خیلی از کتابخونههای مرتبط با سریالکردن در هسکل، API ِ مشابهی باهاش دارن. با مثالی که زدیم بازی کنین و ببینین چطور با تغییر تایپِ dec
میشه یه لیست از اعداد یا نوشته رو پارس کرد.