۱۳ - ۱۱قدم دوم: ایجاد یه لیست لغات
برای وضوحِ بیشتر، از یه تایپ مستعار استفاده میکنیم تا بدونیم منظورمون از [String] چیه. جلوتر یه نسخهی دیگه که با استفاده از newtype باز هم خواناتر شده رو نشون میدیم. از گرامر ِ do هم برای خوندن محتویاتِ دیکشنری به داخل یه متغیر به اسمِ dict استفاده میکنیم. با استفاده از تابعِ lines اون نوشته ِ گُنده رو به یه لیست از نوشته تبدیل میکنیم که هر کدوم از المانهاش یکی از خطها میشن. هر خط هم یک لغته، پس نتیجهی کارمون میشه WordList:
type WordList = [String]
allWords :: IO WordList
allWords = do
dict <- readFile "data/dict.txt"
return (lines dict)قبل از اینکه پیش بریم یه نگاه به lines بندازیم. این تابع نوشتهها رو از جاهایی که خط ِ جدید شروع میشن جدا میکنه و یه لیست از نوشته برمیگردونه:
Prelude> lines "aardvark\naaron"
["aardvark","aaron"]
Prelude> length $ lines "aardvark\naaron"
2
Prelude> length $ lines "aardvark\naaron\nwoot"
3
Prelude> lines "aardvark aaron"
["aardvark aaron"]
Prelude> length $ lines "aardvark aaron"
1اگه متوجه شده باشین، کاری که این تابع انجام میده مشابه تابعِ words که نوشتهها رو از فاصلهها (که معمولاً بین لغات وجود دارن) جدا میکنه هست. البته words نوشتهها رو از خط جدیدها هم جدا میکنه:
Prelude> words "aardvark aaron"
["aardvark","aaron"]
Prelude> words "aardvark\naaron"
["aardvark","aaron"]در بخش بعدی از ساخت لیستِ لغات برای بازیمون، حد بالا و حد پایینِ طول لغاتمون رو تعیین میکنیم. هرطور دوست داشتین تغییرشون بدین:
minWordLength :: Int
minWordLength = 5
maxWordLength :: Int
maxWordLength = 9حالا خروجیِ allWords رو فیلتر میکنیم تا با شرایطِ بالا جور بشه. با این کار لیستِ کوتاهتری از لغات برای بازی بدست میاریم:
gameWords :: IO WordList
gameWords = do
aw <- allWords
return (filter gameLength aw)
where gameLength w =
let l = length (w :: String)
in l >= minWordLength
&& l < maxWordLengthیه جفت تابع هم لازم داریم که یه لغتِ تصادفی رو از لیستِ لغات بیرون بکشن. برای این کار از تابعِ randomRIO که بالاتر گفتیم استفاده میکنیم. به تابعِ randomRIO یه توپل از صفر (اولین اندیس ِ لیستِ لغات) و عددی معادلِ طولِ لیستِ لغات منهای یک میدیم. چرا منهای یک؟
باید یکی از طولِ لیستِ لغات کم کنیم چون length از ۱ شروع به شمارش میکنه، اما اندیس ِ لیست از صفر شروع میشه. یه لیست به طولِ ۵ هیچ عضوی در اندیس ِ ۵ نداره – المانهاش در جایگاههای صفر تا ۴ قرار گرفتن:
Prelude> [1..5] !! 4
5
Prelude> [1..5] !! 5
*** Exception: Prelude.!!: index too largeدر نتیجه برای گرفتنِ آخرین مقدارِ یه لیست، باید از اندیسی که برابرِ طولِ لیست منهای یک هست استفاده کنیم:
Prelude> let myList = [1..5]
Prelude> length myList
5
Prelude> myList !! length myList
*** Exception: Prelude.!!: index too large
Prelude> myList !! (length myList) – 1
5با دو تابعِ بعدی، یه لغتِ تصادفی از لیستِ gameWords که بالاتر درست کردیم بیرون میکشیم. اجمالی بخوایم بگیم، تابعِ randomWord یه عددِ تصادفی برمبنای طول لیستِ لغات (wl) پیدا میکنه، بعد لغتی که با اون عددْ اندیس ِ یکسانی داره رو انتخاب میکنه و یه IO String برمیگردونه. با توجه به چیزهایی که از randomRIO و اندیسها میدونین، انتظار داریم خودتون آرگومانِ توپل ِ randomRIO رو بنویسین:
randomWord :: WordList -> IO String
randomWord wl = do
randomIndex <- randomRIO ( , )
-- جای خالی رو پر کنید ^^^^
return $ wl !! randomIndexتابعِ دوم، randomWord'، لیستِ gameWords رو به تابعِ randomWord بایند میکنه تا لغتِ تصادفی ای که میگیریم از لیستِ gameWords باشه. توضیحِ کاملِ عملگر ِ >>= به اسمِ بایند رو موکول میکنیم به فصلی که Monad رو میگیم. فعلاً، مشابهِ چیزی که برای گرامر ِ do گفتیم، بایند هم امکانِ ترکیب ِ اجراییهها به صورت تسلسلی رو میده، طوری که مقدار ایجاد شده از یکی، میشه آرگومان برای بعدی:
randomWord' :: IO String
randomWord' = gameWords >>= randomWordحالا که یه لیست از لغات داریم، روی ساخت یه بازیِ تعاملی که از اون لیست استفاده میکنه متمرکز میشیم.