۹ - ۹تغییر دادن لیست مقادیر
تا اینجا نحوهی ساختِ توابعِ بازگشتی با بیانیههای خود ارجاعی رو دیدیم. اینها از ابزارِ مفید و از بخشهای اصلی در منطقِ هسکل هستن. اما در حقیقت بیشتر از توابعِ سطح بالا برای تغییر دادهها استفاده میشه (که بخشی از دلیلش محاسبهی نااکید در هسکله).
برای مثال، اعمال ِ یک تابع به همهی اعضای یه لیست و پس گرفتنِ لیست با مقادیر جدیدش، از کارهای رایج در هسکله. برای این کار یک تابع که ذاتاً بازگشتی هست لازمه و میشه اون تابع رو به تکتک اعضای لیست اعمال کرد. برای چنین موردی میشه از 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، اون تابع هم وارد میشه.