۲۲ - ۵تاس

حالا با اینها می‌خوایم تاس درست کنیم. از کتابخونه ِ ‏‎random‎‏ و یه نوع‌داده ِ ساده به اسمِ ‏‎Die‎‏ برای تاس ِ شش وجهی استفاده می‌کنیم:

module RandomExample where

import System.Random

-- تاس شش وجهی
data Die =
    DieOne
  | DieTwo
  | DieThree
  | DieFour
  | DieFive
  | DieSix
  deriving (Eq, Show)

intToDie :: Int -> Die
intToDie n =
  case n of
    1 -> DieOne
    2 -> DieTwo
    3 -> DieThree
    4 -> DieFour
    5 -> DieFive
    6 -> DieSix
    -- error در استفاده از
    -- .به شدت صرفه‌جویی کنید
    x -> 
      error $
        "intToDie got non 1-6 integer: "
        ++ show x

خارج از آزمایش‌هایی مثلِ این از ‏‎error‎‏ استفاده نکنین، یا فقط وقتهایی استفاده کنین که میشه ثابت کرد اون شاخه غیرممکنه.*

*

توابع ناقص واقعاً عذاب میدن، پس فقط باید وقتهایی از ‏‎error‎‏ استفاده کنین که اون شاخه واقعاً هیچ وقت اتفاق نمیوفته. شکست‌های نرم‌افزاریِ غیرمنتظره معمولاً از همین چیزها پیش میان. تازه تو هسکل اصلاً لازم نیست چنین کاری کنیم؛ جایگزین‌های مناسبی مثل ‏‎Maybe‎‏ و ‏‎Either‎‏ داریم. تنها دلیلی که اینجا ازشون استفاده نکردیم اینه که می‌خواستیم ساده بمونه تا تمرکز روی ‏‎State Monad‎‏ باشه.

حالا باید تاس رو بندازیم:

rollDieThreeTimes :: (Die, Die, Die)
rollDieThreeTimes = do
  let s = mkStdGen 0
      (d1, s1) = randomR (1, 6) s
      (d2, s2) = randomR (1, 6) s1
      (d3, _) = randomR (1, 6) s2
  (intToDie d1, intToDie d2, intToDie d3)

این کد کار می‌کنه، اما خوب نیست. به خاطرِ اینکه عاری از هر اثری هست، هر دفعه یک جواب رو درست می‌کنه، اما اگه مقدار اولیه‌ش رو تغییر بدین جواب دیگه‌ای میده. چندبار امتحان کنین تا منظورمون رو متوجه بشین.

خوب حالا چطور میشه این کد رو ارتقا بدیم؟ با ‏‎State‎‏!

module RandomExample2 where

import Control.Applicative (liftA3)
import Control.Monad (replicateM)
import Control.Monad.Trans.State
import System.Random
import RandomExample

اول باید چندتا واردات اضافه کنیم. برای وارد کردنِ ‏‎State‎‏ باید ‏‎transformers‎‏ نصب شده باشه، که احتمالاً با GHC نصب شده و مشکلی نیست.

با ‏‎State‎‏ میشه ایجاد ِ یک ‏‎Die‎‏ رو فاکتور کنیم:

rollDie :: State StdGen Die
rollDie = state $ do
  (n, s) <- randomR (1, 6)
  return (intToDie n, s)

میشه گفت تابعِ ‏‎state‎‏ یه سازنده هست که یه تابع شبیهِ ‏‎State‎‏ می‌گیره و میذاردش توی مونَد ترانسفورمر ِ ‏‎State‎‏. فعلاً به اون "ترانسفورمر" فکر نکنین – جلوتر توضیح میدیم. تایپِ ‏‎state‎‏ اینطوره:

state :: Monad m
      => (s -> (a, s))
      -> StateT s m a

دقت کنین اینجا بجای استفاده از ‏‎let‎‏، نتیجه‌ی ‏‎randomR‎‏ رو بایند کردیم. اینطوری هنوز هم زیادی کُد نوشتیم. میشه تابعِ ‏‎intToDie‎‏ رو از روی ‏‎State‎‏ لیفت کرد:

rollDie' :: State StdGen Die
rollDie' =
  intToDie <$> state (randomR (1, 6))

آخرین آرگومان تایپیِ اون ‏‎State StdGen‎‏ یه ‏‎Int‎‏ بود. ما هم ‏‎Int -> Die‎‏ رو از روش لیفت کردیم و اون آخرین آرگومان تایپی رو به ‏‎Die‎‏ تغییر دادیم. در تابع بعدی باز هم به خلاصه‌نویسی ادامه میدیم:

rollDieThreeTimes'
  :: State StdGen (Die, Die, Die)
rollDieThreeTimes' =
  liftA3 (,,,) rollDie rollDie rollDie

توپل‌ساز ِ سه‌تایی رو از روی سه‌تا اجراییه ِ ‏‎State‎‏ که با یه حالت ِ اولیه مقادیرِ ‏‎Die‎‏ درست می‌کنن لیفت کردیم. در عمل چطور میشه؟

Prelude> evalState rollDieThreeTimes' (mkStdGen 0)
(DieSix,DieSix,DieFour)
Prelude> evalState rollDieThreeTimes' (mkStdGen 1)
(DieSix,DieFive,DieTwo)

به نظر خوب کار می‌کنه. باز هم هر ورودی یک خروجی میده. اگه بجای توپل یه لیستِ ‏‎Die‎‏ بخوایم چطور؟

-- مناسب هست؟
repeat :: a -> [a]

infiniteDie  :: State StdGen [Die]
infiniteDie = repeat <$> rollDie

همون کاری که انتظار داریم رو انجام میده؟ چه چیزی رو تکرار می‌کنه؟

λ> take 6 $ evalState infiniteDie (mkStdGen 0)
[DieSix,DieSix,DieSix,DieSix,DieSix,DieSix]

از مثالِ قبلی می‌دونیم که با مقدار دانه‌ای ِ صفر، سه‌تا مقدارِ اول نباید یکسان باشن. پس چی شد؟ اینجا یک مقدار تاس رو تکرار کردیم، اجراییه ِ حالت که تاس درست می‌کنه رو تکرار نکردیم. باید این کار رو کنیم:

replicateM :: Monad m
           => Int -> m a -> m [a]

nDie :: Int -> State StdGen [Die]
nDie n = replicateM n rollDie

در عمل:

Prelude> evalState (nDie 5) (mkStdGen 0)
[DieSix,DieSix,DieFour,DieOne,DieFive]
Prelude> evalState (nDie 5) (mkStdGen 1)
[DieSix,DieFive,DieTwo,DieSix,DieFive]

دقیقاً همون چیزی رو گرفتیم که می‌خواستیم.

باز هم تاس بنداز

در مثال زیر، انقدر تاس میندازیم تا مجموع‌شون ۲۰ یا بیشتر بشه.

rollsToGetTwenty :: State -> Int
rollsToGetTwenty g = go 0 0 g
  where
    go :: Int -> Int -> State -> Int
    go sum count gen
      | sum >= 20 = count
      | otherwise =
        let (die, nextGen) = 
              randomR (1, 6) gen
        in go (sum + die)
              (count + 1) nextGen

طرز کارش رو ببینیم:

Prelude> rollsToGetTwenty (mkStdGen 0)
5
Prelude> rollsToGetTwenty (mkStdGen 0)
5

از ‏‎randomIO‎‏ که با استفاده از ‏‎IO‎‏ هر بار یه مقدار جدید می‌گیره هم میشه استفاده کرد. اونطوری دیگه لازم نیست برای هر مقدار جدید یه ‏‎StdGen‎‏ ِ جدید بدیم:

Prelude> :t randomIO
randomIO :: Random a => IO a
Prelude> (rollsToGetTwenty . mkStdGen) <$> randomIO
6
Prelude> (rollsToGetTwenty . mkStdGen) <$> randomIO
7

پشت‌پرده از همون مکانیزمِ ‏‎State Monad‎‏ استفاده می‌کنه، ولی یک ‏‎StdGen‎‏ ِ سراسری رو تغییر میده تا با هربار استفاده ژنراتور رو جلو ببره. اگه می‌خواین طرزِ کارش رو با جزئیاتِ بیشتری یاد بگیرین، کُدِ منبع ِ کتابخونه ِ ‏‎random‎‏ رو مطالعه کنین.

تمرین‌ها: تاس خودتون رو بندازین

۱.

کاری کنین تابعِ ‏‎rollsToGetTwenty‎‏ حد مجموع تاس‌ها رو به عنوان ورودی بگیره.

rollsToGetN :: Int -> State -> Int
rollsToGetN = undefined

۲.

کاری کنین که ‏‎rollsToGetN‎‏ علاوه بر تعداد تاس‌ها، همه‌ی مقادیرشون هم ذخیره کنه.

rollsCountLogged :: Int
                 -> State
                 -> (Int, [Die])
rollsCountLogged = undefined