۱۸ - ۶اعمال و ترکیب

چیزهایی که تا اینجا دیدیم، عمدتاً راجع به اعمالِ توابع بودن. خیلی حواسمون به رابطه‌ی بین اعمالِ توابع و ترکیب ِ اونها نبوده، چون با ‏‎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‎‏ برمی‌گردونه.