۷ - ۶توابع سطح بالا

توابع سطح بالا (یا HOF ها) توابعی‌‌ هستن که توابع رو به عنوان آرگومان قبول می‌کنن. توابع مقادیراند – چرا نشه مثل بقیه‌ی مقادیر ازشون استفاده کرد؟ این یکی از بخش‌های مهمِ برنامه‌نویسی تابعی‌ه و راه مؤثری برای ترکیب توابع در اختیار میذاره.

تابع استاندارد و سطح بالا ِ ‏‎flip‎‏ رو در نظر بگیریم:

Prelude> :t flip
flip :: (a -> b -> c) -> b -> a -> c

-- (a -> b -> c) استفاده از (-) به عنوانِ
Prelude> (-) 10 1
9
Prelude> let fSub = flip (-)
Prelude> fSub 10 1
-9
Prelude> fSub 5 10
5

پارامتر اولِ ‏‎flip‎‏ یه تابع‌ مثل ‏‎(-)‎‏ ِه که خودش دو پارامتر داره. تابع ‏‎flip‎‏ ترتیب آرگومان‌ها رو برعکس می‌کنه.

خودمون هم می‌تونیم تابعِ ‏‎flip‎‏ رو، با استفاده از متغیرِ ‏‎f‎‏ برای نشون دادن تابعِ ‏‎(a -> b -> c)‎‏، بنویسیم:

flip :: (a -> b -> c) -> b -> a -> c 
flip f x y = f y x

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

myFlip :: (a -> b -> c) -> b -> a -> c 
myFlip f = \ x y -> f y x

هیچ فرقی بین کاری که ‏‎flip‎‏ انجام میده با ‏‎myFlip‎‏ نیست: یکی‌شون پارامترها رو در تعریف تابع معرفی کرده، اون یکی پارامترها رو در تابع بی‌نام ِ خروجی معرفی کرده. اما چه چیزی ‏‎flip‎‏ رو یه تابع سطح بالا می‌کنه؟ این:

flip :: (a -> b -> c) -> b -> a -> c
--     [      1      ]
flip f x y = f y x
--  [2]     [3]

۱.

وقتی می‌خوایم یه آرگومانِ تابعی رو تویِ تایپِ یه تابع نشون بدیم، باید از پرانتز استفاده کنیم.

۲.

آرگومانِ ‏‎f‎‏، میشه همون تابعِ ‏‎a -> b -> c‎‏.

۳.

بطور معمول، ‏‎f‎‏ باید اول به ‏‎x‎‏ و بعد به ‏‎y‎‏ اعمال بشه، اما ‏‎flip‎‏ این ترتیب رو عوض می‌کنه و ‏‎f‎‏ اول به ‏‎y‎‏ و بعد به ‏‎x‎‏ اعمال میشه.

برای درک بهتر طرز کارِ HOF ها از لحاظ گرامری، خوبه شرکت‌پذیری در تایپ سیگنچرها رو به خاطر بیاریم.

تایپِ تابع زیر رو ببینید:

returnLast :: a -> b -> c -> d -> d
returnLast _ _ _ d = d

اگه بخوایم ‏‎returnLast‎‏ رو صراحتاً پرانتزگذاری کنیم، باید با شرکت‌پذیری ِ ‏‎->‎‏ همخونی داشته باشه، یعنی شرکت‌پذیری از راست رو رعایت کنه. پرانتزگذاری مثل زیر موردی نداره. دقت کنید که این در واقع کاری کردن ِ پیش‌فرض رو صراحتاً نشون میده:

returnLast' :: a -> (b -> (c -> (d -> d)))
returnLast' _ _ _ d = d

اما این یکی خلافِ شرکت‌پذیری ِ ‏‎->‎‏ هست و کار نمی‌کنه:

returnBroke :: (((a -> b) -> c) -> d) -> d
returnBroke _ _ _ d = d

تابعِ ‏‎returnBroke‎‏ رو که بارگذاری کنین، خطای تایپ می‌گیرین:

Couldn't match expected type
                 ‘t0 -> t1 -> t2 -> t2’
            with actual type ‘d’

‘d’ is a rigid type variable bound by
  the type signature for
returnBroke :: (((a -> b) -> c) -> d) -> d

Relevant bindings include
returnBroke :: (((a -> b) -> c) -> d) -> d

The equation(s) for ‘returnBroke’
  have four arguments,
but its type ‘(((a -> b) -> c) -> d) -> d’
  has only one

این پیغام میگه تایپِ ‏‎returnBroke‎‏ فقط یک آرگومان، اون هم با تایپِ ‏‎((a -> b) -> c) -> d‎‏ رو مشخص کرده، ولی تعریف تابع‌مون ظاهراً انتظار چهار آرگومان داره. تایپ سیگنچر ِ ‏‎returnBroke‎‏ فقط یک تابع رو به عنوان تنها آرگومانِ ‏‎returnBroke‎‏ تعیین کرده.*

*

جهت اطلاع، ‏‎returnBroke‎‏ یه تابع غیرممکنه.

البته برای تابعی که کاری غیر از کار تابع ‏‎returnLast‎‏ انجام میده میشه تایپ‌ش رو با این ترتیب پرانتزگذاری کرد:

returnAfterApply :: (a -> b) -> a -> c -> b
returnAfterApply f a c = f a

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

HOF ها کمک به اداره‌ی بهتر در اعمال ِ توابع به آرگومان‌ها می‌کنن. برای درک یکی دیگه از فواید اونها، یه نگاه دیگه به تابع ‏‎compare‎‏ از تایپکلاس ‏‎Ord‎‏ میندازیم:

Prelude> :t compare
compare :: Ord a => a -> a -> Ordering
Prelude> :info Ordering
data Ordering = LT | EQ | GT
Prelude> compare 10 9
GT
Prelude> compare 9 9
EQ
Prelude> compare 9 10
LT

حالا باهاش یه تابع دیگه می‌نویسیم:

data Employee = Coder   -- کدنویس
              | Manager --مدیر 
              | Veep    -- معاون رئیس
              | CEO     -- مدیرعامل
              deriving (Eq, Ord, Show)

reportBoss :: Employee -> Employee -> IO ()
reportBoss e e' =
  putStrLn $ show e ++
             " is the boss of " ++
             show e'

employeeRank :: Employee
             -> Employee
             -> IO ()
employeeRank e e' =
  case compare e e' of
    GT -> reportBoss e e'
--  [         1         ]
    EQ -> putStrLn "Neither employee\
                   \ is the boss"
--  [                    2          ]
    LT -> (flip reportBoss) e e'
--  [             3            ]

اون ‏‎case‎‏ در تابعِ ‏‎employeeRank‎‏ یه بیانیه‌ی case ِه. این تابع با مقایسه ِ ‏‎e‎‏ و ‏‎e'‎‏ سه حالت رو در نظر می‌گیره:

۱.

در حالتی که ‏‎e‎‏ بزرگتر از ‏‎e'‎‏ باشه، ‏‎reportBoss e e'‎‏ رو برمی‌گردونه.

۲.

وقتی برابر باشن، جمله‌ی ‏‎"هیچ کدوم از کارمندها رئیس نیستن."‎‏ رو برمی‌گردونه.

۳.

اگه ‏‎e‎‏ کوچکتر از ‏‎e'‎‏ باشه، تابعِ ‏‎reportBoss‎‏ رو برعکس می‌کنه. اینطور هم میشد نوشت ‏‎reportBoss e' e‎‏.

تابعِ ‏‎compare‎‏ از نمونه ِ ‏‎Ord‎‏ ِ تایپ برای مقایسه ِ مقادیرِ اون استفاده می‌کنه. در این مثال، تعریف داده ِ ما ‏‎کدنویس‎‏ رو در کمترین مقام، و ‏‎مدیرعامل‎‏ رو در بالاترین مقام معرفی کرده، پس ‏‎compare‎‏ هم از همین ترتیب‌بندی برای محاسبه‌ی جواب‌هاش استفاده می‌کنه.

اگه بارگذاری و امتحان‌ش کنیم:

Prelude> employeeRank Veep CEO
CEO is the boss of Veep
-- م. مدیرعامل رئیسِ معاون‌ِشه

احتمالاً تو اکثر شرکت‌ها همینطوره! از اونجا که برنامه‌نویس‌های ماهری هستیم، یه کم این تابع رو انعطاف‌پذیرتر می‌نویسیم – دقت کنین چطور تایپِ ‏‎employeeRank‎‏ رو تغییر میدیم:

data Employee = Coder
              | Manager
              | Veep 
              | CEO 
              deriving (Eq, Ord, Show)

reportBoss :: Employee -> Employee -> IO ()
reportBoss e e' =
  putStrLn $ show e ++
             " is the boss of " ++
             show e'

employeeRank :: ( Employee
               -> Employee
               -> Ordering )
             -> Employee
             -> Employee
             -> IO ()
employeeRank f e e' =
  case f e e' of
    GT -> reportBoss e e'
    EQ -> putStrLn "Neither employee\
                   \ is the boss"
    LT -> (flip reportBoss) e e'

حالا تابعِ ‏‎employeeRank‎‏ یه تابع با تایپِ ‏‎Employee -> Employee -> Ordering‎‏ رو به عنوان آرگومان (که اسم پارامترش رو ‏‎f‎‏ گذاشتیم) می‌گیره و ازش بجای تابعِ ‏‎‎‏compare استفاده می‌کنه. می‌بینید که از همون بیانیه‌ی case استفاده کردیم. اگه تابعِ ‏‎compare‎‏ رو بهش بدیم، همون رفتار قبلی رو از تابع می‌بینیم:

Prelude> employeeRank compare Veep CEO
CEO is the boss of Veep
Prelude> employeeRank compare CEO Veep
CEO is the boss of Veep

ولی ما هَکِرهای زِرَنگی هستیم، پس با یه تابعِ مقایسه ِ دیگه، ترتیب مقام‌ها رو به‌هم میزنیم:

data Employee = Coder
              | Manager
              | Veep 
              | CEO 
              deriving (Eq, Ord, Show)

reportBoss :: Employee -> Employee -> IO ()
reportBoss e e' = putStrLn $ show e ++
                    " is the boss of " ++ show e'

codersRuleCEOsDrool :: Employee
                    -> Employee
                    -> Ordering
codersRuleCEOsDrool Coder Coder = EQ
codersRuleCEOsDrool Coder _ = GT
codersRuleCEOsDrool _ Coder = LT
codersRuleCEOsDrool e e' =
  compare e e'

employeeRank :: ( Employee
               -> Employee
               -> Ordering )
             -> Employee
             -> Employee
             -> IO ()
employeeRank f e e' =
  case f e e' of
    GT -> reportBoss e e'
    EQ -> putStrLn "Neither employee\
                   \ is the boss"
    LT -> (flip reportBoss) e e'

یه تابعِ جدید درست کردیم که رفتارِ تابعِ معمولیِ ‏‎compare‎‏ رو به کمکِ تطبیق الگو روی داده‌ساز ِ ‏‎Coder‎‏ تغییر میده. در حالتی که ‏‎Coder‎‏ اولین مقدار باشه (و مقدار دوم هرچیزی باشه – به اون خط تیره دقت کنین)، جواب ‏‎GT‎‏ یا بزرگتر از میشه. اگرهم ‏‎Coder‎‏ مقدارِ دوم باشه، تابع ‏‎LT‎‏ یا کوچکتر از جواب میده. هر حالت دیگه‌ای که ‏‎Coder‎‏ هیچ کدوم از آرگومان‌ها نیست، رفتارِ عادیِ ‏‎compare‎‏ پیاده میشه. بیانیه‌ی case هم در ‏‎employeeRank‎‏ تغییری نکرده.

این هم در عمل:

Prelude> employeeRank compare Coder CEO
CEO is the boss of Coder
Prelude> let cs = codersRuleCEOsDrool
Prelude> employeeRank cs Coder CEO
Coder is the boss of CEO
Prelude> employeeRank cs CEO Coder
Coder is the boss of CEO

اگه از ‏‎compare‎‏ برای آرگومانِ ‏‎f‎‏ استفاده کنیم، رفتار متفاوتی نمی‌بینیم. اما اگه از تابع جدیدمون، ‏‎codersRuleCEOsDrool‎‏ به عنوانِ آرگومان برای ‏‎f‎‏ استفاده کنیم، رفتارِ دیگه‌ای می‌گیریم و تو اداره آشوب به پا می‌کنیم.

اینجا تونستیم به رفتارِ تابعِ ‏‎compare‎‏ اتکا کنیم، و فقط بخشی که می‌خواستیم رو تغییر بدیم. این ارزشِ HOFهاست، شروع یه راه قوی برای استفاده‌ی دوباره از کُدها و ترکیب ِ اونها.

تمرین‌ها: طفره‌ی هنرمندانه

با توجه به تعاریف زیر، نتیجه‌ی اعمال ِ توابع رو پیدا کنین. حداقل چندتا از جواب‌ها رو که پیدا کردین، تعاریف رو تو یه فایل بنویسین و تو GHCi بارگذاری‌‌ش کنین تا جواب‌هاتون رو تست کنین:

-- تایپ‌‌ها رو ننوشتیم
-- خودتون سعی کنین بنویسین

dodgy x y = x + y * 10
oneIsOne = dodgy 1
oneIsTwo = (flip dodgy) 2

۱.

برای مثال، فکر می‌کنین جوابِ بیانیه‌ی ‏‎dodgy 1 0‎‏ چی میشه؟ اگه تعاریف رو تو یه فایل بذارین و در GHCi بارگذاری کنین، جواب رو اینطوری می‌تونین پیدا کنین:

Prelude> dodgy 1 0
1

سعی کنین جواب بیانیه‌های زیر رو اول تو ذهن‌تون پیدا کنین، و با REPL از صحت جواب‌تون مطمئن شین.

۲.

‏‎dodgy 1 1‎‏

۳.

‏‎dodgy 2 2‎‏

۴.

‏‎dodgy 1 2‎‏

۵.

‏‎dodgy 2 1‎‏

۶.

‏‎oneIsOne 1‎‏

۷.

‏‎oneIsOne 2‎‏

۸.

‏‎oneIsTwo 1‎‏

۹.

‏‎oneIsTwo 2‎‏

۱۰.

‏‎oneIsOne 3‎‏

۱۱.

‏‎oneIsTwo 3‎‏