۷ - ۸ترکیب توابع

با ترکیب توابع، که یک نوع تابع سطح بالا هست، میشه توابع رو طوری با هم ترکیب کرد که نتیجه‌ی اعمال ِ یک تابع، به عنوان آرگومان به تابع بعدی داده بشه. سبک نوشتاریِ خیلی خلاصه‌ای رو در اختیار میذاره که با سبک تابعی و مختصر هسکل همخونی داره. اولش ممکنه به نظر سخت و پیچیده بیاد، ولی وقتی کار باهاش دست‌تون بیاد، دوست‌داشتنی میشه! با تایپ سیگنچر و معنی‌ش شروع می‌کنیم:

(.) :: (b -> c) -> (a -> b) -> a -> c
--        [1]         [2]     [3]  [4]

۱.

این یه تابع از ‏‎b‎‏ به ‏‎c‎‏ هست، که به عنوان آرگومان داده میشه (به همین خاطر پرانتز داره).

۲.

یه تابع از ‏‎a‎‏ به ‏‎b‎‏.

۳.

یه مقدار با تایپِ ‏‎a‎‏، که همون تایپِ ورودی در ‏‎[2]‎‏ هست.

۴.

یه مقدار با تایپِ ‏‎c‎‏، که همون تایپِ خروجی در ‏‎[1]‎‏ هست.

اگه یه پرانتز اضافه کنیم:

(.) :: (b -> c) -> (a -> b) -> (a -> c)
--        [1]         [2]         [3]

به زبان خودمون:

۱.

یه تابع از ‏‎b‎‏ به ‏‎c‎‏

۲.

و یه تابع از ‏‎a‎‏ به ‏‎b‎‏

۳.

یه تابع از ‏‎a‎‏ به ‏‎c‎‏ میده.

نتیجه‌ی ‏‎(a -> b)‎‏ آرگومانِ ‏‎(b -> c)‎‏ میشه و اینطوری از یه آرگومانِ ‏‎a‎‏ به یه جواب ‏‎c‎‏ می‌رسیم. انگار دوتا تابع رو به هم جوش دادیم طوری که جواب یکی آرگومان اون یکی بشه.

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

(f . g) x = f (g x)

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

این پروسه رو قدم به قدم پیش بریم. عملگر ِ ترکیب یا ‏‎(.)‎‏ رو میشه به چشم یه جور لوله‌کشی که داده رو از چند تابع رد می‌کنه نگاه کرد. تابعِ ترکیبی ِ زیر اول مقادیر یه لیست رو با هم جمع می‌کنه و بعد نتیجه‌ی اون رو منفی می‌کنه:

Prelude> negate . sum $ [1, 2, 3, 4, 5]
-15

-- که به این ترتیب محاسبه میشه
negate . sum $ [1, 2, 3, 4, 5]

-- نکته: این کُد هم کار می‌کنه
negate (sum [1, 2, 3, 4, 5])
negate (15)
-15

این کار رو مستقیماً در REPL انجام دادیم، چون عملگرِ ترکیب از ‏‎Prelude‎‏ در گستره هست. جمعِ لیست ۱۵ میشه؛ این جواب به تابعِ ‏‎‎‏negate داده میشه و (۱۵-) برمی‌گردونه.

شاید کاربردِ عملگر ِ ‏‎$‎‏ براتون سؤال شده باشه. خیلی وقت پیش، از تقدم ِ عملگرهای مختلف صحبت کردیم. اگه یادتون باشه گفتیم که این اوپراتور تقدم ِ کمتری از اعمال توابع (که معمولاً با فاصله‌ی سفید نشون داده میشه) داره (م. در واقع کمترین تقدم رو داره). اعمالِ تابع ِ معمولی، تقدم ِ ۱۰ داره (از ۱۰). عملگرِ ترکیب هم تقدم ِ ۹. اگه علامتِ دلار رو نمی‌ذاشتیم، اینطور محاسبه میشد:

negate . sum [1, 2, 3, 4, 5]
negate . 15

به خاطر تقدم ِ بیشتر برای اعمالِ توابع، قبل از ترکیب ِ دو تابع، اول تابعِ ‏‎sum‎‏ اعمال شد. در این حالت، آرگومانِ دومِ عملگرِ ترکیب (که باید تابع باشه) یه مقدارِ عددی شده. با استفاده از ‏‎$‎‏ مشخص کردیم که ترکیب ِ توابع بعد از اعمالِ تابع پیاده بشه.

بجای عملگر ِ ‏‎$‎‏، از پرانتز هم میشد استفاده کنیم:

Prelude> (negate . sum) [1, 2, 3, 4, 5]
-15

انتخاب بین پرانتز و علامتِ دلار مهم نیست؛ فقط روی سبک نوشتاری و خوانایی تأثیر میذاره.

در مثال بعدی از دو تابعِ ‏‎take‎‏ و ‏‎reverse‎‏، و لیست اعدادِ ۱ تا ۱۰ به عنوان آرگومان استفاده می‌کنیم. چیزی که انتظار داریم اینه که اول لیست‌مون معکوس بشه (از ۱۰ تا ۱) و بعد ۵ المانِ اول لیست جدید رو به عنوان جواب پس بگیریم:

Prelude> take 5 . reverse $ [1..10]
[10,9,8,7,6]

در کُدِ زیر، چطور می‌تونیم از ترکیب توابع بجای پرانتز استفاده کنیم؟

Prelude> take 5 (enumFrom 3)
[3,4,5,6,7]

می‌دونیم باید پرانتز رو حذف کنیم، عملگرِ ترکیب رو اضافه کنیم، و عملگر ِ ‏‎$‎‏ رو هم اضافه کنیم. این شِکلی میشه:

Prelude> take 5 . enumFrom $ 3
[3,4,5,6,7]

اینطور هم میشه نوشت (که بیشتر در فایل‌‌های منبع نوشته میشه):

Prelude> let f x = take 5 . enumFrom $ x
Prelude> f 3
[3,4,5,6,7]

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

تابعِ ‏‎filter odd‎‏ برامون جدیده، اما فقط اعدادِ فرد رو از لیستی که ‏‎enumFrom‎‏ می‌سازه فیلتر می‌کنه (اگه بخواین می‌تونین از ‏‎filter even‎‏ برای اعداد زوج استفاده کنین). در نهایت، ‏‎take‎‏ تعداد المان‌هایی که در آرگومان‌ش تعیین کردیم رو برمی‌گردونه. اگه دوست داشتین آرگومان‌ها رو تغییر بدین و نتیجه‌شون رو ببینین.

Prelude> take 5 . filter odd . enumFrom $ 3
[3,5,7,9,11]

همین که توابع بیشتری رو ترکیب کنین، متوجه ناخوشایند بودنِ پرانتزهای زیاد میشین. این عملگر کمک می‌کنه از چنین چیزی پرهیز کنیم. از مزایای دیگه اینکه می‌تونیم کُدمون رو با سبکِ باز هم خلاصه‌تری به اسمِ بی‌نقطه بنویسیم.