۱۶ - ۷فانکتورهای رایج
حالا که با Functor
و طرزِ کارش آشنا شدیم، میریم سراغِ مثالهای طولانیتر. این بخش بیشترش کُد و مثاله و کمتر توضیحات داره. تعامل با این مثالها به درک بهتر از مطالبی که گفتیم کمک میکنه (بدون وِراجی!).
با تابعِ const
شروع میکنیم:
Prelude> :t const
const :: a -> b -> a
Prelude> let replaceWithP = const 'p'
Prelude> replaceWithP 10000
'p'
Prelude> replaceWithP "woohoo"
'p'
Prelude> replaceWithP (Just 10)
'p'
حالا این تابع رو با fmap
روی نوعدادههای مختلفی که Functor
دارن امتحان میکنیم:
-- data Maybe a = Nothing | Just a
Prelude> fmap replaceWithP (Just 10)
Just 'p'
Prelude> fmap replaceWithP Nothing
Nothing
-- data [] a = [] | a : [a]
Prelude> fmap replaceWithP [1, 2, 3, 4, 5]
"ppppp"
Prelude> fmap replaceWithP "Ave"
"ppp"
Prelude> fmap (+1) []
[]
Prelude> fmap replaceWithP []
""
-- data (,) a b = (,) a b
Prelude> fmap replaceWithP (10, 20)
(10,'p')
Prelude> fmap replaceWithP (10, "woo")
(10,'p')
یه ذره جلوتر میگیم چرا مقدارِ اولِ توپل رو رد میکنه. به گونه ِ توپلها و گونه ِ f
در Functor
ربط داره.
حالا نمونه برای توابع:
Prelude> negate 10
-10
Prelude> let tossEmOne = fmap (+1) negate
Prelude> tossEmOne 10
-9
Prelude> tossEmOne (-10)
11
فانکتور ِ توابع رو تا به فصلِ ریدِر نرسیم کامل توضیح نمیدیم، ولی شاید یه کم براتون آشنا باشه:
Prelude> let tossEmOne' = (+1) . negate
Prelude> tossEmOne' 10
-9
Prelude> tossEmOne' (-10)
11
ببینیم دیگه چه کارهایی میشه کرد.
فانکتورها رو میشه تلنبار کرد
همونطور که قبلاً هم دیدیم، نوعدادهها رو میشه ترکیب کرد، معمولاً با تودرتو کردن. در مثالهای زیر از مَدک برای "تقریباً معادل است با" استفاده کردیم:
-- lms ~ List (Maybe (String))
Prelude> let n = Nothing
Prelude> let w = Just "woohoo"
Prelude> let ave = Just "Ave"
Prelude> let lms = [ave, n, w]
Prelude> let replaceWithP = const 'p'
Prelude> replaceWithP lms
'p'
Prelude> fmap replaceWithP lms
"ppp"
چیزی جدید نیست، فقط lms
بیشتر از یک تایپِ Functor
داره. Maybe
و لیست (که شاملِ String
هم میشه) هردوشون نمونههای Functor
دارن. آیا مجبوریم فقط روی بیرونیترین نوعداده fmap
کنیم؟ نخیر:
Prelude> (fmap . fmap) replaceWithP lms
[Just 'p',Nothing,Just 'p']
Prelude> let tripFmap = fmap . fmap . fmap
Prelude> tripFmap replaceWithP lms
[Just "ppp",Nothing,Just Just "pppppp"]
یه بار با جزئیات دوره کنیم:
-- lms ~ List (Maybe (String))
Prelude> let n = Nothing
Prelude> let w = Just "woohoo"
Prelude> let ave = Just "Ave"
Prelude> let lms = [ave, n, w]
Prelude> replaceWithP lms
'p'
Prelude> :t replaceWithP lms
replaceWitP lms :: Char
-- :در
replaceWithP lms
-- :میشه replaceWithP تایپِ ورودیِ
List (Maybe String)
-- ه Char تایپ خروجی هم
-- پس اعمالِ
replaceWithP
-- به
lms
-- چنین کاری رو انجام میده
List (Maybe String) -> Char
تایپِ خروجیِ replaceWithP
همیشه یک چیزه.
اگه این کار رو انجام بدیم:
Prelude> fmap replaceWithP lms
"ppp"
-- ساختار رو اطراف fmap
-- :جواب حفظ میکنه
Prelude> :t fmap replaceWithP lms
fmap replaceWithP lms :: [Char]
با اشعه X ببینیم:
-- :در
fmap replaceWithP lms
-- :میشه replaceWithP تایپِ ورودیِ
Maybe String
-- هست Char تایپ خروجی هم
-- پس اعمالِ
fmap replaceWithP
-- به
lms
-- چنین کاری رو انجام میده
List (Maybe String) -> List Char
List Char ~ String
اگه دوبار لیفت کنیم چطور؟
روی هم تلنبار کردن رو ادامه میدیم:
Prelude> (fmap . fmap) replaceWithP lms
[Just 'p',Nothing,Just 'p']
Prelude> :t (fmap . fmap) replaceWithP lms
(fmap . fmap) replaceWithP lms :: [Maybe Char]
و با اشعه X:
-- :در
(fmap . fmap) replaceWithP lms
-- :میشه replaceWithP تایپِ ورودیِ
-- [Char] یا List Char یا String
-- ه Char تایپ خروجی هم
-- پس اعمالِ
(fmap . fmap) replaceWithP
-- به
lms
-- چنین کاری رو انجام میده
List (Maybe String) -> List (Maybe Char)
چنین چیزی اصلاً چطور تایپچک میشه؟
شاید اولش واضح نباشه که چطور (fmap . fmap)
تایپچِک میشه. ازتون میخوایم که خودتون با بررسیِ تایپها دلیلش رو پیدا کنین. شاید مثلِ جولی ترجیح بدین روی کاغذ بنویسین، یا مثلِ کریس همه رو تو یه برنامهی ویرایش نوشته تایپ کنین. برای شروع تایپ سیگنچرها رو بهتون میدیم. از اونجا که دوتا fmap
ای که با هم ترکیب میشن ممکنه تایپهای متفاوتی داشته باشن، ما هم متغیرهای تایپ رو یکتا تعریف میکنیم. با جایگزین کردن تایپِ هر fmap
بجای آرگومان تابعیِ (.)
در تایپ سیگنچرِش شروع کنین:
(.) :: (b -> c) -> (a -> b) -> a -> c
-- fmap fmap
fmap :: Functor f => (m -> n) -> f m -> f n
fmap :: Functor g => (x -> y) -> g x -> g y
استعلام ِ تایپِ (fmap . fmap)
هم ممکنه کمک کنه؛ اونطوری میبینین تایپِ نهاییتون باید چی باشه.
بازم منو لیفت کن
اگه بخوایم، میتونیم از روی یه لایهی دیگه هم لیفت کنیم:
Prelude> let tripFmap = fmap . fmap . fmap
Prelude> tripFmap replaceWithP lms
[Just "ppp",Nothing,Just Just "pppppp"]
Prelude> :t tripFmap replaceWithP lms
(fmap . fmap . fmap) replaceWithP lms
:: [Maybe [Char]]
دید با اشعه X:
-- :در
(fmap . fmap . fmap) replaceWithP lms
-- :میشه replaceWithP تایپِ ورودیِ
-- Char
-- لیفت کردیم [Char] در [] چون از روی
-- هست Char تایپ خروجی هم
-- پس اعمالِ
(fmap . fmap . fmap) replaceWithP
-- به
lms
-- چنین کاری رو انجام میده
List (Maybe String) -> List (Maybe String)
پس متوجهِ یه الگویی میشیم.
تایپ واقعیِ چیزی که میره پایین
بالاتر الگو رو دیدیم، اما برای وضوح، قبل از ادامه اینجا خلاصه میکنیم:
Prelude> fmap replaceWithP lms
"ppp"
Prelude> (fmap . fmap) replaceWithP lms
[Just 'p',Nothing,Just 'p']
Prelude> let tripFmap = fmap . fmap . fmap
Prelude> tripFmap replaceWithP lms
[Just "ppp",Nothing,Just Just "pppppp"]
برای اطمینان از درکمون، تایپها رو هم خلاصه میکنیم:
-- عوض کردیم [Char] رو با String عمداً
replaceWithP' :: [Maybe [Char]] -> Char
replaceWithP' = replaceWithP
[Maybe [Char]] -> [Char]
[Maybe [Char]] -> [Maybe Char]
[Maybe [Char]] -> [Maybe [Char]]
یه کم تأمل کنین تا از درکِ چیزهایی که تا اینجا گفتیم مطمئن بشین. اگه متوجه نشدین، انقدر باهاشون بازی کنین تا حس راحتی پیدا کنین.
برو بالا بیا پایین
با یه مثالی که ساختار ِ بیشتری داره، همون ایده رو بیشتر بررسی میکنیم:
-- lmls ~ List (Maybe (List String))
Prelude> let ha = Just ["Ha", "Ha"]
Prelude> let lmls = [ha, Nothing, Just []]
Prelude> (fmap . fmap) replaceWithP lmls
[Just 'p',Nothing,Just 'p']
Prelude> let tripFmap = fmap . fmap . fmap
Prelude> tripFmap replaceWithP lmls
[Just "pp",Nothing,Just ""]
Prelude> (tripFmap.fmap) replaceWithP lmls
[Just ["pp","pp"],Nothing,Just []]
ببینید شما هم میتونین تغییرات تایپ رو مثل بالا دنبال کنین؟
باز هم با تابعِ P
یه بار دیگه به تغییر تایپها با لیفتکردن از روی لایههای متعددِ فانکتوری نگاه میندازیم. این بار از یه فایل منبع شروع میکنیم:
module ReplaceExperiment where
replaceWithP :: b -> Char
replaceWithP = const 'p'
lms :: [Maybe [Char]]
lms = [Just "Ave", Nothing, Just "woohoo"]
-- فقط برای اختصاصیتر کردنِ تایپ
replaceWithP' :: [Maybe [Char]] -> Char
replaceWithP' = replaceWithP
اگه لیفتِش کنیم چی میشه؟
-- Prelude> :t fmap replaceWithP
-- fmap replaceWithP :: Functor f
-- => f a -> f Char
liftedReplace :: Functor f => f a -> f Char
liftedReplace = fmap replaceWithP
اما میشه تایپِ liftedReplace
رو هم اختصاصیتر کنیم!
liftedReplace' :: [Maybe [Char]] -> [Char]
liftedReplace' = liftedReplace
اون []
اطرافِ Char
، همون f
در f Char
، یا همون ساختاریه که از روش لیفت کردیم. f
در f a
، میشه بیرونیترین []
در [Maybe [Char]]
. پس وقتی تایپ رو خاصتر میکنیم، چه از طریقِ اعمال ِ تابع به یه مقدار با تایپِ [Maybe [Char]]
، چه از طریق صریح نوشتنِ تایپ با liftedReplace'
، f
به []
تعیین میشه.
اگه دوبار لیفت کنیم چطور؟
-- Prelude> :t (fmap . fmap) replaceWithP
-- (fmap . fmap) replaceWithP
-- :: (Functor f1, Functor f)
-- => f (f1 a) -> f (f1 Char)
twiceLifted :: (Functor f1, Functor f) =>
f (f1 a) -> f (f1 Char)
twiceLifted = (fmap . fmap) replaceWithP
-- نسخهی خاصتر
twiceLifted' :: [Maybe [Char]]
-> [Maybe Char]
twiceLifted' = twiceLifted
-- f ~ []
-- f1 ~ Maybe
سهباره؟
-- Prelude> let rWP = replaceWithP
-- Prelude> :t (fmap . fmap . fmap) rWP
-- (fmap . fmap . fmap) rWP
-- :: (Functor f2, Functor f1, Functor f)
-- => f (f1 (f2 a)) -> f (f1 (f2 Char))
thriceLifted ::
(Functor f2, Functor f1, Functor f)
=> f (f1 (f2 a)) -> f (f1 (f2 Char))
thriceLifted =
(fmap . fmap . fmap) replaceWithP
-- نسخهی خاصتر یا معینتر
thriceLifted' :: [Maybe [Char]]
-> [Maybe [Char]]
thriceLifted' = thriceLifted
-- f ~ []
-- f1 ~ Maybe
-- f2 ~ []
حالا میشه جوابِ بیانیهها رو چاپ و مقایسه کنیم:
main :: IO ()
main = do
putStr "replaceWithP' lms: "
print (replaceWithP' lms)
putStr "liftedReplace lms: "
print (liftedReplace lms)
putStr "liftedReplace' lms: "
print (liftedReplace' lms)
putStr "twiceLifted lms: "
print (twiceLifted lms)
putStr "twiceLifted' lms: "
print (twiceLifted' lms)
putStr "thriceLifted lms: "
print (thriceLifted lms)
putStr "thriceLifted' lms: "
print (thriceLifted' lms)
همهی اینها رو تو یه فایل بنویسین، تو REPL بارگذاری کنین، و main
رو اجرا کنین تا خروجیها رو ببینین. بعد تایپها رو تغییر بدین و کلاً یه کم با کُد بازی کنین و حدس بزنین چه چیزی باید کار کنه و چه چیزی نباید. ایجاد فرضیهها، درست کردن یا تغییر دادن آزمایشها بر اساس اونها، و بررسیِ درست بودنِ اون فرضیهها از کارهای خیلی مهم برای پیدا کردنِ درک خوب از انتزاعهایی مثلِ Functor
ِه!
تمرینها: وزنه برداری
با استفاده از fmap
، پرانتز، و ترکیب توابع کاری کنین بیانیههای زیر تایپچک بشن و جوابِ مورد نظر رو بِدن.
۱.
a = (+1) $ read "[1]" :: [Int]
-- جواب مورد نظر
Prelude> a
[2]
۲.
b = (++ "lol") (Just ["Hi,", "Hello"])
Prelude> b
Just ["Hi,lol","Hellolol"]
۳.
c = (*2) (\x -> x - 2)
Prelude> c 1
-2
۴.
d =
((return '1' ++) . show)
(\x -> [x, 1..3])
Prelude> d 0
"1[0,1,2,3]"
۵.
e :: IO Integer
e = let ioi = readIO "1" :: IO Integer
changed = read ("123"++) show ioi
in (*3) changed
Prelude> e
3693