۱۴ - ۶نمونههای Arbitrary
یکی دیگه از بخشهای مهمِ QuickCheck که باید بلد باشیم، نحوهی تعریفِ نمونههای تایپکلاسِ Arbitrary برای نوعدادههامونه. سازگار کردن کُدهاتون با QuickCheck از کارهاییه که شاید یه کم ناخوشایند باشه، ولی لازمه و نهایتاً هم باعثِ راحتیِ خودتون میشه. همین یک تایپکلاس چندتا مفهوم و مسئله رو با هم حل میکنه، به همین خاطر اولش برای تازهکارها یه مقدار گیجکنندهست.
اولین Arbitrary
اول با یه نمونه ِ Arbitrary ِ خیلی ساده برای نوعداده ِ Trivial شروع میکنیم:
module Main where
import Test.QuickCheck
data Trivial =
Trivial
deriving (Eq, Show)
trivialGen :: Gen Trivial
trivialGen =
return Trivial
instance Arbitrary Trivial where
arbitrary = trivialGenبرای برگردوندن ِ Trivial داخلِ مونَد ِ Gen، باید از return استفاده کنیم:
main :: IO ()
main = do
sample trivialGenچندتا نمونه بگیریم:
Prelude> sample trivialGen
Trivial
Trivial
Trivial
Trivial
Trivial
Trivial
Trivial
Trivial
Trivial
Trivial
Trivialاحتمالاً با Trivial به تنهایی چیزی معلوم نمیشه، اما مقادیرِ Gen، ژنراتورهای مقادیرِ تصادفیای هستن که QuickCheck مقادیرِ تست رو از اونها میگیره.
بحران هویت*
برای این نوعداده یه کم متفاوت میشه. با اینکه خودِ ساختار ِ Identity تغییری نمیکنه (نمیتونه تغییری کنه)، با این حال مقادیرِ تصادفی تولید میکنه:
data Identity a =
Identity a
deriving (Eq, Show)
identityGen :: Arbitrary a =>
Gen (Identity a)
identityGen = do
a <- arbitrary
return (Identity a)م. لغت identity علاوه بر "همانی،" معنی "هویت" هم میده...
اینجا از موند ِ Gen استفاده کردیم تا "از تو هوا" یک مقدارِ a درست کنیم، بذاریمش زیرِ Identity، و به عنوانِ Gen پسش بدیم. میدونیم یه کم عجیب به نظر میرسه، اما اگه ده-بیست بار انجام بدین شاید دیگه خوشتون بیاد.
از همون identityGen که نوشتیم دوباره استفاده میکنیم. اگه معادلِ arbitrary (در نمونه ِ Arbitrary) تعریفش کنیم، میشه ژنراتور ِ اصلی (پیشفرض) برای تایپِ Identity:
instance Arbitrary a =>
Arbitrary (Identity a) where
arbitrary = identityGen
identityGenInt :: Gen (Identity Int)
identityGenInt = identityGenبا identityGenInt، از تایپِ Identity رفعِ ابهام کردیم، و حالا میشه با اعمالِ تابعِ sample به این ژنراتور ازش نمونه بگیریم. خروجیش در ترمینال چیزی مثل زیر میشه:
Prelude> sample identityGenInt
Identity 0
Identity (-1)
Identity 2
Identity 4
Identity (-3)
Identity 5
Identity 3
Identity (-1)
Identity 12
Identity 16
Identity 0اگه تایپ معیّن ِ آرگومانِ تایپیِ Identity رو چیزِ دیگهای تعیین کنین، مقادیرِ نمونه ِ دیگهای میگیرین.
ضربهای Arbitrary
نمونههای Arbitrary برای تایپهای ضرب یه کم جذابترند، اما فقط یه تعمیم از همون چیزهایی که برای Identity گفتیم هستن:
data Pair a b =
Pair a b
deriving (Eq, Show)
pairGen :: (Arbitrary a,
Arbitrary b) =>
Gen (Pair a b)
pairGen = do
a <- arbitrary
b <- arbitrary
return (Pair a b)از همین تابعِ pairGen برای مقدارِ arbitrary در نمونه ِ Arbitrary استفاده میکنیم:
instance (Arbitrary a,
Arbitrary b) =>
Arbitrary (Pair a b) where
arbitrary = pairGen
pairGenIntString :: Gen (Pair Int String)
pairGenIntString = pairGenحالا میشه چندتا مقدارِ نمونه هم ایجاد کنیم:
Pair 0 ""
Pair (-2) ""
Pair (-3) "26"
Pair (-5) "B\NUL\143:\254\SO"
Pair (-6) "\184*\239\DC4"
Pair 5 "\238\213=J\NAK!"
Pair 6 "Pv$y"
Pair (-10) "G|J^"
Pair 16 "R"
Pair (-7) "("
Pair 19 "i\ETX]\182\ENQ"String ِ تصادفی... چقدر زیبا...
بزرگتر از مجموعِ اجزا
نوشتن نمونه ِ Arbitrary برای تایپهای جمع باز هم جذابتره. اول از همه، حتماً این رو وارد کنین:
import Test.QuickCheck.Gen (oneof)تایپهای جمع، معادلِ فصل ِ منطقیاند، پس برای یه تایپی مثلِ Sum باید همهی حالتهای ممکن رو داخلِ Gen ارائه بدیم. یه راه اینه که به تعداد لازم برای انواعِ حالتهای تایپ جمع ِمون، مقدارِ arbitrary دربیاریم (در این مثال دو دادهساز برای تایپمون داریم، پس دو مقدارِ arbitrary هم لازم داریم). بعد اونها رو دوباره میبریم زیرِ Gen و یه مقدار با تایپِ [Gen a] میگیریم که میشه به oneof بدیم:
data Sum a b =
First a
| Second b
deriving (Eq, Show)
-- احتمال مساوی برای هر کدوم
sumGenEqual :: (Arbitrary a,
Arbitrary b) =>
Gen (Sum a b)
sumGenEqual = do
a <- arbitrary
b <- arbitrary
oneof $ [return $ First a,
return $ Second b]تابعِ oneof با احتمالِ مساوی، از یک لیستِ Gen a، یک Gen a درست میکنه. از اونجا به بعد دیگه کار رو میسپرین به نمونههای Arbitrary ِ تایپهای a و b.
sumGenCharInt :: Gen (Sum Char Int)
sumGenCharInt = sumGenEqualحالا که تعیین کردیم از کدوم نمونههای Arbitrary برای a و b استفاده کنیم، امتحانش میکنیم:
Prelude> sample sumGenCharInt
First 'P'
First '\227'
First '\238'
First '.'
Second (-3)
First '\132'
Second (-12)
Second (-12)
First '\186'
Second (-11)
First '\v'تایپهای جمع وقتی جذابیتشون بیشتر میشه که وزنهای متفاوت برای احتمالِ انتخاب شدنشون تعریف کنیم. این نمونه ِ Maybe Arbitrary از کتابخونه ِ QuickCheck رو نگاه کنین:
instance Arbitrary a =>
Arbitrary (Maybe a) where
arbitrary =
frequency [(1, return Nothing),
(3, liftM Just arbitrary)]اینجا مقادیرِ تصادفی ِ Just با سه برابر احتمال نسبت به مقادیرِ Nothing درست میشن، چون احتمالِ مفید بودنشون هم بیشتره، با این حال بد نیست هرازگاهی Nothing هم بگیریم.
به همین روال، ما هم میتونیم احتمال انتخاب دادهساز ِ First رو در یه Gen ِ دیگه برای Sum ۱۰ برابر کنیم:
sumGenFirstPls :: (Arbitrary a,
Arbitrary b) =>
Gen (Sum a b)
sumGenFirstPls = do
a <- arbitrary
b <- arbitrary
frequency [(10, return $ First a),
(1, return $ Second b)]
sumGenCharIntFirst :: Gen (Sum Char Int)
sumGenCharIntFirst = sumGenFirstPlsبا این نسخه، کمتر مقدارِ Second میبینین:
First '\208'
First '\242'
First '\159'
First 'v'
First '\159'
First '\232'
First '3'
First 'l'
Second (-16)
First 'x'
First 'Y'یکی از نکاتِ حائز اهمیت اینجا اینه که حتماً لازم نیست نمونه ِ Arbitrary از یه نوعداده، تنها راهِ ایجاد ِ مقادیرِ تصادفی از اون نوعداده برای تستهای QuickCheck باشه. میشه از Genهای متفاوت، با رفتارهای جذابتر و حتی مفیدتر هم برای تایپتون استفاده کنین.
CoArbitrary
CoArbitrary یه همتا برای Aribitrary به حساب میاد که امکان ایجاد ِ توابعی برای تایپی بخصوص رو فراهم میکنه. بجای اینکه از طریقِ Gen مقادیرِ تصادفی بده، بهتون این امکان رو میده تا از طریق تابعهایی که یک مقدارِ ورودی از تایپِ a میگیرن، یک Gen رو تغییر بدین:
arbitrary :: Arbitrary a =>
Gen a
coarbitrary :: CoArbitrary a =>
a -> Gen b -> Gen b
-- [1] [ 2 ] [ 3 ]اینجا از [1] برای برگردوندن ِ یه نسخهی دستکاریشده (یا نسخهی متفاوت) از [2] استفاده میشه، تا نهایتاً نتیجهی [3] رو بده.
فقط کافیه نوعداده ِ موردِ نظرتون یه نمونه از Generic رو مشتق بگیره تا نمونه ِ CoArbitrary هم مجانی داشته باشه. مثالِ زیر خوب کار میکنه:
{-# LANGUAGE DeriveGeneric #-}
module CoArbitrary where
import GHC.Generics
import Test.QuickCheck
data Bool' =
True'
| False'
deriving (Generic)
instance CoArbitrary Bool'با این کد میشه چنین کارهایی کرد:
import Test.QuickCheck
-- بهعلاوهی کُدهای بالا
trueGen :: Gen Int
trueGen = coarbitrary True' arbitrary
falseGen :: Gen Int
falseGen = coarbitrary False' arbitraryدر کل این راهی برای ایجاد توابعِ تصادفی ِه. شاید الان درکِ اهمیتش سخت باشه، اما هر وقت یه چیزِ تصادفی میخواستین که یه جایی ازش تایپِ (->) داشته باشه، اهمیتش مشخص میشه.