۱۷ - ۵اپلیکتیو در عمل

حتماً تا اینجا متوجه شدین که خیلی از نوع‌داده‌هایی که در دو فصلِ قبلی باهاشون کار کردیم، نمونه ِ ‏‎Applicative‎‏ هم دارن. حالا که دیگه حسابی با لیست و ‏‎Maybe‎‏ آشنا شدیم، اینجا هم با همون‌ها شروع می‌کنیم. بعداً تو این فصل تایپ‌های جدیدی معرفی می‌کنیم، پس فعلاً دندون رو جیگر بذارین!

اپلیکتیو ِ لیست

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

-- f ~ []

(<*>) ::  f  (a -> b)  ->  f  a ->  f  b
(<*>) :: [ ] (a -> b)  -> [ ] a -> [ ] b

-- با گرامر رایج‌تر
(<*>) ::    [(a -> b)] -> [a] ->      [b]

pure :: a ->   f a
pure :: a ->  [ ] a

یا باز هم اگه ۸ GHC یا جدیدتر دارین، بجاش می‌تونین این کار رو انجام بدین:

Prelude> :set -XTypeApplications
Prelude> :type (<*>) @[]
(<*>) @[] :: [a -> b] -> [a] -> [b]
Prelude> :type pure @[]
pure @[] :: a -> [a]

اپلیکتیو ِ لیست چه کاری انجام میده؟

قبلاً با ‏‎Functor‎‏، یه تابعِ مجرد رو روی جمیعی از مقادیر اعمال می‌کردیم:

Prelude> fmap (2^) [1, 2, 3]
[2,4,8]
Prelude> fmap (^2) [1, 2, 3]
[1,4,9]

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

Prelude> [(+1), (*2)] <*> [2, 4]
[3,5,4,8]

با توجه به:

(<*>) :: Applicative f
      => f (a -> b) -> f a -> f b

-- f ~ []

listApply :: [(a -> b)] -> [a] -> [b]

listFmap  ::  (a -> b)  -> [a] -> [b]

ساختاری که در تابعِ ‏‎listApply‎‏ تابع‌مون رو پوشونده، خودش لیست‌ه. پس ‏‎a -> b‎‏ از ‏‎Functor‎‏ شده یه لیست از ‏‎a -> b‎‏.

پس اون بیانیه‌ای که مثال زدیم چطور کار کرد؟

[(+1), (*2)] <*> [2, 4] == [3,5,4,8]

    [ 3 , 5 , 4 , 8 ]
--   [1] [2] [3] [4]

۱.

اولین المانِ لیست (۳)، از اعمال ِ (۱+) به ۲ نتیجه شده.

۲.

۵ نتیجه‌ی اعمال ِ (۱+) به ۴ ِه.

۳.

۴ هم از اعمال ِ (۲*) به ۲.

۴.

۸ هم از اعمال ِ (۲*) به ۴.

بصری‌تر:

[(+1), (*2)] <*> [2, 4]

[(+1) 2 , (+1) 4 , (*2) 2 , (*2) 4 ]

تک‌تکِ مقادیرِ تابعی در لیست اول رو به لیستِ دوم نگاشت می‌کنه، عملیات‌ها رو اعمال می‌کنه، و یه لیست برمی‌گردونه. اینکه دوتا لیست، یا یه لیستِ تودرتو، یا هر ترکیب دیگه‌ای که هردو ساختار رو حفظ کرده باشه برنمی‌گردونه، مربوط به بخش مانویدی میشه؛ و اینکه یه لیستِ توابع، الحاق شده با یه لیستِ مقادیر نداریم، مربوط به بخشِ اعمال تابع میشه.

به کمکِ توپل‌ساز به همراهِ ‏‎Applicative‎‏ ِ لیست، این رابطه رو بهتر میشه دید. از عملگر ِ میانوند ِ ‏‎fmap‎‏ برای نگاشت ِ توپل‌ساز ‏‎(,)‎‏ روی لیستِ اول استفاده می‌کنیم. چنین کاری، یه تابعِ اعمال‌نشده (در این مورد، توپل‌ساز) رو می‌بره به داخلِ یه ساختار (در این مورد، لیست)، و یه لیست از توابعِ نیمه اعمال‌شده برمی‌گردونه. بعد، اپلیکتیو (میانوند یه لیست از عملیات‌ها رو به لیستِ دوم اعمال می‌کنه، و دو لیست رو به صورتِ مانویدی با هم ترکیب می‌کنه:

Prelude> (,) <$> [1, 2] <*> [3, 4]
[(1,3),(1,4),(2,3),(2,4)]

میشه اینطور بهش فکر کرد:

Prelude> (,) <$> [1, 2] <*> [3, 4]

-- ‎‏روی لیست اول‏‎ (,) کردنِ fmap
[(1, ), (2, )] <*> [3, 4]

-- بعد لیست اول رو به
-- لیست دوم اعمال می‌کنیم
[(1,3),(1,4),(2,3),(2,4)]

با تابعِ ‏‎liftA2‎‏ هم میشه این رو به طریقِ دیگه نوشت:

Prelude> liftA2 (,) [1, 2] [3, 4]
[(1,3),(1,4),(2,3),(2,4)]

چندتا مثالِ مشابهِ دیگه ببینیم:

Prelude> (+) <$> [1, 2] <*> [3, 4]
[4,6,5,7]
Prelude> liftA2 (+) [1, 2] [3, 4]
[4,6,5,7]

Prelude> max <$> [1, 2] <*> [1, 4]
[1,4,2,4]
Prelude> liftA2 max [1, 2] [1, 4] 
[1,4,2,4]

اگه با ضرب کارتزین* آشنا هستین، این احتمالاً خیلی شبیهِ اونه، فقط با توابع‌ه.

*

ضرب کارتزین حاصلضرب دو مجموعه هست که جواب‌ش هم جفت (توپل) های مرتب شده از المان‌های اون مجموعه‌هاست.

چندتا مثالِ دیگه میزنیم تا درکِ بهتری از موارد کاربرد این تابع‌ها پیدا کنین. مثال‌های زیر از تابعی به اسمِ ‏‎lookup‎‏ استفاده می‌کنن که خیلی مختصر نشون‌ش میدیم:

Prelude> :t lookup
lookup :: Eq a => a -> [(a, b)] -> Maybe b

Prelude> let l = lookup 3 [(3, "hello")]
Prelude> l
Just "hello"
Prelude> fmap length $ l
Just 5
Prelude> let c (x:xs) = toUpper x:xs
Prelude> fmap c $ l
Just "Hello"

پس ‏‎lookup‎‏ داخل یه لیستِ توپل دنبال توپلی میگرده که مقدارِ اول‌ش با مقدار ورودیِ تابع جور بشه، و مقدارِ جفتِ اون رو با یه بافت ِ ‏‎Maybe‎‏ برمی‌گردونه.

اگه بجای لیستِ توپل‌ها با ساختارِ داده ِ ‏‎Map‎‏ کار می‌کنین، با وارد کردنِ ‏‎Data.Map‎‏ می‌تونین با استفاده از ‏‎fromList‎‏، از یه نسخه‌ی ‏‎Map‎‏ ِ تابعِ ‏‎lookup‎‏ برای همون کار استفاده کنین:

Prelude> let m = fromList [(3, "hello")]
Prelude> fmap c $ Data.Map.lookup 3 m
Just "Hello"

شاید خیلی پیش‌وپاافتاده به نظر بیاد، اما ‏‎Map‎‏ یه ساختارِ داده‌ای ِه که زیاد استفاده میشه، پس می‌ارزه بشناسین‌ش.

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

import Control.Applicative

f x =
  lookup x [ (3, "hello")
           , (4, "julie")
           , (5, "kbai")]

g x =
  lookup y [ (7, "sup?")
           , (8, "chris")
           , (9, "aloha")]

h z =
  lookup z [(2, 3), (5, 6), (7, 8)] 

m x =
  lookup x [(4, 10), (8, 13), (1, 9001)]

می‌خوایم دنبالِ یه چیزایی بگردیم* و با هم جمع‌شون کنیم. با چندتا عملیات روی این داده‌ها شروع می‌کنیم:

Prelude> f 3
Just "hello"
Prelude> g 8
Just "chris"
Prelude> (++) <$> f 3 <*> g 7
Just "hellosup?"
Prelude> (+) <$> h 5 <*> m 1
Just 9007
Prelude> (+) <$> h 5 <*> m 6
Nothing
*

م. look up کردن، معنای "دنبالِ چیزی گشتن" میده.

پس اول اون توابع رو روی مقدارِ داخلِ بافت ِ ‏‎Maybe‎‏ ِ اول ‏‎fmap‎‏ می‌کنیم؛ اگه یه مقدارِ ‏‎Just‎‏ بود، به یه تابعِ نیمه اعمال‌شده در بافت ِ ‏‎Maybe‎‏ میرسیم. بعد اون رو با ‏‎اَپلای‎‏ به مقدارِ دوم (که اون هم زیرِ یه ‏‎Maybe‎‏ هست) اعمال می‌کنیم. هرکدوم از مقادیر ‏‎Nothing‎‏ باشن، ‏‎Nothing‎‏ می‌گیریم.

باز هم با ‏‎liftA2‎‏ میشه همون کار رو انجام بدیم:

Prelude> liftA2 (++) (g 9) (f 4)
Just "alohajulie"
Prelude> liftA2 (^) (h 5) (m 4)
Just 60466176
Prelude> liftA2 (*) (h 5) (m 4)
Just 60
Prelude> liftA2 (*) (h 1) (m 1)
Nothing

بافت ِ اپلیکتیو ممکنه ‏‎IO‎‏ هم باشه:

(++) <$> getLine <*> getLine
(,) <$> getLine <*> getLine

امتحان کنین. حالا سعی کنین با ‏‎fmap‎‏ طولِ نوشته ِ حاصل در مثالِ اول رو پیدا کنین.

تمرین‌ها: ‏‎lookup‎‏

با استفاده از توابعِ ‏‎pure‎‏، ‏‎(<$>)‎‏، و ‏‎(<*>)‎‏ کاری کنین که بیانیه‌های زیر تایپچِک بشن.

۱.

added :: Maybe Integer
added =
  (+3) (lookup 3 $ zip [1, 2, 3] [4, 5, 6])

۲.

y :: Maybe Integer
y = lookup 3 $ zip [1, 2, 3] [4, 5, 6]

z :: Maybe Integer
z = lookup 2 $ zip [1, 2, 3] [4, 5, 6]

tuple :: Maybe (Integer, Integer)
tuple = (,) y z

۳.

import Data.List (elemIndex)

x :: Maybe Int
x = elemIndex 3 [1, 2, 3, 4, 5] 

y :: Maybe Int
y = elemIndex 4 [1, 2, 3, 4, 5] 

max' :: Int -> Int -> Int
max' = max

maxed :: Maybe Int
maxed = max' x y

۴.

xs = [1, 2, 3]
ys = [4, 5, 6]

x :: Maybe Integer
x = lookup 3 $ zip xs ys

y :: Maybe Integer
y = lookup 2 $ zip xs ys

summed :: Maybe Integer
summed = sum $ (,) x y

همانی

با تایپِ ‏‎Identity‎‏ میشه بدونِ تغییر در مفهومِ کاری که داریم انجام میدیم‌، ساختار معرفی کنیم. ازش برای این تایپکلاس‌هایی که مرتبط با اعمال تابع از روی ساختار هستن استفاده می‌کنیم، ولی خودش به تنهایی جذابیتی نداره، چون هیچ مفهومی اضافه نمی‌کنه.

-- f ~ Identity
-- Applicative f =>
type Id = Identity

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

pure :: a ->  f a
pure :: a -> Id a

چرا با ‏‎Identity‎‏ ساختار معرفی کردیم؟ معنیِ این کارها چیه؟

Prelude> let xs = [1, 2, 3]
Prelude> let xs' = [9, 9, 9]
Prelude> const <$> xs <*> xs'
[1,1,1,2,2,2,3,3,3]
Prelude> let mkId = Identity
Prelude> const <$> mkId xs <*> mkId xs'
Identity [1,2,3]

به خاطرِ این ساختار ِ اضافه اطرافِ مقادیرمون، بجای نگاشت ِ تابعِ ‏‎const‎‏ روی لیست‌ها، تابع از روی ‏‎Identity‎‏ نگاشت شد. باید از روی یه ساختار ِ ‏‎f‎‏ رَد شیم تا تابع رو به مقادیرِ داخل اعمال کنیم. اگه اون ‏‎f‎‏ لیست باشه، مثلِ بالا، ‏‎const‎‏ به مقادیرِ داخلِ لیست اعمال میشه. اگر هم ‏‎Identity‎‏ باشه، اونموقع ‏‎const‎‏ بجای اینکه با اون لیست‌ها مثلِ ساختار‌های شاملِ مقدار رفتار کنه، مثلِ مقادیرِ مجرد رفتار می‌کنه.

تمرین: نمونههای ‏‎Identity‎‏

برای ‏‎Identity‎‏ یه نمونه ِ ‏‎Applicative‎‏ بنویسین.

newtype Identity a = Identity a
  deriving (Eq, Ord, Show)

instance Functor Identity where
  fmap = undefined

instance Applicative Identity where
  pure = undefined
  (<*>) = undefined

ثابت

این تایپ فرقِ زیادی با تایپِ ‏‎Identity‎‏ نداره، فقط علاوه بر تأمینِ یه ساختار، مشابهِ تابع ‏‎const‎‏ هم عمل می‌کنه. یه جورایی اعمالِ تابع رو دور میندازه. شاید به نظر گیج کننده بیاد، چون واقعاً هست. با این حال، این تایپ هم مثلِ ‏‎Identity‎‏ کاربردِ واقعی داره و ممکنه در کُدِ بقیه‌ی مردم ببینین‌ش. احتمالاً عادت به استفاده ازش در کُدهای خودتون سخت باشه، ولی ما به تلاش‌مون ادامه میدیم.

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

اختصاصی‌سازی تایپ‌ها

خیلی خوب، تایپ‌ها اینطور میشن:

-- f ~ Constant e
type C = Constant

(<*>) ::   f (a -> b) ->   f a ->   f b
(<*>) :: C e (a -> b) -> C e a -> C e b

pure :: a ->   f a
pure :: a -> C e a

اینجا هم چندتا مثال از طرزِ کارش. قبول داریم، این مثال‌ها یه کم ساختگی و "زورَکی" اند، اما اگه کُدِ واقعی با این تایپ رو نشون‌تون میدادیم، فهمیدن‌ش خیلی سخت‌تر میشد:

Prelude> let f = Constant (Sum 1)
Prelude> let g = Constant (Sum 2)
Prelude> f <*> g
Constant {getConstant = Sum {getSum = 3}}
Prelude> Constant undefined <*> g
Constant {getConstant = Sum {getSum =
  *** Exception: Prelude.undefined
Prelude> pure 1
1
Prelude> pure 1 :: Constant String Int
Constant {getConstant = ""}

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

تمرین: نمونههای ‏‎Constant‎‏

برای ‏‎Constant‎‏ یه نمونه ِ ‏‎Applicative‎‏ بنویسین.

newtype Constant a b =
  Constant { getConstant :: a }
  deriving (Eq, Ord, Show)

instance Functor (Constant a) where
  fmap = undefined

instance Monoid a
      => Applicative (Constant a) where
  pure = undefined
  (<*>) = undefined

اپلیکتیو ِ ‏‎Maybe‎‏

کاری که با ‏‎Maybe‎‏ انجام میدیم، یه کم با نوع‌داده‌های قبل فرق داره. قبلاً دیدیم چطور از ‏‎fmap‎‏ با ‏‎Maybe‎‏ استفاده کنیم، اما اینجا تابع‌مون هم داخلِ یه ساختار ِ ‏‎Maybe‎‏ ِه. بنابراین وقتی ‏‎f‎‏ میشه ‏‎Maybe‎‏، در واقع داریم میگیم که خودِ تابع هم ممکنه وجود نداشته باشه، چون حالتی رو اجازه میدیم که تابعی که می‌خوایم اعمال کنیم هم ‏‎Nothing‎‏ باشه.

اختصاصی‌سازیِ تایپ‌ها

وقتی از ‏‎Maybe‎‏ به عنوانِ ساختار ِمون استفاده کنیم، تایپ‌ها اینطوری میشن:

-- f ~ Maybe
type M = Maybe

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

pure :: a -> f a
pure :: a -> M a

آماده‌این چند نفر رو تأیید کنین؟ معلومه که هستین.

استفاده از اپلیکتیو ِ ‏‎Maybe‎‏

در مثالِ زیر، ورودی‌ها رو برای ساختِ یه مقدار از تایپِ ‏‎Maybe Person‎‏ تأیید می‌کنیم، دلیلِ استفاده از ‏‎Maybe‎‏ هم به خاطرِ امکانِ معتبر نبودنِ ورودی‌هاست:

validateLength :: Int
               -> String
               -> Maybe String
validateLength maxLen s =
  if (length s) > maxLen
  then Nothing
  else Just s

newtype Name =
  Name String deriving (Eq, Show)
newtype Address =
  Address String deriving (Eq, Show)

mkName :: String -> Maybe Name
mkName s =
  fmap Name $ validateLength 25 s

mkAddress :: String -> Maybe Address
mkAddress a =
  fmap Address $ validateLength 100 a

حالا یه سازنده‌ی هوشمند برای ‏‎Person‎‏ درست می‌کنیم:

data Person =
  Person Name Address
  deriving (Eq, Show)

mkPerson :: String
         -> String
         -> Maybe Person
mkPerson n a =
  case mkName n of
   Nothing -> Nothing
   Just n' ->
     case mkAddress a of
      Nothing -> Nothing
      Just a' ->
        Just $ Person n' a'

با اینکه اینجا به موفقیت از تابعِ ‏‎fmap‎‏ از ‏‎Functor‎‏ در تابع‌های ساده‌ترِ ‏‎mkName‎‏ و ‏‎mkAddress‎‏ استفاده کردیم، با ‏‎mkPerson‎‏ نمیشه ازش استفاده کرد. ببینیم چرا:

Prelude> :t fmap Person (mkName "Friend")
fmap Person (mkName "Friend")
  :: Maybe (Address -> Person)

تا اینجا برای اولین آرگومانِ سازنده ِ ‏‎Person‎‏ (که می‌خوایم تأیید کنیم) کار کرد، ولی به بن‌بست خوردیم. مشکل رو می‌بینین؟

Prelude> :{
*Main| fmap (fmap Person (mkName "Friend"))
*Main|      (mkAddress "old macdonald's")
*Main| :}
Couldn't match expected type ‘Address -> b’
with actual type
  ‘Maybe (Address -> Person)’

Possible cause: ‘fmap’ is applied to too
  many arguments
In the first argument of ‘fmap’, namely
  ‘(fmap Person (mkName "Friend"))’

In the expression:
  fmap (fmap Person (mkName "Friend"))
       (mkAddress "old macdonald's")

مشکل اینجاست که الان دیگه ‏‎(a -> b)‎‏ توی ‏‎Maybe‎‏ قایِم شده. یه بار دیگه تایپِ ‏‎fmap‎‏ رو ببینیم:

fmap :: Functor f => (a -> b) -> f a -> f b

قطعاً ‏‎Maybe‎‏ یه ‏‎Functor‎‏ هست، ولی اینجا خیلی کمکی نمی‌کنه. چیزی که لازم داریم اینه که بتونیم تابعی که داخلِ ‏‎f‎‏ پوشونده شده رو اعمال کنیم. ‏‎Applicative‎‏ دقیقاً همون چیزی که لازم داریم رو میده!

(<*>) :: Applicative f
      => f (a -> b) -> f a -> f b

حالا ببینیم چطور میشه از این اسباب‌بازیِ جدید استفاده کنیم:

Prelude> let s = "old macdonald's"
Prelude> let addy = mkAddress s
Prelude> let b = mkName "Friend"
Prelude> let person = fmap Person b
Prelude> person <*> addy
Just (Person (Name "Friend")
             (Address "old macdonald's"))

خوبه، ولی یه کم زشت شده. اگه از مستعار ِ میانوند ِ ‏‎fmap‎‏ یعنی ‏‎<$>‎‏ استفاده کنیم، یه کم تر و تمیز میشه (حداقل به چشم هسکل‌نویس‌ها):

Prelude> Person <$> mkName "Friend" <*> addy
Just (Person (Name "Friend")
             (Address "old macdonald's"))

هنوز هم برای اولین لیفت از روی ‏‎Maybe‎‏، از ‏‎fmap‎‏ (با مستعار ِ ‏‎<$>‎‏) استفاده می‌کنیم. بعد از اون دیگه ‏‎(a -> b)‎‏ زیرِ ‏‎f‎‏ قایم میشه، که ‏‎Maybe = f‎‏، پس اگه بخوایم به نگاشت ادامه بدیم باید از اینجا به بعد از ‏‎Applicative‎‏ استفاده کنیم.

حالا ‏‎mkPerson‎‏ رو می‌تونیم خیلی کوتاه‌تر تعریف کنیم!

mkPerson :: String
         -> String
         -> Maybe Person
mkPerson n a =
  Person <$> mkName n <*> mkAddress a

مزیتِ دیگه‌ای که داره، اگه خواستیم به ‏‎Person‎‏ فیلد اضافه کنیم، ویرایشِ این تابع هم خیلی خیلی ساده‌تره.

بررسیِ اون مثال

همون کاری که با فولد‌ها کردیم، با نمونه‌های ‏‎Functor‎‏ و ‏‎Applicative‎‏ برای ‏‎Maybe‎‏ هم انجام میدیم. یه کم طولانی‌ه. بعضی جاها شاید به نظر برسه زیادی با جزئیات‌ه؛ با هر عمقی که حس می‌کنین لازمه بخونین. همین‌جا نشسته و با حوصله منتظر می‌مونه هر وقت اگه لازم داشتین دوباره با دقتِ بیشتری بخونین‌ش.

فانکتور ِ ‏‎Maybe‎‏ و سازنده ِ ‏‎Name‎‏

instance Functor Maybe where
    fmap _ Nothing    = Nothing
    fmap f (Just a)   = Just (f a)

instance Applicative Maybe where
    pure = Just

    Nothing <*> _     = Nothing
    _ <*> Nothing     = Nothing
    Just f <*> Just a = Just (f a)

نمونه ِ ‏‎Applicative‎‏ ِ بالا دقیقاً عینِ نمونه ِ توی ‏‎base‎‏ نیست، خواستیم ساده‌تر بشه که اینطور نوشتیم. برای کارِ ما، همون خروجی‌ها رو میده.

اول تعاریفِ تابع و نوع‌داده برای فانکتور‌ِمون، نحوه‌ی استفاده‌مون از تابعِ ‏‎validateLength‎‏ با ‏‎Name‎‏ و ‏‎Address‎‏ رو تشریح می‌کنه:

validateLength :: Int
               -> String
               -> Maybe String
validateLength maxLen s =
  if (length s) > maxLen
  then Nothing
  else Just s

newtype Name =
  Name String deriving (Eq, Show)

newtype Address =
  Address String deriving (Eq, Show)

mkName :: String -> Maybe Name
mkName s =
  fmap Name $ validateLength 25 s

mkAddress :: String -> Maybe Address
mkAddress a =
  fmap Address $ validateLength 100 a

حالا تو تعاریف جاگذاری می‌کنیم و مثلِ کاری که در فصل فولدها کردیم، اونها رو باز می‌کنیم.

اول ‏‎mkName‎‏ رو به مقدارِ ‏‎"friend"‎‏ اعمال می‌کنیم تا ‏‎s‎‏ به اون نوشته مقیّد بشه:

mkName s =
  fmap Name $ validateLength 25 s
mkName "friend" =
  fmap Name $ validateLength 25 "friend"

حالا اول باید ببینیم قضیه‌ی ‏‎validateLength‎‏ چیه، چون اول باید بدونیم چیه تا ‏‎fmap‎‏ بتونه از روش نگاشت کنه. اینجا داریم به ۲۵ و ‏‎"friend"‎‏ اعمال‌ِش می‌کنیم، طول ِ نوشته‌مون یعنی ‏‎"friend"‎‏ رو محاسبه می‌کنیم، و بعد تعیین می‌کنیم کدوم شاخه از ‏‎if-then-else‎‏ برنده میشه:

validateLength :: Int
               -> String
               -> Maybe String
validateLength 25 "friend" =
  if (length "friend") > 25
  then Nothing
  else Just "friend"

  if 6 > 25
  then Nothing
  else Just "friend"

-- ‎‏بزرگتر نیست، پس:‏‎ 25 ‎‏از‏‎ 6
validateLength 25 "friend" =
  Just "friend"

حالا بجای اعمال ِ ‏‎validateLength‎‏ به ۲۵ و ‏‎"friend"‎‏، جواب‌ش رو میذاریم، بعد هم میریم سراغ نتیجه‌ی ‏‎fmap‎‏ کردنِ ‏‎Name‎‏ روی ‏‎Just "friend"‎‏:

mkName "friend" =
  fmap Name $ Just "friend"

fmap Name $ Just "friend"

اگه تایپِ ‏‎fmap‎‏ رو به خاطر بیاریم، می‌بینیم که داده‌ساز ِ ‏‎Name‎‏ همون تابعِ ‏‎(a -> b)‎‏ ِه که می‌خوایم از روی ساختار ِ فانکتوری ِ ‏‎f‎‏ نگاشت بدیم. در این مورد، معادلِ ‏‎Maybe‎‏ ِه. اون ‏‎a‎‏ هم در ‏‎f a‎‏، ‏‎String‎‏ ِه:

(a -> b) -> f a -> f b

:t Name          :: String -> Name
:t Just "friend" :: Maybe String

type M = Maybe

     (a -> b)    -> f a      -> f b
(String -> Name) -> M String -> M String

می‌دونیم با نمونه‌ ِ ‏‎Functor‎‏ از ‏‎Maybe‎‏ سروکار داریم:

fmap _ Nothing  = Nothing
fmap f (Just a) = Just (f a)

-- ،ه ِJust "friend" مقدارمون
-- رو رد می‌کنیم Nothing پس حالتِ
-- fmap _ Nothing = Nothing

fmap f    (Just a)        =
  Just (f a)
fmap Name (Just "friend") =
  Just (Name "friend")

mkName "friend" = fmap Name $ Just "friend"
mkName "friend" = Just (Name "friend")
--                f     b

‏‎Person‎‏ و اپلیکتیو ِ ‏‎Maybe‎‏

data Person =
  Person Name Address
  deriving (Eq, Show)

اول از ‏‎Functor‎‏ استفاده می‌کنیم تا داده‌ساز ِ ‏‎Person‎‏ رو از روی مقدارِ ‏‎Maybe Name‎‏ نگاشت کنیم. برخلافِ ‏‎Name‎‏ و ‏‎Address‎‏ که یک آرگومان می‌گیرن، ‏‎Person‎‏ دو آرگومان می‌گیره.

    Person
<$> Just (Name "friend")
<*> Just (Address "farm")

fmap Person (Just (Name "friend"))

:t Person :: Name -> Address -> Person

:t Just (Name "friend") :: Maybe Name

(a -> b) -> f a -> f b
  (Name -> Address -> Person)
--   a  -> b
-> Maybe Name -> Maybe (Address -> Person)
-- f     a         f             b


fmap _ Nothing  = Nothing
fmap f (Just a) = Just (f a)

fmap Person (Just (Name "friend"))

f :: Person
a :: Name "friend"

-- تطبیق الگوی
-- fmap _ Nothing = Nothing
-- داریم Just رو رد می‌کنیم چون

fmap f      (Just a)               =
  Just (f a)

fmap Person (Just (Name "friend")) =
  Just (Person (Name "friend"))

مشکل اینجاست که ‏‎Person (Name "friend")‎‏ منتظرِ یه آرگومانِ دیگه‌ست (آدرس) پس یه تابعِ نیمه اعمال‌شده هست. این همون ‏‎(a -> b)‎‏ در تایپِ ‏‎ (<*>) ‎‏ از ‏‎Applicative‎‏ ِه. ‏‎f‎‏ ای که ‏‎(a -> b)‎‏ رو می‌پوشونه ‏‎Maybe‎‏ ِه به این خاطر که ممکن بود از اولِ کار هیچ ‏‎a‎‏ ای که بهش اعمال کنیم رو نمی‌داشتیم (یعنی بجای ‏‎Just Name‎‏، مقدارِ ‏‎Nothing‎‏ می‌داشتیم):

-- منتظر یه آرگومان دیگه‌ست Person
:t Just (Person (Name "friend"))
     :: Maybe (Address -> Person)

:t Just (Address "farm") :: Maybe Address

-- می‌خوایم تابعِ نیمه اعمال‌شده‌ی
-- Just داخلِ (Person "friend")
-- .اعمال کنیم Just داخلِ "farm" رو به

    Just (Person (Name "friend"))
<*> Just (Address "farm")

پس به خاطر اینکه تابعی که می‌خوایم اعمال کنیم داخلِ همون ساختاری ِه که مقدارمون هست، تابعِ ‏‎<*>‎‏ از ‏‎Applicative‎‏ رو لازم داریم. اینجا تایپ‌ها و اختصاصی شدن‌شون برای این کاربرد رو یادآوری می‌کنیم:

f (a -> b) -> f a -> f b

type M = Maybe
type Addy = Address

M (Addy -> Person) -> M Addy -> M Person
f (  a  ->   b)    -> f a    -> f   b

میدونیم که از ‏‎Maybe Applicative‎‏ استفاده می‌کنیم، پس میشه تعریف‌ش رو اینجا هم بیاریم. فقط جهت یادآوری اینکه، این نسخه از این نمونه ِ ‏‎Applicative‎‏، نسخه‌ی ساده‌شده‌ی GHC ِه، لطفاً ایمیل نزنین از این نمونه ایراد بگیرین:

instance Applicative Maybe where
  pure = Just

  Nothing <*> _     = Nothing
  _ <*> Nothing     = Nothing
  Just f <*> Just a = Just (f a)

می‌دونیم که میشه حالت‌های ‏‎Nothing‎‏ رو نادیده بگیریم، چون تابع‌مون زیرِ ‏‎Just‎‏ ِه، مقدارمون هم ‏‎Just‎‏ ِه.

اگه بجای ‏‎f‎‏ ِ بالا سازنده ِ ‏‎Person‎‏ که ناقص اعمال شده رو جایگزین کنیم، بجای ‏‎a‎‏ هم مقدارِ ‏‎Address‎‏ رو، جواب آخر هم راحت میشه پیدا کرد.

-- نیستن Nothing تابع و مقدار، هیچ کدوم
-- پس این دو حالت رو رد می‌کنیم
-- Nothing <*> _ = Nothing
-- _ <*> Nothing = Nothing

Just  f <*> Just a = Just (f a)
Just (Person (Name "friend"))
  <*> Just (Address "farm") =
    Just (Person (Name "friend")
                 (Address "farm"))

قبل از ادامه:

data Cow = Cow {
      name   :: String
    , age    :: Int
    , weight :: Int
   } deriving (Eq, Show)

noEmpty :: String -> Maybe String
noEmpty ""  = Nothing
noEmpty str = Just str

noNegative :: Int -> Maybe Int
noNegative n | n >= 0 = Just n
             | otherwise = Nothing

-- های string برای جلوگیری از
-- خالی و اعداد منفی
cowFromString :: String
              -> Int
              -> Int
              -> Maybe Cow
cowFromString name' age' weight' =
  case noEmpty name' of
   Nothing -> Nothing
   Just nammy ->
     case noNegative age' of
      Nothing -> Nothing
      Just agey ->
        case noNegative weight' of
         Nothing -> Nothing
         Just weighty ->
           Just (Cow namey agey weighty)

اون ‏‎cowFromString‎‏... خوب نیست. احتمالاً خودتون هم تشخیص دادین. ولی با استفاده از ‏‎Applicative‎‏ میشه ارتقائش داد!

-- استفاده می‌کنین GHC <7.10 اگه از
-- باید این رو وارد کنین

import Control.Applicative

cowFromString' :: String
               -> Int
               -> Int
               -> Maybe Cow
cowFromString' name' age' weight' =
  Cow <$> noEmpty name'
      <*> noNegative age'
      <*> noNegative weight'

یا اگه بخوایم بقیه هسکل‌نویس‌ها فکر کنن ما خیلی باحال‌یم:

cowFromString'' :: String
                -> Int
                -> Int
                -> Maybe Cow
cowFromString'' name' age' weight' =
  liftA3 Cow (noEmpty name')
             (noNegative age')
             (noNegative weight')

پس اینجا از ‏‎Maybe Applicative‎‏ بهره بردیم. چه شکلی میشه؟ اول از گرامر ِ میانوندی ِ ‏‎fmap‎‏، یعنی ‏‎<$>‎‏ استفاده می‌کنیم، بعد هم ‏‎<*>‎‏ رو اعمال می‌کنیم:

Prelude> let cow1 = Cow <$> noEmpty "Bess"

Prelude> :t cow1
cow1 :: Maybe (Int -> Int -> Cow)

Prelude> let cow2 = cow1 <*> noNegative 1

Prelude> :t cow2
cow2 :: Maybe (Int -> Cow)

Prelude> let cow3 = cow2 <*> noNegative 2

Prelude> :t cow3
cow3 :: Maybe Cow

بعد هم ‏‎liftA3‎‏:

Prelude> let cow1 = liftA3 Cow

Prelude> :t cow1
cow1 :: Applicative f
     => f String -> f Int -> f Int -> f Cow

Prelude> let cow2 = cow1 (noEmpty "blah")

Prelude> :t cow2
cow2 :: Maybe Int -> Maybe Int -> Maybe Cow

Prelude> let cow3 = cow2 (noNegative 1)

Prelude> :t cow3
cow3 :: Maybe Int -> Maybe Cow

Prelude> let cow4 = cow3 (noNegative 2)

Prelude> :t cow4
cow4 :: Maybe Cow

پس اگه بخوایم ساده نگاه کنیم، با ‏‎Applicative‎‏ در واقع میگیم که:

-- فانکتوری ``f'' تابع‌مون رو روی یه
-- کردیم، یا خودش از اول fmap
-- بود ``f'' داخل یه

-- f ~ Maybe
cow1 :: Maybe (Int -> Int -> Cow)
cow1 = fmap Cow (noEmpty "Bess")

-- و به وضعیتی خوردیم که می‌خوایم
-- ،رو نگاشت کنیم f (a -> b)
-- .رو (a -> b) نه فقط
(<*>) :: Applicative f
      => f (a -> b) -> f a -> f b
-- باشه f a می‌خوایم نگاشت‌مون روی یه
-- بگیریم f b تا یه

cow2 :: Maybe (Int -> Cow)
cow2 = cow1 <*> noNegative 1

در نتیجه شاید با خودتون یه همچین چیزی بگین: "یه کاری شبیه ‏‎fmap‎‏ می‌خوام انجام بدم، ولی تابع‌م هم زیر ساختار ِ فانکتوری پوشونده شده، نه فقط مقداری که می‌خوام تابع رو بهش اعمال کنم." این یه انگیزه‌ی پایه‌ای برای ‏‎Applicative‎‏ ِه.

با نمونه ِ ‏‎Applicative‎‏ برای ‏‎Maybe‎‏، در واقع داریم اعمال ِ فانکتوری رو با قابلیتِ "شاید اصلاً هیچ تابعی نداشته باشم" تقویت می‌کنیم.

این رو میشه با این اختصاصی‌سازیِ تابعِ اَپلای (‏‎<*>‎‏) ببینیم:

(<*>) :: Applicative f
      => f (a -> b) -> f a -> f b
-- f ~ Maybe
type M = Maybe
maybeApply :: M (a -> b) -> M a -> M b
maybeFmap  ::   (a -> b) -> M a -> M b

-- ه که fmap همون maybeFmap
-- اختصاصی شده Maybe تایپ‌ش برای

این اختصاصی‌سازی‌ها (نسخه‌های معیّن‌تر) از تایپ‌ها رو می‌تونین تست کنین:

maybeApply :: Maybe (a -> b)
           -> Maybe a
           -> Maybe b
maybeApply = (<*>)

maybeMap :: (a -> b) 
         -> Maybe a
         -> Maybe b
maybeMap = fmap

اگه اشتباهی کنین، کامپایلر بهتون میگه:

maybeMapBad :: (a -> b) 
            -> Maybe a
            -> f b
maybeMapBad = fmap
Couldn't match type ‘f1’ with ‘Maybe’
‘f1’ is a rigid type variable bound by
  an expresiion type signature:
    (a1 -> b1) -> Maybe a1 -> f1 b1

تمرین‌ها: تعمیرات

با استفاده از ‏‎(<$>)‎‏ از ‏‎Functor‎‏، ‏‎(<*>)‎‏ و ‏‎pure‎‏ از تایپکلاسِ ‏‎Applicative‎‏، کُدهای خرابِ زیر رو درست کنین.

۱.

const <$> Just "Hello" <*> "World"

۲.

    (,,,) Just 90
<*> Just 10 Just "Hello" [1, 2, 3]