۹ - ۶استخراج بخشی از لیست‌ها

در این بخش، نگاهی میندازیم به چندتا تابعِ مفید برای استخراج بخشهایی از یه لیست، و همچنین توابعی برای تقسیم‌بندی لیست‌ها. سه تابعِ اول تایپ سیگنچرهای مشابه دارن، یه آرگومانِ ‏‎Int‎‏ و یه آرگومانِ لیست می‌گیرن:

take :: Int -> [a] -> [a]
drop :: Int -> [a] -> [a]
splitAt :: Int -> [a] -> ([a], [a])

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

تابع ‏‎take‎‏ تعداد مشخصی از المان‌های یه لیست رو درمیاره و لیستی که فقط حاوی اون مقادیره برمی‌گردونه. همونطور که می‌بینید یک آرگومانِ ‏‎Int‎‏ می‌گیره اون رو به یک آرگومان لیست اعمال می‌کنه. اینطور کار می‌کنه:

Prelude> take 7 ['a'..'z']
"abcdefg"

Prelude> take 3 [1..10]
[1,2,3]

Prelude> take 3 []
[]

وقتی بهش لیست خالی دادیم، اون هم یه لیست خالی برگردوند. لیست‌هایی هم که نوشتیم، با شکر ِ گرامر ِ بازه‌ای لیست می‌سازن. از ‏‎take‎‏ با یه تابعِ لیست‌ساز مثل ‏‎enumFrom‎‏ هم می‌تونستیم استفاده کنیم. یادآوری: تابعِ ‏‎enumFrom‎‏ در صورتی که تایپِ ورودی‌ش یه مجموعه ِ بینهایت باشه (مثل ‏‎Integer‎‏)، یه لیست بینهایت درست می‌کنه. ولی اگه فقط تعداد مشخصی از المان‌هاش رو بگیریم، لیست بینهایت درست نمی‌کنه:

Prelude> take 10 (enumFrom 10)
[10,11,12,13,14,15,16,17,18,19]

تابع ‏‎drop‎‏ مشابه ‏‎take‎‏ هست، ولی تعداد المان‌های تعیین شده رو از اول لیست حذف می‌کنه (میندازه). باز هم میشه از بازه‌ها یا توابعِ لیست‌ساز استفاده کنیم:

Prelude> drop 5 [1..10]
[6,7,8,9,10]

Prelude> drop 8 ['a'..'z']
"ijklmnopqrstuvwz"

Prelude> drop 4 []
[]

Prelude> drop 2 (enumFromTo 10 20)
[12,13,14,15,16,17,18,19,20]

تابعِ ‏‎splitAt‎‏ یه لیست رو، در المانی که با ‏‎Int‎‏ تعیین شده، به دو بخش تقسیم می‌کنه و یه توپل از دو لیست درست می‌کنه:

Prelude> splitAt 5 [1..10]
([1,2,3,4,5],[6,7,8,9,10])

Prelude> splitAt 10 ['a'..'z']
("abcdefghij","klmnopqrstuvwxyz")

Prelude> splitAt 5 []
([],[])

Prelude> splitAt 3 (enumFromTo 5 15)
([5,6,7],[8,9,10,11,12,13,14,15])

توابعِ سطح بالا ِ ‏‎takeWhile‎‏ و ‏‎dropWhile‎‏، همونطور که از تایپ سیگنچر ِشون پیداست، کمی متفاوت‌اند:

takeWhile :: (a -> Bool) -> [a] -> [a]
dropWhile :: (a -> Bool) -> [a] -> [a]

این دو تابع کارهای ‏‎take‎‏ و ‏‎drop‎‏ رو تا زمانی که یک شرط (به ‏‎Bool‎‏ دقت کنین) برقرار باشه انجام میدن. تابعِ ‏‎takeWhile‎‏ المان‌هایی که شرط ِ داده شده براشون صادق هست رو برمی‌داره، و با رسیدن به اولین المانی که شرط رو ارضا نمی‌کنه متوقف میشه.

المان‌هایی که کوچکتر از ۳ هستن رو بردار:

Prelude> takeWhile (<3) [1..10]
[1,2]

المان‌هایی که کوچکتر از ۸ هستن رو بردار:

Prelude> takeWhile (<8) (enumFromTo 5 15)
[5,6,7]

مثال بعدی لیستِ خالی برمی‌گردونه، چون این تابع به محض رسیدن به المانی که شرط براش صادق نیست متوقف میشه، که در این مثال میشه المانِ اول:

Prelude> takeWhile (>6) [1..10]
[]

در مثال پایین، چرا فقط یک a برمی‌گردونه؟

Prelude> takeWhile (=='a') "abracadabra"
"a"

احتمالاً بتونین حدس بزنین تابعِ ‏‎dropWhile‎‏ چطور کار می‌کنه. برای بهتر دیدن تفاوت‌هاشون، در مثال‌های زیر هم از همون آرگومان‌هایی که برای ‏‎takeWhile‎‏ استفاده کردیم، آوردیم:

Prelude> dropWhile (<3) [1..10]
[3,4,5,6,7,8,9,10]

Prelude> dropWhile (<8) (enumFromTo 5 15)
[8,9,10,11,12,13,14,15]

Prelude> dropWhile (>6) [1..10]
[1,2,3,4,5,6,7,8,9,10]

Prelude> dropWhile (=='a') "abracadabra"
"bracadabra"

تمرین‌ها: تقارن ترسناک

۱.

با استفاده از ‏‎takeWhile‎‏ و ‏‎dropWhile‎‏ تابعی بنویسین که لغاتِ یه جمله رو در یه لیست برگردونه (لغات رو به کمکِ فاصله از هم تشخیص بده)، مثل نمونه‌ی زیر:

Prelude> myWords "sheryl wants fun"
["Sheryl", "wants", "fun"]

۲.

تابعی بنویسین که یه نوشته بگیره و یه لیست از خط‌های نوشته ِ ورودی برگردونه (که هر خط با کاراکترِ "خط جدید" یا ‏‎\n‎‏ از خط ِ بعد جدا میشه). تعریف تابع رو بجای ‏‎undefined‎‏ بنویسین:

module PoemLines where

firstSen = "Tyger Tyger, burning bright\n"
secondSen = "In the forests of the night\n"
thirdSen = "What immortal hand or eye\n"
fourthSen = "Could frame thy fearful\
           \ symmetry?"
sentences = firstSen ++ secondSen
         ++ thirdSen ++ fourthSen

-- باید این رو چاپ کنه putStrLn sentences
-- Tyger Tyger, burning bright
-- In the forests of the night
-- What immortal hand or eye
-- Could frame thy fearful symmetry?

-- تابع رو تعریف کنین
myLines :: String -> [String]
myLines = undefined

-- هم myLines sentences جواب
-- باشه shouldEqual باید برابر
shouldEqual =
  ["Tyger Tyher, burning bright\n"
  , "In the forests of the night\n"
  , "What immortal hand or eye\n"
  , "Could frame thy fearful symmetry?"
  ]

-- رو فقط برای main تابع
-- تست تابع‌تون تعریف کردیم.
main :: IO ()
main =
  print $
  "Are they equal? "
  ++ show (myLines sentences
           == shouldEqual)

۳.

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