۱۴ - ۶نمونه‌های 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

در کل این راهی برای ایجاد توابعِ تصادفی ِه. شاید الان درکِ اهمیت‌ش سخت باشه، اما هر وقت یه چیزِ تصادفی می‌خواستین که یه جایی ازش تایپِ ‏‎(->)‎‏ داشته باشه، اهمیت‌ش مشخص میشه.