۲۲ - ۵تاس
حالا با اینها میخوایم تاس درست کنیم. از کتابخونه ِ 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