۲۱ - ۶توابع Applicative هم دارن

تا اینجا چندتا مثال از ‏‎Applicative‎‏ ِ توابع و طرز کارشون دیدیم. حالا میریم سراغ جزئیات.

اول نحوه‌ی اختصاصی‌سازی تایپ‌ها رو ببینیم:

-- Applicative f =>

-- f ~ (->) r

pure :: a ->     f a
pure :: a -> (r -> a)

(<*>) ::    f (a -> b)
         ->     f a
         ->     f b
(<*>) :: (r -> a -> b)
         -> (r -> a)
         -> (r -> b)

همونطور که در نمونه ِ ‏‎Functor‎‏ دیدیم، ‏‎r‎‏ در ‏‎Reader‎‏ بخشی از ساختار ِ ‏‎f‎‏ ِه. تو این تابع دو آرگومان داریم که هردوشون منتظرِ یه آرگومانِ ‏‎r‎‏ هستن. وقتی اون آرگومان میرسه، هردو تابع اعمال میشن تا یه جواب نهاییِ ‏‎b‎‏ برگردونه.

نمایشِ ‏‎Applicative‎‏ ِ تابع

این مثال شبیه بقیه‌ی مثال‌هایی‌ه که تو کتاب زدیم، ولی تو این مثال هدف‌مون نشون دادنِ یه کاربردِ بخصوص از ‏‎Applicative‎‏ ِ توابع‌ه که عموماً استفاده میشه. با چندتا ‏‎newtype‎‏ برای تمایزِ ‏‎String‎‏‌های مختلف استفاده می‌کنیم:

newtype HumanName =
  HumanName String 
  deriving (Eq, Show)

newtype DogName =
  DogName String 
  deriving (Eq, Show) 

newtype Address =
  Address String 
  deriving (Eq, Show)

این کار رو کردیم تا تایپ‌ها گویاتر باشن و یه موقع با هم قاطی‌شون نکنیم. کار با یه تایپ مثل این:

String -> String -> String

در دو حالتِ زیر سخت میشه:

۱.

هرکدوم‌شون قرار نیست هر ‏‎String‎‏ ای باشن.

۲.

همه‌شون به یه نحو پردازش نمیشن. مثلاً با آدرس‌ها و اسم‌ها یه جور برخورد نمیشه.

پس تفاوت رو صریح کنین.

دوتا تایپِ رکورد هم درست می‌کنیم:

data Person =
  Person {
    humanName :: HumanName
  , dogName :: DogName
  , address :: Address
  } deriving (Eq, Show)

data Dog =
  Dog {
    dogsName :: DogName 
  , dogsAddress :: Address
  } deriving (Eq, Show)

چندتا داده برای آزمایش. اگه دوست داشتین تغییرشون بدین:

pers :: Person
pers =
  Person (HumanName "Big Bird")
         (DogName "Barkley")
         (Address "Sesame Street")

chris :: Person
chris = Person (HumanName "Chris Allen")
               (DogName "Papu")
               (Address "Austin")

تابع‌مون رو با ‏‎Reader‎‏ و بدون اون اینطوری تعریف می‌کردیم:

-- Reader بدون
getDog :: Person -> Dog
getDog p =
  Dog (dogName p) (address p)

-- Reader با
getDogR :: Person -> Dog
getDogR =
  Dog <$> dogName <*> address 

‏‎Reader‎‏ رو نمی‌بینین؟ اگه یه ذره تایپ‌ها رو معین‌تر کنیم چطور؟

(<$->>) :: (a -> b) 
        -> (r -> a)
        -> (r -> b)
(<$->>) = (<$>)

(<*->>) :: (r -> a -> b) 
        -> (r -> a)
        -> (r -> b)
(<*->>) = (<*>)

-- Reader با
getDogR' :: Person -> Dog
getDogR' =
  Dog <$->> dogName <*->> address

چیزی که می‌خوایم نشون بدیم اینه که ریدِر همیشه ‏‎Reader‎‏ نیست، گاهی اوقات ‏‎Applicative‎‏ یا ‏‎Monad‎‏ ِ منسوب به تایپِ توابعِ نیمه اعمال‌شده هست، که اینجا میشه ‏‎r ->‎‏.

این الگو برای این استفاده از ‏‎Applicative‎‏ خیلی رایج‌ه، با ‏‎liftA2‎‏ از یه راهِ دیگه میشه تعریف‌ش کرد:

import Control.Applicative (liftA2)

-- یه تعریف دیگه ،Reader با
getDogR' :: Person -> Dog
getDogR' =
  liftA2 Dog dogName address

تایپِ ‏‎liftA2‎‏:

liftA2 :: Applicative f =>
          (a -> b -> c)
       -> f a -> f b -> f c

باز هم منتظرِ ورودی از جای دیگه هستیم. بجای اینکه یه آرگومان بدیم و همه‌ی توابع رو بهش اعمال کنیم، آرگومان رو حذف می‌کنیم و بقیه کارها رو می‌سپریم به تایپ‌ها.

تمرین: درک مطلب

۱.

تابعِ ‏‎liftA2‎‏ رو خودتون بنویسین. میشه بهش مثل تجرید ِ تفاوتِ بینِ ‏‎getDogR‎‏ و ‏‎getDogR'‎‏ فکر کرد.

liftA2 :: Applicative f =>
          (a -> b -> c)
       -> f a -> f b -> f c
liftA2 = undefined

۲.

تابعِ زیر رو تعریف کنین. باز هم ساده‌تر از اون چیزی‌ه که به نظر میرسه.

asks :: (r -> a) -> Reader r a
asks f = Reader ???

۳.

نمونه ِ ‏‎Applicative‎‏ برای ‏‎Reader‎‏ رو تعریف کنین.

برای نوشتنِ این نمونه ِ ‏‎Applicative‎‏ از یه توسعه به اسمِ ‏‎InstanceSigs‎‏ استفاده می‌کنیم. با این توسعه‌ی زبانی میشه تایپِ متود ِ تایپکلاس‌ها رو نوشت. در حالتِ عادی نمیشه برای نمونه‌ها تایپ سیگنچر نوشت. کامپایلر از تایپِ توابع خبر داره، پس معمولاً نیازی به تعیینِ تایپ‌ها نیست. ما برای وضوحِ بیشتر این کار رو کردیم تا تایپِ ‏‎Reader‎‏ رو صراحتاً در تایپ سیگنچرها نشون بدیم.

-- این پراگما لازمه
{-# LANGUAGE InstanceSigs #-}

instance Applicative (Reader r) where
  pure :: a -> Reader r a
  pure a = Reader $ ???

  (<*>) :: Reader r (a -> b)
        -> Reader r a
        -> Reader r b
  (Reader rab) <*> (Reader ra) =
    Reader $ \r -> ???

چندتا راهنمایی:

a)

برای ‏‎pure‎‏ خاطرتون باشه که می‌خواین یه تابعی بسازین که یه مقدار از تایپِ ‏‎r‎‏ (که هیچی ازش نمی‌دونین) می‌گیره، و یه مقدار با تایپِ ‏‎a‎‏ برمی‌گردونه. با توجه به اینکه هیچ کاری با ‏‎r‎‏ نمی‌کنین، کلاً یه جواب بیشتر نداره.

b)

تعریفِ تابعِ اپلای رو براتون شروع کردیم، حالا کاری که باید انجام بده رو توصیف می‌کنیم، شما هم کُدِش رو بنویسین. اگه تایپِ ‏‎Reader‎‏ رو باز کنین، تایپ‌ش اینطور میشه:

(<*>) :: (r -> a -> b) 
      -> (r -> a)
      -> (r -> b)

-- مقایسه کنین fmap این رو با تایپ

fmap :: (a -> b) 
     -> (r -> a)
     -> (r -> b)

فرق‌شون چیه؟ فرق اینجاست که اپلای برخلافِ ‏‎fmap‎‏، یه آرگومان از تایپِ ‏‎r‎‏ هم می‌گیره.

ببینیم چه می‌کنین.