۱۶ - ۷فانکتورهای رایج

حالا که با ‏‎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