۲۱ - ۲شروعی تازه
این فصل رو یه کم متفاوت از بقیهی فصلها شروع میکنیم، چون امیدواریم از این راه نشون بدیم کارهایی که اینجا انجام میدیم تفاوتِ چندانی با چیزهایی که تا اینجا یاد گرفتین ندارن. پس با چندتا مثال شروع میکنیم. یه فایل رو مثل زیر شروع کنین:
import Control.Applicative
boop = (*2)
doop = (+10)
bip :: Integer -> Integer
bip = boop . doop
از روی تایپهای boop
، doop
، و (.)
مشخصه که bip
، یک آرگومان میگیره. دقت کنین که اگه تایپها رو تعیین نکنین و از فایل بارگذاری کنین، بطورِ پیشفرض تکریختی میشه؛ اگه بخواین bip
چندریختی بشه، علاوه بر تایپِ خودش، باید تایپِ دوتا تابعی هم که ازشون درست شده رو پلیمورفیک کنین.
وقتی bip
رو به یه آرگومان اعمال میکنیم، اول doop
به اون آرگومان اعمال میشه، و بعد جوابش به عنوانِ ورودی به boop
پاس میشه. تا اینجا چیزی جدید نبوده.
این ترکیب تابع رو اینطوری هم میشه نوشت:
bloop :: Integer -> Integer
bloop = fmap boop doop
هنوز fmap
کردنِ یه تابع از روی یه تابعِ دیگه برامون کامل مأنوس نشده، و ممکنه واستون سؤال شده باشه که بافت ِ فانکتوری اینجا کجاست. منظورمون از بافت ِ فانکتوری همون ساختار (نوعداده) ایه که تابع از روش لیفت میشه تا به مقدارِ داخلِ اون ساختار اعمال بشه. برای مثال، لیست یه بافت ِ فانکتوری ِه که میشه تابعها رو از روش لیفت کرد. میگیم تابع از روی ساختار ِ لیست لیفت میشه و به مقادیرِ داخلِ لیست اعمال یا نگاشت میشه.
در تابعِ bloop
، بافتِمون یه تابعِ نیمه اعمال شده ِه. مشابهِ ترکیب توابع، fmap
هم دوتا تابع رو قبل از اعمال به آرگومان، با هم ترکیب میکنه. پس جوابِ یکی به عنوانِ ورودی به اون یکی داده میشه. اینجا fmap
، تابعِ نیمه اعمال شده رو از روی تابعِ بعدی لیفت میکنه، چیزی شبیهِ این:
fmap boop doop x == (*2) ((+10) x)
-- که از راه میرسه، اولین x این
-- میشه، و (+10) آرگومان برای
-- جوابِ اون میشه اولین
-- .(*2) آرگومان برای
این فانکتور ِ توابع ِه. به زودی جزئیات بیشتری از این رو توضیح میدیم.
فعلاً بریم سراغِ چندتا مثالِ دیگه. این کُدها رو تو همون فایل بنویسین تا boop
و doop
در گستره باشن:
bbop :: Integer -> Integer
bbop = (+) <$> boop <*> doop
duwop :: Integer -> Integer
duwop = liftA2 (+) boop doop
حالا یه بافت ِ Applicative
داریم. یه تابعِ دیگه اضافه کردیم تا از روی بافت ِ تابعهای نیمه اعمال شده لیفت کنیم. این بار هم تابعهای نیمه اعمال شده داریم که منتظرِ آرگوماناند، اما این متفاوت از fmap
کردن کار میکنه. اینجا آرگومانِ ورودی به طورِ موازی به boop
و doop
داده میشه، و جوابهاشون با هم جمع میشه.
boop
و doop
هردو منتظرِ یه ورودیاند. اینطوری میتونیم همزمان اعمالِشون کنیم:
Prelude> bbop 3
19
کاری شبیهِ این انجام میده:
((+) <$> (*2) <*> (+10)) 3
-- :fmap اول
(*2) :: Num a => a -> a
(+) :: Num a => a -> a -> a
(+) <$> (*2) :: Num a => a -> a -> a
اگه تابعی که منتظرِ دو آرگومان هست رو روی یه تابع که یک آرگومان میخواد نگاشت کنیم، تابعی درست میشه که دو آرگومان میخواد.
دقت کنین که این دقیقاً عینِ ترکیب توابع ِه:
(+) . (*2) :: Num a => a -> a -> a
جوابش هم همینطور:
Prelude> ((+) . (*2)) 5 3
13
Prelude> ((+) <$> (*2)) 5 3
13
پس چه اتفاقی داره میوفته؟
((+) <$> (*2)) 5 3
-- به یاد داشته باشین که این
-- .هست (.) پشت پرده همون
((+) . (*2)) 5 3
-- f . g = \ x -> f (g x)
((+) . (*2)) == \ x -> (+) (2 * x)
نکته اینجاست که حتی بعد از اعمال ِ x
، میرسیم به اعمالِ ناقص ِ (+)
روی آرگومانِ اولش (که همون x
ِه که با (*2)
دوبرابر شده). پس آرگومانِ دومش میشه عددی که به آرگومانِ اول (که دوبرابر شده) اضافه میشه:
-- (*2) :اولین تابعی که اعمال میشه
-- 5 :اولین آرگومان
-- هم یه آرگومان میگیره، پس داریم: (*2)
((+) . (*2)) 5 3
(\ x -> (+) (2 * x)) 5 3
(\ 5 -> (+) (2 * 5)) 3
((+) 10) 3
-- رو جمع میکنه. 3 و 10 بعد
13
اوکِی، ولی قسمت دومش چطور؟
((+) <$> (*2) <*> (+10)) 3
-- ...وایسا ببینم
-- آرگومان اول کجا رفت؟
((+) <$> (*2) <*> (+10)) :: Num a => b -> b
یکی از خوبیهای هسکل اینه که میشه یه تایپِ معینتر برای توابعی مثل (<*>)
اعلام کنیم و ببینیم که آیا کامپایلر موافقت میکنه یا نه. اول تایپِ (<*>)
رو دوره کنیم:
λ> :t (<*>)
(<*>) :: Applicative f => f (a -> b) -> f a -> f b
-- f ~ ((->) a) :در این مورد میدونیم
-- :پس همین رو تعیین میکنیم
λ> :t (<*>) :: (a -> a -> b) -> (a -> a) -> (a -> b)
(<*>) :: (a -> a -> b) -> (a -> a) -> (a -> b)
کامپایلر میپذیره که این یه تایپِ ممکن برای (<*>)
هست.
حالا چطوری کار میکنه؟ اتفاقی که میوفته اینه که به (*2)
و (+10)
یکی یه آرگومان میدیم و دوتا جوابی که میگیریم، دوتا آرگومانِ (+)
میشن:
((+) <$> (*2) <*> (+10)) 3
((3*2) + (3+10))
(6 + 13)
19
مواقعی از این استفاده میکنیم که دو تابع یه ورودیِ مشترک میگیرن و میخوایم یه تابعِ دیگه به جوابِ اون دوتا اعمال کنیم تا به یه جوابِ نهایی برسیم. خیلی بیشتر از اونکه اولش به نظر میرسه لازم میشه، یک مثال ازش در کُدِ واقعی ببینیم:
module Web.Shipping.Utils ((<||>)) where
import Control.Applicative (liftA2)
(<||>) :: (a -> Bool)
-> (a -> Bool)
-> a
-> Bool
(<||>) = liftA2 (||)
ایده همون ایدهی duwop
ِه که بالاتر دیدیم.
یه مثالِ دیگه:
bbop :: Integer -> Integer
bbop = do
a <- boop
b <- doop
return (a + b)
این دقیقاً همون کاری رو میکنه که مثالِ Applicative
میکرد، فقط اینجا بافتِمون مونَدی شده. این تمایز تأثیری تو این تابعِ بخصوص نداره. متغیرِ a
رو به تابعِ نیمه اعمالشده ِ boop
نسبت میدیم، و متغیرِ b
رو به doop
. به محضِ گرفتنِ یه ورودی، جاهای خالیِ boop
و doop
پر میشن؛ جوابهاشون به a
و b
انقیاد و به return
داده میشن.
پس تا اینجا دیدیم که میشه برای تابعهای نیمه اعمالشده Functor
، Applicative
، و Monad
داشته باشیم. در همهی این حالتهاشون یک آرگومان میگیرن تا هر دو تابعشون محاسبه بشه. Functor
ِ توابع همون ترکیب توابع ِه. Applicative
و Monad
علاوه بر ترکیب، آرگومان رو همزمان به همهی تابعها میدن (اپلیکتیوها و موندها هردو حالتهای خاصی از فانکتورها اند، پس در ذاتشون اون رفتارِ فانکتوری رو دارن).
این ایدهی Reader
ِه؛ راهی برای زنجیرکردنِ تابعهاییه که همهشون منتظرِ یک ورودی از یه محیطِ مشترکاند. جزئیات و نحوهی کارش رو الان میگیم، اما درکِ مهمی که باید ازش پیدا کنین، اینه که Reader
راهی برای انتزاعی کردنِ اعمالِ توابع ِه؛ در نتیجه با Reader
میشه محاسبات رو بر حسبِ آرگومانی انجام داد که هنوز تأمین نشده. کاربردش معمولاً برای مواقعیه که یه مقدار ثابت رو از محیطی خارج از برنامه میگیریم و به عنوانِ آرگومان برای یه عالَم تابع ازش استفاده میکنیم. با استفاده از Reader
دیگه لازم نیست اون آرگومان رو صراحتاً به توابع بدیم.
تمرین کوتاه: نرمش
اینجا میخوایم کاری مشابهِ چیزی که بالا دیدین انجام بدیم، هم برای تمرین، هم برای اینکه یه حسی از مفاهیمِ پیشِ رو پیدا کنین. در واقع انقدر شبیهِ همونهاست که تقریباً میشه همهشو کپی/پِیست کنین، پس زیادی فکر نکنین.
اول یه فایل رو با این کُدها شروع کنین:
import Data.Char
cap :: [Char] -> [Char]
cap xs = map toUpper xs
rev :: [Char] -> [Char]
rev xs = reverse xs
دو تابعِ ساده با تایپِ یکسان. میشه با (.)
یا fmap
با هم ترکیب ِشون کنیم:
composed :: [Char] -> [Char]
composed = undefined
fmapped :: [Char] -> [Char]
fmapped = undefined
خروجیِ هردوشون باید یکسان باشه: یه نوشته که هم همهی حروفش بزرگ شدن، و هم سروته شده:
Prelude> composed "Julie"
"EILUJ"
Prelude> fmapped "Chris"
"SIRHC"
حالا میخوایم جوابِ cap
و rev
رو تو یه توپل مثلِ زیر برگردونیم:
Prelude> tupled "Julie"
("JULIE","eiluJ")
-- یا
Prelude> tupled' "Julie"
("eiluJ","JULIE")
از یه Applicative
استفاده کنین. تایپش اینطوری میشه:
tupled :: [Char] -> ([Char], [Char])
دلیلی برای موندی بودنِ چنین تابعی نیست، ولی برای تمرین این کار هم انجام بدین. یه بار با گرامر ِ do
، یه بار هم با (>>=)
تعریف کنین. تایپشون مثل تایپِ tupled
میشه.