۲۲ - ۳اعداد تصادفی

مثلِ فصلِ قبل با یه مثالِ مفصّل شروع می‌کنیم. این مثال کمک می‌کنه یه کم با مسائلی که میشه با نوع‌داده ِ ‏‎State‎‏ حل کرد آشنا بشین.

در این مثال از نسخه‌ی ۱٫۱ کتابخونه ِ ‏‎random‎‏ استفاده می‌کنیم.

اول یه نگاه به توابعی که می‌خوایم استفاده کنیم بندازیم. قبلاً تو فصلی که داربازی رو درست کردیم از کتابخونه ِ ‏‎System.Random‎‏ استفاده کردیم، اما برای این مثال از توابعِ متفاوتی استفاده می‌کنیم. تو این مثال نگرش کلی داریم؛ نمی‌خوایم وارد جزئیاتِ طرز کار این ژنراتورها بشیم.

‏‎System.Random‎‏ برای ایجاد ِ مقادیرِ نیمه‌تصادفی طراحی شده. میشه اون مقادیر رو با تأمینِ یه مقدار دانه‌ای بدست آورد، یا با استفاده از یه ژنراتوری که سیستم خودش راه میندازه. از کتابخونه اینها رو استفاده می‌کنیم:

۱.

‏‎StdGen‎‏ یکی از تایپ‌هایی‌ه که اینجا می‌بینیم، و ضرب ِ دو مقدارِ ‏‎Int32‎‏ ِه. پس یه مقدار از تایپِ ‏‎StdGen‎‏ همیشه متشکل از دو مقدارِ ‏‎Int32‎‏ ِه. هردوشون مقادیرِ دانه‌ای برای ایجاد ِ عددِ تصادفی ِ بعدی‌اند.

۲.

تایپِ تابعِ ‏‎mkStdGen‎‏ از این قراره:

mkStdGen :: Int -> StdGen

اینجا با تعریفِ این تابع کاری نداریم، الان اون جزئیات مهم نیستن. این تابع یه آرگومانِ ‏‎Int‎‏ می‌گیره و به یه ژنراتور نگاشت‌ش میده و یه مقدارِ ‏‎StdGen‎‏ برمی‌گردونه، که در واقع یه جفت از مقادیرِ ‏‎Int32‎‏ ِه.

۳.

تایپِ تابعِ ‏‎next‎‏ از این قراره:

next :: g -> (Int, g)

که ‏‎g‎‏ یه مقدار با تایپِ ‏‎StdGen‎‏ ِه. اون ‏‎Int‎‏ در اولِ توپل، عددِ نیمه‌تصادفی ایه که از مقدارِ ‏‎StdGen‎‏ ایجاد شده؛ عضوِ دومِ توپل یه مقدارِ ‏‎StdGen‎‏ ِ جدیده.

۴.

تایپِ تابعِ ‏‎random‎‏ از این قراره:

random :: (RandomGen g, Random a)
       => g -> (a, g)

این هم شبیهِ ‏‎next‎‏ ِه، ولی با این تابع میشه مقادیرِ تصادفی ِ غیر از اعداد هم ایجاد کرد. این که چه توپِلی درست شه با تایپ تعیین میشه.

خوب حالا یه ذره طرز کارشون رو ببینیم:

Prelude> import System.Random
Prelude> mkStdGen 0
1 1
Prelude> :t mkStdGen 0
mkStdGen 0 :: StdGen
Prelude> let sg = mkStdGen 0
Prelude> :t next sg
next sg :: (Int, StdGen)
Prelude> next sg
(2147482884, 40014 40692)
Prelude> next sg
(2147482884, 40014 40692)

اون تابعِ پشت‌پرده که مقادیرِ خروجی رو تعیین می‌کنه خالص ِه، به همین خاطر دو بار یک مقدار گرفتیم؛ تایپ اجازه‌ی هیچ اثری رو نمیده تا اتفاقات عجیب‌غریب نیوفتن. یه نسخه‌ی دیگه از ‏‎sg‎‏ با یه ورودیِ دیگه به ‏‎StdGen‎‏ تعریف کنین و ببینین چی میشه.

نهایتاً به یه مقدار به اسمِ ‏‎next sg‎‏ رسیدیم. حالا اگه بخوایم با استفاده از اون عددِ تصادفی ِ بعدی رو ایجاد کنیم، باید مقدار ‏‎StdGen‎‏ از اون توپل رو دوباره به ‏‎next‎‏ بدیم. با تابع ‏‎snd‎‏ میشه اون مقدار رو بیرون کشید و به عنوانِ ورودی به ‏‎next‎‏ داد:

Prelude> snd (next sg)
40014 40692
Prelude> let newSg = snd (next sg)
Prelude> newSg
newSg :: StdGen
Prelude> next newSg
(2092764894,1601120196 1655838864)

اگه دوباره ‏‎newSg‎‏ رو به ‏‎next‎‏ بدین باز هم همون جواب رو می‌گیرین، اما میشه دوباره مقدارِ ‏‎StdGen‎‏ رو استخراج کرد و به ‏‎next‎‏ داد تا یه توپل ِ جدید گرفت:

Prelude> next (snd (next newSg))
(1679949200,1635875901 2103410263)

حالا چندتا مثال از ‏‎random‎‏ ببینیم. چون ‏‎random‎‏ می‌تونه از تایپ‌های مختلف مقادیر ایجاد کنه، باید تعیین کنیم از چه تایپی استفاده کنه:

Prelude> :t random newSg
random newSg :: Random a => (a, StdGen)
Prelude> random newSg :: (Int, StdGen)
(138890298504988632,439883729 1872071452)
Prelude> random newSg :: (Double, StdGen)
(0.41992072972993366,439883729 1872071452)

ساده‌ست، اگه یک عدد از یه بازه بخوایم چطور؟

Prelude> :t randomR
randomR :: (RandomGen g, Random a)
        => (a, a) -> g -> (a, g)
Prelude> randomR (0, 3) newSg :: (Int, StdGen)
(1,1601120196 1655838864)
Prelude> randomR (0, 3) newSg :: (Double, StdGen)
(1.259762189189801,439883729 1872071452)

باید حالت ِ جدیدِ ژنراتور ِ عددِ تصادفی رو به تابعِ ‏‎next‎‏ بدیم تا یه مقدارِ جدید بگیریم:

λ> let rx :: (Int, StdGen); rx = random (snd sg3)
λ> rx
(2387576047905147892,1038587761 535353314)
λ> snd rx
1038587761 535353314

اینطور زنجیر‌کردنِ حالت ممکنه خسته‌کننده شه. هدفِ ما تو این فصل حلِ این مسئله‌ست.