۷ - ۶توابع سطح بالا
توابع سطح بالا (یا 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