۹ - ۹تغییر دادن لیست مقادیر

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

برای مثال، اعمال ِ یک تابع به همه‌ی اعضای یه لیست و پس گرفتنِ لیست با مقادیر جدیدش، از کارهای رایج در هسکل‌ه. برای این کار یک تابع که ذاتاً بازگشتی هست لازمه و میشه اون تابع رو به تک‌تک اعضای لیست اعمال کرد. برای چنین موردی میشه از ‏‎map‎‏ یا ‏‎fmap‎‏ استفاده کرد. تابعِ ‏‎map‎‏ فقط با ‏‎[]‎‏ کار می‌کنه. ‏‎fmap‎‏ در تایپکلاسی به اسمِ ‏‎Functor‎‏ تعریف شده و میشه ازش با داده‌‌هایی غیر از لیست هم استفاده کرد. بعداً از ‏‎Functor‎‏ بیشتر صحبت می‌کنیم، فعلاً به کاربردش با لیست کار داریم. در زیر چند مثال از ‏‎map‎‏ و ‏‎fmap‎‏ آوردیم:

Prelude> map (+1) [1, 2, 3, 4]
[2,3,4,5]
Prelude> map (1-) [1, 2, 3, 4]
[0,-1,-2,-3]
Prelude> fmap (+1) [1, 2, 3, 4] 
[2,3,4,5]
Prelude> fmap (2*) [1, 2, 3, 4]
[2,4,6,8]
Prelude> fmap id [1, 2, 3]
[1,2,3]
Prelude> map id [1, 2, 3]
[1,2,3]

تایپ‌های ‏‎map‎‏ و ‏‎fmap‎‏ از این قرارند:

map  ::              (a -> b) -> [a] -> [b]
fmap :: Functor f => (a -> b) -> f a -> f b

ببینیم تایپ‌ها در عمل چطور تغییر می‌کنن:

map :: (a -> b) -> [a] -> [b]

map (+1)

تایپِ ‏‎(a -> b)‎‏ معیّن‌تر میشه و به ‏‎Num a => a -> a‎‏ تبدیل میشه:

Prelude> :t map (+1)
map (+1) :: Num b => [b] -> [b]

حالا یه لیست از ‏‎Num‎‏ به عنوان آرگومان می‌گیره و یه لیست از ‏‎Num‎‏ هم جواب میده.

تایپِ ‏‎fmap‎‏ هم رفتار مشابهی داره:

fmap :: Functor f => (a -> b) -> f a -> f b
-- دقت کنین Functor به محدودیت تایپکلاسی

fmap (+1)
-- معیّن‌تر میشه (a -> b) دوباره

کمی با ‏‎map‎‏ فرق داره چون تایپکلاسِ ‏‎Functor‎‏ چیزهای بیشتری از لیست رو شامل میشه:

Prelude> :t fmap (+1)
fmap (+1) :: (Num b, Functor f) => f b -> f b

تابع ‏‎map‎‏ در ‏‎base‎‏ اینطور تعریف شده:

map :: (a -> b) -> [a] -> [b]
map _  []    = []
-- [1] [2]    [3]
map f (x:xs) = f x :  map f xs
-- [4] [5]    [6] [7] [8]

۱.

اینجا از خط تیره برای نادیده گرفتن ِ آرگومان تابع استفاده شده چون لازم‌ش نداریم.

۲.

اینجا حالتی که لیست خالی باشه رو تطبیقِ الگو دادیم. لیست یه تایپ جمع با دو حالت‌ه پس در هر تطبیق الگویی باید هر دو حالت‌ش رو در نظر بگیریم.

۳.

هر وقت لیستِ ورودی خالی باشه، تنها جوابِ صحیح یه لیستِ خالی‌ِه، اینجا هم جواب رو ‏‎[]‎‏ گذاشتیم. هر چیز دیگه‌ای می‌نوشتیم کامپایلر بِهِمون فِف می‌کرد.

۴.

پارامتر تابع رو با ‏‎f‎‏ تعریف می‌کنیم چون اسم خیلی گویاییه. کلاً ‏‎f‎‏ و ‏‎g‎‏ اسم‌های رایجی برای مقادیرِ تابعیِ نامعیّن در هسکل هستن. این تابعی‌ه که با ‏‎map‎‏ روی کل لیست نگاشت میدیم.

۵.

کل لیست رو به یک اسم مقیّد نمی‌کنیم. به خاطرِ تطبیق الگویی که بالاتر روی ‏‎[]‎‏ انجام داده بودیم، می‌دونیم که در این نقطه، لیستِ ورودی حداقل یک المان رو داره. اینجا روی دوّمین داده‌ساز ِ لیست، یعنی ‏‎(:)‎‏ که خودش یک ضرب ِه، تطبیقِ الگو کردیم. تنها مقدارِ این ضرب ِ cons رو با ‏‎x‎‏ معرفی کردیم، مابقی لیست هم با ‏‎xs‎‏.

۶.

تابعِ ‏‎f‎‏ ِمون رو به مقدارِ ‏‎x‎‏ اعمال می‌کنیم. این در واقع همون بخشی از تابعِ ‏‎map‎‏ ِه که اعمال ِ تابع به محتوای لیست صورت می‌گیره.

۷.

مقداری که از بیانیه‌ی ‏‎f x‎‏ می‌گیریم رو، به ابتدای لیستِ حاصل از ‏‎map‎‏ کردنِ مابقیِ لیست، cons می‌کنیم. داده‌‌ها در هسکل تغییرناپذیر اند. وقتی نگاشت میدیم، لیستی که داشتیم رو تغییر نمیدیم، بلکه با مقادیری که از اعمال ِ تابع بدست میاریم یه لیست جدید می‌سازیم.

۸.

خودِ ‏‎map‎‏ رو برای اعمال به ‏‎f‎‏ و ‏‎xs‎‏ صدا می‌زنیم. این بیانیه ارائه دهنده‌ی مابقی لیست، بعد از اعمال ِ ‏‎f‎‏ به تک‌تکِ المان‌های اون به حساب میاد.

چطور میشه مراحل کارِ ‏‎map f‎‏ رو باز کرد و نوشت؟ فقط دقت کنین این ترتیبِ محاسبه که اینجا نوشتیم، معادلِ ترتیبی که محاسبه‌ی نااکید پیاده می‌کنه نیست، ولی کمک می‌کنه بفهمیم چه خبره:

map (+1) [1, 2, 3]
-- هست، یعنی شرکت‌پذیری infixr 5 ،(:)
-- ‎‏داره‏‎ 5 ‎‏از راست با تقدم‏‎
map (+1) (1 : (2 : (3 : []))) -- نسخه‌ی تلخ

-- لیست خالی نیست، پس تطبیقِ
-- ‎‏رو‏‎ (+1) .‎‏الگوی دوم اجرا میشه‏‎
-- به مقدار مجرد اعمال می‌کنه
-- .می‌کنه map بعد هم
(+1) 1 : 
  map (+1)
    (2 : (3 : []))

-- ‎‏رو به مقدار بعدی اعمال کن،‏‎ (+1)
-- کردن map و به نتیجه‌ی حاصل از
-- .کن cons روی مابقی لیست
(+1) 1 :
  ((+1) 2 :
    (map (+1)
      (3 : [])))

-- بار آخری که شاخه‌ی دوم
-- اجرا میشه map تطبیق الگو در
(+1) 1 :
  ((+1) 2 : 
    ((+1) 3 :
      (map (+1) [])))

-- حالا فرمان پایه که لیست خالی
-- رو به عهده می‌گیره اجرا می‌کنیم
-- .و یه لیست خالی برمی‌گردونیم
(+1) 1 :
  ((+1) 2 : 
    ((+1) 3 : []))

-- حالا ساده می‌کنیم
2 : ((+1) 2 : ((+1) 3 : []))
2 : 3 : (+1) 3 : []
2 : 3 : 4 : [] == [2, 3, 4]

با استفاده از شکر گرامری ِ لیست، کاری که ‏‎map‎‏ انجام میده رو میشه اینطور تقریب زد:

map f [1, 2, 3] == [f 1, f 2, f 3]

map (+1) [1, 2, 3]
         [(+1) 1, (+1) 2, (+1) 3]
         [2, 3, 4]

یا با گرامرِ ستونی که بالاتر معرفی کردیم:

  :
 / \
1   : 
   / \
  2   :
     / \
    3  []


map (+1) [1, 2, 3]

       :
      / \
(+1) 1   : 
        / \
  (+1) 2   :
          / \
    (+1) 3  []

همونطور که گفتیم، اینها هیچ نوع ارائه‌ای از محاسبه‌ی نااکید نیستن. ‏‎map‎‏ اصلاً به طور آنی لیست رو طی و تابع رو اعمال نمی‌کنه. تابع تک‌به‌تک به مقادیری که از ‏‎map‎‏ به اجبار بیرون کشیده میشن اعمال میشه. اگه داخل لیست‌هامون چند جا مقادیرِ ‏‎undefined‎‏ بذاریم، می‌تونیم چنین رفتاری رو ببینیم:

Prelude> map (+1) [1, 2, 3]
[2,3,4]

-- کل لیست اجبار شد چون
-- لیست حاصل رو چاپ کرد GHCi


Prelude> (+1) undefined
*** Exception: Prelude.undefined

Prelude> (1, undefined)
(1,*** Exception: Prelude.undefined
Prelude> fst (1, undefined)
1

Prelude> map (+1) [1, 2, undefined]
[2,3,*** Exception: Prelude.undefined

Prelude> take 2 $ map (+1) [1, 2, undefined]
[2,3]

در مثالِ آخر، چون با ‏‎take 2‎‏ فقط دو المانِ اول رو درخواست کردیم، مقدارِ ‏‎undefined‎‏ اصلاً اجبار نشد و خطایی هم نداد. با ‏‎map (+1)‎‏ فقط مقادیرِ داخل سلول‌‌های cons ای که محاسبه شدن رو اجبار می‌کنیم. در صورتی مقادیر رو اجبار می‌کنیم که مقدارِ حاصل در لیستِ خروجی از تابعِ ‏‎map‎‏ رو محاسبه کنیم.

نکته‌ی مهم اینجا اینه که اکید بودن فقط از بیرون به داخل نیست. میشه یه کدِ اکید (مثلِ ‏‎+‎‏) رو، بذاریم تویِ یه کدِ تنبل (مثلِ ‏‎map‎‏). در واقع، می‌تونیم طریقه‌ی محاسبه‌ی ستون یا برگ‌ها رو (که تنبل باشن یا اکید) مستقل از هم انتخاب کنیم. برای کُدهای هسکلی که عملکرد ِشون مهمه، تنبل در ستون، اکید در برگ‌‌ها شُعارِ رایج‌ایه. چیزی نیست که اکثر کاربرهای هسکل بهش اهمیت بِدَن، اما وقتی از نااکیدی و ساختارهای داده صحبت کنیم، اینها رو هم کامل‌تر توضیح میدیم.

از ‏‎map‎‏ و ‏‎fmap‎‏ با توابعِ دیگه و تایپ‌های دیگه‌ای از لیست هم میشه استفاده کرد. در مثال زیر با ‏‎fst‎‏، المانِ اول از توپل‌‌های یک لیستِ توپل رو برمی‌گردونیم:

Prelude> map fst [(2,3), (4,5), (6,7), (8,9)]
[2,4,6,8]

Prelude> fmap fst [(2,3), (4,5), (6,7), (8,9)]
[2,4,6,8]

در این مثال، یک اعمال ناقص از تابعِ ‏‎take‎‏ رو نگاشت می‌کنیم:

Prelude> map (take 3) [[1..5], [1..5], [1..5]]
[[1,2,3],[1,2,3],[1,2,3]]

در مثال بعد، با یه تابعِ بی‌نام، یه ‏‎if-then-else‎‏ رو به لیست نگاشت می‌کنیم. این تابع هر مقداری که با ۳ مساوی باشه رو پیدا می‌کنه، منفی‌ش می‌کنه و بَرِش می‌گردونه:

Prelude> :{
Prelude| map
Prelude|   (\x -> if x==3 then (-x) else x)
Prelude|   [1..10]
Prelude| :}
[1,2,-3,4,5,6,7,8,9,10]

دیگه خودتون می‌تونین نگاشت کردنِ توابع مختلف رو امتحان کنین. پیشنهاد می‌کنیم اول با نگاشت کردن راحت بشین، بعد فصلِ فولدها رو شروع کنین.

تمرین‌ها: بازَم تهی

مثل همیشه، بهتره قبل از دیدنِ جواب‌ها با REPL، خودتون به جواب برسین:

۱.

بیانیه‌ی زیر جواب میده یا ⊥ میشه؟

take 1 $ map (+1) [undefined, 2, 3]

۲.

آیا بیانیه‌ی زیر جواب میده؟

take 1 $ map (+1) [1, undefined, 3]

۳.

آیا بیانیه‌ی زیر جواب میده؟

take 2 $ map (+1) [1, undefined, 3]

۴.

تابعِ رمزآلود زیر چه کاری انجام میده؟ تایپ‌ش چیه؟ اول به کلام توصیف‌ش کنین (برای خودتون یا یکی از نزدیکان‌تون) بعد در REPL تست کنین تا از جواب‌تون مطمئن بشین.

itIsMystery xs =
  map (\x -> elem x "aeiou") xs

۵.

جوابِ توابع زیر رو پیدا کنین:

a)

map (^2) [1..10]

b)

map minimum [[1..10], [10..20], [20..30]]
-- که min با minimum تابع
-- قبلاً دیدیم فرق داره

c)

map sum [[1..5], [1..5], [1..5]]

۶.

در فصلِ ۷ یه تابعی به اسم ‏‎foldBool‎‏ نوشتین. اون تابع در ماژولِ ‏‎Data.Bool‎‏ با اسمِ ‏‎bool‎‏ وجود داره. تابعی بنویسین که همون کارِ تابعِ ‏‎map (if-then-else)‎‏ رو انجام بده اما با استفاده از ‏‎bool‎‏ بجای ‏‎if-then-else‎‏ (یا اگه دوست دارین، یه کار مشابه انجام بده). اولین قدم، وارد کردنِ تابعِ ‏‎bool‎‏ در گستره هست، که با تایپ کردنِ ‏‎import Data.Bool‎‏ در prompt، اون تابع هم وارد میشه.