۱۷ - ۵اپلیکتیو در عمل
حتماً تا اینجا متوجه شدین که خیلی از نوعدادههایی که در دو فصلِ قبلی باهاشون کار کردیم، نمونه ِ 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]