۹ - ۷لیست‌های توصیفی

لیست‌های توصیفی راهی برای ساخت یه لیست جدید از یه لیست یا لیست‌های دیگه‌ست. این ایده دقیقاً از مجموعه‌های توصیفی در ریاضیات میاد (حتی گرامر ِ مشابهی داره). حداقل یک لیست، به نام ژنراتور، برای تأمین اشیائی که لیستِ جدید از روش ساخته بشه لازم دارن. در این توصیف‌ها، ممکنه شرایطی برای المان‌های قابلِ قبول از ژنراتور، و یا توابعی برای اعمال به اون المان‌ها تعریف شده باشن.

با یه مثال خیلی ساده شروع کنیم:

[  x^2  |  x <- [1..10]]
-- [1] [2]   [   3   ]

۱.

تابع خروجی که به اعضای لیستِ داده شده اعمال میشه.

۲.

خط عمودی برای جدا کردنِ ورودی و تابعِ خروجی نوشته میشه.

۳.

مجموعه ِ ورودی: یه لیستِ ژنراتور (م. یا ایجادکننده) به همراه یه متغیر به نمایندگی المان‌های گرفته شده از لیست. در واقع چنین چیزی میگه: از یه لیست اعداد ۱ تا ۱۰، هر المان رو بگیر (‏‎<-‎‏) و به عنوان ورودی به تابع خروجی بده.*

*

م. این علامتِ ‏‎<-‎‏ رو می‌تونین به دو شکل نگاه کنین: یکی فِلِشی که به سمتِ چپ اشاره داره، یکی هم سمبُلِ معادل برای نمادِ عضویت در مجموعه‌‌ها (‏‎∈‎‏).

به زبان ساده، این توصیف لیست، یه لیست جدید شاملِ مجذورِ همه‌ی اعداد از ۱ تا ۱۰ درست می‌کنه:

Prelude> [x^2 | x <- [1..10]]
[1,4,9,16,25,36,49,64,81,100]

حالا به روش‌هایی برای تعیینِ اینکه کدوم المان‌ها از ژنراتور(ها) گرفته بشن نگاه می‌کنیم.

اضافه کردنِ شروط

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

برای مثال فرض کنیم یه لیستِ توصیفی مشابهِ مثالِ بالا بخوایم، با این تفاوت که فقط مجذورِ اعدادِ زوج رو شامل بشه و اعدادِ فرد رو نادیده بگیره. پس یه کاما بعد از لیستِ ژنراتور میذاریم و بعدش شرط رو می‌نویسیم:

Prelude> [x^2 | x <- [1..10], rem x 2 == 0]
[4,16,36,64,100]

اینجا تعیین کردیم که ‏‎x‎‏هایی که از ژنراتور می‌گیریم، باقیمانده ِشون از تقسیمِ با ۲ صفر باشه – یعنی زوج باشن.

میشه لیست‌های توصیفی که بیشتر از یک ژنراتور دارن هم نوشت. نکته‌ای که باید مدنظر داشت اینه که راست‌ترین ژنراتور اول مصرف میشه، و بعد راست‌ترین ژنراتور ِ بعدی و همینطور تا آخر.

برای مثال در لیستِ زیر، بجای مجذورِ اعداد، یه لیست از اعدادِ ‏‎x‎‏ به توانِ ‏‎y‎‏ درست می‌کنیم. دو تا ورودی رو با یه کاما جدا کردیم:

Prelude> [x^y | x <- [1..5], y <- [2, 3]]
[1,1,4,8,9,27,16,64,25,125]

اگه لیست خروجی رو نگاه کنیم، می‌بینیم که هر مقدارِ ‏‎x‎‏ اول به توانِ ۲ و بعد به توانِ ۳ رسیده، و به دنبال‌ش مقدارِ بعدیِ ‏‎x‎‏ به توانِ ۲ و بعد به توانِ ۳ رسیده، و همینطور تکرار شده و با مقادیرِ ۵۲ و ۵۳ تموم شده. اینجا تابع رو به همه‌ی جفت‌های ممکن از مقادیری که از دو لیستِ ورودی دَرمیاریم اعمال کردیم. کارِش رو با تلاش برای گرفتنِ یک مقدار از چپ‌ترین ژنراتور (که ‏‎x‎‏ رو ازش می‌گیریم) شروع می‌کنه.

برای نتیجه‌ی نهایی هم میشه شرط تعریف کرد. مثلاً اگه فقط لیستِ مقادیری رو بخوایم که از ۲۰۰ کوچکتر باشن، یه کاما ِ دیگه اضافه می‌کنیم و شرط رو می‌نویسیم:

Prelude> :{
Prelude| [x ^ y |
Prelude|  x <- [1..10],
Prelude|  y <- [2,3],
Prelude|  x ^ y < 200]
Prelude| :}
[1,1,4,8,9,27,16,64,25,125,36,49,64,81,100]

میشه محتوای چندتا ژنراتور رو در لیستی از توپل‌‌ها هم ترکیب کرد. لازم نیست ژنراتورها طول مساوی داشته باشن؛ و به خاطر ماهیتِ توپل‌‌ها، ملزوم به داشتنِ تایپ یکسان هم نیستن:

Prelude> :{
Prelude| [(x, y) |
Prelude|  x <- [1, 2, 3],
Prelude|  y <- [6, 7]]
Prelude| :}
[(1,6),(1,7),(2,6),(2,7),(3,6),(3,7)]

Prelude> :{
Prelude| [(x, y) |
Prelude|  x <- [1, 2, 3],
Prelude|  y <- ['a', 'b']]
Prelude| :}
[(1,'a'),(1,'b'),(2,'a'),
 (2,'b'),(3,'a'),(3,'b')]

باز هم مثل قبل، اول همه‌ی توپل‌‌های ممکن با اولین ‏‎x‎‏ رو درست کرد، بعد رفت سراغِ ‏‎x‎‏ ِ دوم، و الی آخر.

اولین لیست توصیفی که دیدیم، یه لیست از همه‌ی مقادیرِ x۲ (که x عددی از ۱ تا ۱۰ بود) درست می‌کرد. حالا فرض کنیم بخوایم از اون لیست در یه لیستِ توصیفی ِ دیگه استفاده کنیم. اول از همه باید یه اسم بَراش تعیین کنیم، مثلاً ‏‎mySqr‎‏:

Prelude> let mySqr = [x^2 | x <- [1..10]]

حالا میشه ازش به عنوانِ ژنراتور ِ یه لیستِ توصیفی ِ دیگه استفاده کرد. اینجا برای اینکه کُدها خلاصه‌تر بشن، فقط مقادیری که کوچکتر از ۴ هستن رو برای ورودی برمی‌داریم:

Prelude> let mySqr = [x^2 | x <- [1..10]]
Prelude> :{
Prelude| [(x, y) |
Prelude|  x <- mySqr,
Prelude|  y <- [1..3], x < 4]
Prelude| :}
[(1,1),(1,2),(1,3)]

تمرین‌ها: به وصف لیست خویش بنشین

توابعِ زیر رو نگاه کنین و خروجی‌شون رو پیدا کنین. بعد هم جواب‌تون رو در REPL تست کنین (فقط ‏‎mySqr‎‏ از بالا رو باید در گستره داشته باشین):

[x | x <- mySqr, rem x 2 == 0]

[(x, y) | x <- mySqr,
          y <- mySqr,
          x < 50, y > 50]

take 5 [ (x, y) | x <- mySqr, 
                  y <- mySqr,
                  x < 50, y > 50 ]

لیست‌های توصیفی با ‏‎String‎‏

اگه یادتون باشه نوشته‌ها هم لیست‌اند، پس در لیست‌های توصیفی، از نوشته هم میشه استفاده کرد. برای تشخیص وجود یه المان در یه لیست، از یکی از توابعِ استاندارد به اسمِ ‏‎elem‎‏ استفاده می‌کنیم.* خروجیِ این تابع از تایپِ ‏‎Bool‎‏ ِه، پس میشه ازش به عنوانِ شرط در لیست‌های توصیفی استفاده کرد:

Prelude> :t elem
elem :: Eq a => a -> [a] -> Bool
Prelude> elem 'a' "abracadabra"
True
Prelude> elem 'a' "Julie"
False
*

یادآوری: باز هم ‏‎Foldable‎‏ در تایپِ ‏‎elem‎‏ رو لیست فرض کنین تا ‏‎Foldable‎‏ رو توضیح بدیم.

در مثال اول، a یکی از المان‌های abracadabra بود پس جوابِ ‏‎True‎‏ گرفتیم. اما در مثالِ دوم، هیچ a ای در Julie نیست و جواب ‏‎False‎‏ شد. همونطور که از تایپ سیگنچر می‌بینین، ‏‎elem‎‏ فقط برای کاراکترها و نوشته‌ها نیست، ولی ما اینجا فقط برای همین ازش استفاده می‌کنیم. ببینیم میشه یه لیست توصیفی بنویسیم که همه‌ی حروف کوچک رو از یه نوشته حذف کنه. پس باید فقط ‏‎x‎‏ هایی رو از ژنراتور ِمون بگیریم که عضوی از لیست حروف بزرگ باشن:

Prelude> :{
Prelude| [x |
Prelude|  x <- "Three Letter Acronym",
Prelude|  elem x ['A'..'Z']]
Prelude| :}
"TLA"

حالا سعی کنیم این رو تبدیل به یه "مخفف‌ساز" کنیم که نوشته‌های متنوع رو قبول کنه، که مجبور نباشیم هر دفعه کلِ لیست توصیفی رو بنویسیم. برای این کار یه تابع با یه پارامترِ لیست تعریف می‌کنیم، که اون لیست به عنوان ژنراتور ِ لیست توصیفی استفاده میشه. پس آرگومان تابع و ژنراتور ِ نوشته باید یه چیز باشن:

Prelude> :{
Prelude| let acro xs =
Prelude|      [x | x <- xs, 
Prelude|      elem x ['A'..'Z']]
Prelude| :}

از ‏‎s‎‏ ِ جمع جلوی ‏‎x‎‏ در ‏‎xs‎‏، برای یادآوری به خودمون که آرگومان ورودی باید یه لیست باشه استفاده کردیم. البته هر اسمی میشد بنویسیم و باز هم کار می‌کرد، اما استفاده از یه متغیر که حالت جمع داره رایج‌تره.

خیلی خوب، با تابعِ ‏‎acro‎‏ که تعریف کردیم، میشه از هر نوشته‌ای مخفف ِش رو پیدا کنیم:

Prelude> :{
Prelude| acro "Self Contained Underwater\
Prelude|     \ Breathing Apparatus"
Prelude| :}
"SCUBA"
Prelude> :{
Prelude| acro "National Aeronautics and\
Prelude|     \ Space Administration"
Prelude| :}
"NASA"

فکر می‌کنین کارِ تابعِ زیر چی باشه؟

λ> let myString xs = [x | x <- xs, elem x "aeiou"]

تمرین‌ها: مکعب مربع

با داده‌‌های زیر:

Prelude> let mySqr = [x^2 | x <- [1..5]]
Prelude> let myCube = [y^3 | y <- [1..5]]

۱.

اول یه بیانیه بنویسین که از خروجی‌های ‏‎mySqr‎‏ و ‏‎myCube‎‏ یه لیستِ توپل بسازه.

۲.

حالا تغییرش بدین تا فقط از ‏‎x‎‏ و ‏‎y‎‏ هایی که از ۵۰ کوچکتر هستن استفاده کنه.

۳.

یه تابع به اون لیست توصیفی که نوشتین اعمال کنین تا تعداد توپل‌‌هایی که داره رو پیدا کنین.