۱۶ - ۱۱نادیده‌گرفتن حالت‌ها

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

‏‎Maybe‎‏

با چندتا تطبیق الگو ِ ساده روی ‏‎Maybe‎‏ شروع می‌کنیم:

incIfJust :: Num a => Maybe a -> Maybe a
incIfJust (Just n) = Just $ n + 1
incIfJust Nothing = Nothing

showIfJust :: Show a => Maybe a -> Maybe String
showIfJust (Just s) = Just $ show s
showIfJust Nothing = Nothing

حوصله‌سربَره... تازه زیاده‌نویسی هم کردیم. یکی از زیاده‌نویسی‌ها حالتِ مشترک‌شون برای ‏‎Nothing‎‏ ِه:

someFunc Nothing = Nothing

بعدش هم هردوشون در صورتِ ‏‎Just‎‏ بودن، یه تابعی رو به مقدارِ زیرش اعمال می‌کنن:

someFunc (Just x) = Just $ someOtherFunc x

اگه از ‏‎fmap‎‏ استفاده کنیم چی میشه؟

incMaybe :: Num a => Maybe a -> Maybe a
incMaybe m = fmap (+1) m

showMaybe :: Show a => Maybe a -> Maybe String
showMaybe s = fmap show s

خوب انگار یه کم تَروتمیز شد. هنوز کار می‌کنه؟

Prelude> incMaybe (Just 1)
Just 2
Prelude> incMaybe Nothing
Nothing
Prelude> showMaybe (Just 9001)
Just "9001"
Prelude> showMaybe Nothing
Nothing

آره، ‏‎fmap‎‏ دلیلی نداره خودش رو درگیرِ ‏‎Nothing‎‏ کنه – مقداری نداره که بخواد باهاش کار کنه، پس انگار همه چیز درست کار می‌کنه.

با این حال میشه این رو انتزاعی‌تر کرد. یه راه‌ش اینه که اونها رو کاهشِ اِتا بدیم. یعنی بدون نام‌گذاریِ آرگومان‌ها بازنویسی‌شون کنیم:

incMaybe'' :: Num a => Maybe a -> Maybe a
incMaybe'' = fmap (+1)

showMaybe'' :: Show a => Maybe a -> Maybe String
showMaybe'' = fmap show

و اینکه لازم نیست حتماً مختصِ ‏‎Maybe‎‏ باشن! ‏‎fmap‎‏ با همه‌ی نوع‌داده‌هایی که نمونه ِ ‏‎Functor‎‏ دارن کار می‌کنه! با استعلام ِ تایپ در GHCi تایپِ جامع‌ترشون رو میشه دید:

Prelude> :t fmap (+1)
fmap (+1)
  :: (Functor f, Num b) => f b -> f b

Prelude> :t fmap show
fmap show
  :: (Functor f, Show a) => f a -> f String

حالا میشه خیلی جامع‌تر بازنویسی‌شون کنیم:

-- به این خاطر اسم‌شون رو
-- چون از روی ``lifted'' گذاشتیم
-- لیفت شدن f یه ساختار

liftedInc :: (Functor f, Num b)
          => f b -> f b
liftedInc = fmap (+1)

liftedShow :: (Functor f, Show a)
           => f a -> f String
liftedShow = fmap show

همون رفتار هم دارن:

Prelude> liftedInc (Just 1)
Just 2
Prelude> liftedInc Nothing
Nothing

Prelude> liftedShow (Just 1)
Just "1"
Prelude> liftedShow Nothing
Nothing

با افزایش پلی‌مورفیسم در تایپِ اون ساختار ِ فانکتوری، در مواردِ بیشتری میشه ازشون استفاده کرد:

Prelude> liftedInc [1..5]
[2,3,4,5,6] 

Prelude> liftedShow [1..5]
["1","2","3","4","5"]

تمرین: ممکنه

برای نوع‌داده‌ای که دقیقاً عینِ ‏‎Maybe‎‏ هست، یه نمونه ِ ‏‎Functor‎‏ بنویسین. از این نوع‌داده استفاده می‌کنیم چون ‏‎Maybe‎‏ یه نمونه ِ ‏‎Functor‎‏ داره و نمیشه دوباره براش نوشت.

data Possibly a =
    LolNope
  | Yeppers a
  deriving (Eq, Show)

instance Functor Possibly where
  fmap = undefined

در واقع دارین همین تابع رو می‌نویسین:

applyIfJust :: (a -> b)
            -> Maybe a
            -> Myabe b

‏‎Either‎‏

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

دیگه تا الان می‌دونین که یه نمونه ِ ‏‎Functor‎‏ برای ‏‎Either‎‏ در ‏‎base‎‏ هست تا برنامه‌نویس‌های قدرشناس ازش استفاده کنن. تو چندتا مثال ازش استفاده می‌کنیم. برای سادگی از همون الگویی که ‏‎Maybe‎‏ رو نشون دادیم استفاده می‌کنیم:

incIfRight :: Num a
           => Either e a
           -> Either e a
incIfRight (Right n) = Right $ n + 1
incIfRight (Left  e) = Left e

showIfRight :: Show a
            => Either e a
            -> Either e String
showIfRight (Right s) = Right $ show s
showIfRight (Left  e) = Left e

مثلِ قبل میشه با ‏‎fmap‎‏ ساده‌تر بنویسیم تا لازم نباشه حالتِ خطا رو مستقلاً بررسی کنیم:

incEither :: Num a
          => Either e a
          -> Either e a
incEither m = fmap (+1) m

showEither :: Show a
           => Either e a
           -> Either e String
showEither s = fmap show s

باز هم میشه کاهشِ اتا داد تا آرگومان‌های واضح حذف بشن:

incEither' :: Num a
           => Either e a
           -> Either e a
incEither' = fmap (+1)

showEither' :: Show a
            => Either e a
            -> Either e String
showEither' = fmap show

دوباره تابع‌هایی داریم که نباید لزوماً مختص ‏‎Either‎‏ باشن:

-- f ~ Either e 

liftedInc :: (Functor f, Num b)
          => f b -> f b
liftedInc = fmap (+1)

liftedShow :: (Functor f, Show a)
           => f a -> f String
liftedShow = fmap show

یه کم باهاش بازی کنین تا کامل بفهمین چی به چیه.

تمرین کوتاه

۱.

برای نوع‌داده‌ای که دقیقاً عینِ ‏‎Either‎‏ هست، یه نمونه ِ ‏‎Functor‎‏ بنویسین. از این نوع‌داده استفاده می‌کنیم چون ‏‎Either‎‏ یه نمونه ِ ‏‎Functor‎‏ داره و نمیشه دوباره براش نوشت.

data Sum a b =
    First a
  | Second b
  deriving (Eq, Show)

instance Functor (Sum a) where
  fmap = undefined

برای راهنمایی: دارین این تابع رو می‌نویسین.

applyIfSecond :: (a -> b)
              -> (Sum e) a
              -> (Sum e) b

۲.

چرا نمیشه یه نمونه ِ ‏‎Functor‎‏ باشه که تابع رو فقط به ‏‎First‎‏، یا ‏‎Left‎‏ در ‏‎Either‎‏ اعمال می‌کنه؟ یه کم قبل‌تر توضیح دادیم.