۱۴ - ۶نمونههای 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
در کل این راهی برای ایجاد توابعِ تصادفی ِه. شاید الان درکِ اهمیتش سخت باشه، اما هر وقت یه چیزِ تصادفی میخواستین که یه جایی ازش تایپِ (->)
داشته باشه، اهمیتش مشخص میشه.