۱۳ - ۱۱قدم دوم: ایجاد یه لیست لغات

برای وضوحِ بیشتر، از یه تایپ مستعار استفاده می‌کنیم تا بدونیم منظورمون از ‏‎[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

حالا که یه لیست از لغات داریم، روی ساخت یه بازیِ تعاملی که از اون لیست استفاده می‌کنه متمرکز میشیم.