۱۸ - ۶اعمال و ترکیب
چیزهایی که تا اینجا دیدیم، عمدتاً راجع به اعمالِ توابع بودن. خیلی حواسمون به رابطهی بین اعمالِ توابع و ترکیب ِ اونها نبوده، چون با Functor
و Applicative
خیلی مهم نبود. در هردوی اونها توابع تایپِ عادیِ (a -> b)
داشتن و ترکیبشون به سادگی "کار میکرد؛" و صحتِ چنین چیزی با قوانینِ اون تایپکلاسها تضمین میشد:
fmap id = id
-- این رو
fmap f . fmap g = fmap (f . g)
-- تضمین میکنه
که یعنی ترکیب توابع در فانکتور به سادگی کار میکنه:
Prelude> fmap ((+1) . (+2)) [1..5]
[4,5,6,7,8]
Prelude> fmap (+1) . fmap (+2) $ [1..5]
[4,5,6,7,8]
با Monad
، اولش شاید یه کم نامأنوس باشه. سعی کنیم ترکیب برای توابعِ موندی رو تا جای ممکن ساده تعریف کنیم:
mcomp :: Monad m =>
(b -> m c)
-> (a -> m b)
-> a -> m c
mcomp f g a = f (g a)
اگه بارگذاریِش کنیم چنین خطایی میگیریم:
Couldn't match expected type ‘b’
with actual type ‘m b’
‘b’ is a rigid type variable bound
by the type signature for
mcomp :: Monad m =>
(b -> m c)
-> (a -> m b)
-> a -> m c
at kleisli.hs:21:9
Relevant bindings include
g :: a -> m b (bound at kleisli.hs:22:8)
f :: b -> m c (bound at kleisli.hs:22:6)
mcomp :: (b -> mc)
-> (a -> m b)
-> a -> m c
(bound at kleisli.hs:22:1)
In the first argument of ‘f’, namely ‘(g a)’
In the expression: f (g a)
Failed, modules loaded: none.
پس اونطوری کار نکرد. این پیغام داره میگه f
انتظارِ یه b
به عنوانِ آرگومانِ اولش داره، اما g
داره بهِش یه m b
میده. خوب چطور در حضورِ یه ساختار که میخوایم نادیده بگیریم، یه تابع رو اعمال میکردیم؟ از fmap
استفاده میکردیم. با این کار بجای m c
، یه m (m c)
میگیریم، پس باید اون دوتا ساختار موندی رو با هم join
کنیم.
mcomp :: Monad m =>
(b -> m c)
-> (a -> m b)
-> a -> m c
mcomp f g a = join (f <$> (g a))
وقتی از join
و fmap
با هم استفاده میکنیم، معنیش اینه که میشه اون دوتا رو با خودِ (>>=)
جایگزین کنیم.
mcomp'' :: Monad m =>
(b -> m c)
-> (a -> m b)
-> a -> m c
mcomp'' f g a = g a >>= f
برای ترکیب ِ توابعِ موندی لازم نیست هیچ چیزِ خاصی بنویسین (البته با فرضِ اینکه بافتهای موندی، همهشون یک Monad
اند)، هسکل خودش این کار رو انجام میده: اسمش هم ترکیبِ کلایزلیِه. نگران نشین؛ به عجیبیِ اسمش نیست. الان دیدیم که همون ترکیبِ توابع بر مبنای بایند ِه تا بشه با حضورِ یه ساختار ِ اضافه، توابع رو ترکیب کنیم.
تایپهای عملگر ِ ترکیب ِ معمولیِ توابع و تابعِ بایند رو با هم مقایسه کنیم:
(.) :: (b -> c) -> (a -> b) -> a -> c
(>>=) :: Monad m => m a -> (a -> m b) -> m b
برای رسیدن به ترکیب کلایزلی، باید چندتا از آرگومانها رو جابجا کنیم تا تایپها کار کنن:
import Control.Monad
-- بشه >>= ترتیبها جابجا شدن تا شبیه
(>=>)
:: Monad m
=> (a -> m b) -> (b -> m c) -> a -> m c
شباهتش با یکی از چیزهایی که بلدین رو میبینین؟
(>=>)
:: Monad m
=> (a -> m b) -> (b -> m c) -> a -> m c
flip (.)
:: (a -> b) -> (b -> c) -> a -> c
همون ترکیبِ توابع ِه، فقط ساختارهای موندی بهش آویزون شدن. یه مثال ببینیم!
import Control.Monad ((>=>))
sayHi :: String -> IO String
sayHi greeting = do
putStrLn greeting
getLine
readM :: Read a => String -> IO a
readM = return . read
getAge :: String -> IO Int
getAge = sayHi >=> readM
askForAge :: IO Int
askForAge =
getAge "Hello! How old are you? "
با ترکیب ِ return
و read
تابعی درست کردیم که بعد از بایند روی خروجیِ sayHi
یه ساختار تأمین میکنه. عملگر ِ کلایزلی رو برای چسبوندنِ sayHi
و readM
به هم لازم داشتیم:
sayHi :: String -> IO String
readM :: Read a => String -> IO a
-- [1] [2] [3]
(a -> m b)
String -> IO String
-- [4] [5] [6]
-> (b -> m c)
String -> IO a
-- [7] [8] [9]
-> a -> m c
String -> IO a
۱.
اولین تایپ، تایپِ ورودی به sayHi
ِه، یعنی String
.
۲.
IO
یی که sayHi
اجرا میکنه تا سلام کنه (پیغامش رو چاپ کنه) و ورودی بگیره.
۳.
ورودی ِ String
از کاربر که sayHi
برمیگردونه.
۴.
مقدار String
ای که sayHi
تولید میکنه و readM
به عنوانِ آرگومان میگیره.
۵.
IO
یی که readM
جوابش رو داخلِ اون برمیگردونه. دقت کنین که return
یا pure
مقادیرِ IO
یی درست میکنن که هیچ I/O یی اجرا نمیکنن.
۶.
Int
ای که readM
برمیگردونه.
۷.
ورودی ِ String
ِ اصلی که sayHi
انتظار داره تا بدونه با چه پیغامی به مخاطب سلام کنه و سنِشون رو بپرسه.
۸.
اجراییه ِ IO
ِ نهایی که همهی اثراتِ لازم رو اجرا میکنه تا جوابِ آخر رو تولید کنه.
۹.
مقدارِ داخلِ اجراییه ِ IO
ِ نهایی؛ در این مورد یه مقدارِ Int
که readM
برمیگردونه.