۷ - ۲آرگومان‌ها و پارامترها

اگه از حرف‌های قبلی یادتون باشه، به خاطرِ کاری کردن، توابعِ هسکل ممکنه در ظاهر چند پارامتر داشته باشن؛ ولی در واقع همه‌ی توابع فقط یه آرگومان می‌گیرن و یه خروجی میدن. در هسکل توابع رو از راه‌های گرامری ِ متنوعی می‌سازیم، و مبنای همه‌ی این راه‌های گرامری یه بیانیه‌ایه که آرگومان می‌گیره. توابع کلاً بر این پایه تعریف میشن که قابلیتِ اعمال شدن به یه آرگومان و برگردوندنِ یه جواب رو دارن.

هر مقداری در هسکل می‌تونه آرگومانِ توابع باشه. به مقداری که بشه ازش به عنوانِ آرگومان تابع استفاده کرد، مقدارِ ممتاز میگیم؛ که در هسکل به توابع هم اطلاق میشه، یعنی یه تابع می‌تونه آرگومانِ یه تابعِ دیگه بشه. هر زبان برنامه‌نویسی‌ای اجازه‌ی چنین چیزی رو نمیده، ولی امیدواریم توضیحاتی که از تایپِ تابع و کاری کردن دادیم، دلیل و طرز کار این قابلیت رو تا حدی روشن کرده باشه.

تعیین پارامترها

در هسکل، اسم پارامترها بین اسم تابع (که همیشه سمت چپ قرار داره) و علامت مساوی تعریف میشن (اسم پارامتر، هم از اسمِ تابع و هم از علامت مساوی با فاصله‌ی سفید جدا میشه). این اسمْ یه متغیره، و هر وقت تابع رو به یه آرگومان اعمال می‌کنیم مقدار آرگومان به اون پارامتر مقیّد میشه.

اول یه مقدار بدون پارامتر تعریف می‌کنیم:

myNum :: Integer
myNum = 1

myVal = myNum

اگه تایپِ ‏‎myVal‎‏ رو استعلام کنیم:

Prelude> :t myVal
myVal :: Integer

مقدارِ ‏‎myVal‎‏ تایپ یکسانی با ‏‎myNum‎‏ داره چون با هم برابراند. از روی تایپ‌ش مشخصه که یه مقدارِ بدون پارامتره، پس نمیشه به چیزی اعمال ِش کرد.

حالا یه پارامترِ ‏‎f‎‏ معرفی می‌کنیم:

myNum :: Integer
myNum = 1

myVal f = myNum

ببینیم چه تأثیری رو تایپ‌ش گذاشت:

Prelude> :t myVal
myVal :: t -> Integer

با نوشتنِ ‏‎f‎‏ بعد از ‏‎myVal‎‏، ‏‎myVal‎‏ رو پارامتردار کردیم، و تایپ‌ش رو از ‏‎Integer‎‏ به ‏‎t -> Integer‎‏ تغییر داد. تایپِ ‏‎t‎‏ پلی‌مورفیک ِه چون هیچ کاری باهاش انجام نمیشه – می‌تونه هر چیزی باشه. چون هیچ کاری با ‏‎f‎‏ انجام ندادیم، در نتیجه بیشترین پلی‌مورفیسم براش استنتاج شد. اما اگه یه کاری باهاش انجام بدیم، تایپ هم تغییر می‌کنه:

Prelude> let myNum = 1 :: Integer
Prelude> let myVal f = f + myNum
Prelude> :t myVal
myVal :: Integer -> Integer

دیگه میدونه که تایپِ ‏‎f‎‏ باید ‏‎Integer‎‏ باشه، چون با ‏‎myNum‎‏ جمع‌ش کردیم.

یکی از راه‌هایی که میشه توابع رو از مقادیر تشخیص داد، همین مورده که مقادیر به هیچ آرگومانی اعمال نمیشن، ولی توابع الزاماً پارامترهایی دارن که ممکنه به آرگومان‌ها اعمال بشن.

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

myNum :: Num a => a
myNum = 1
-- [1]

myVal :: Num a => a -> a
myVal f = f + myNum
--       [2]

stillAFunction :: [a] -> [a] -> [a] -> [a]
stillAFunction a b c = a ++ b ++ c
--             [ 3 ]

۱.

تعریفِ یک مقدار با تایپِ ‏‎Num a => a‎‏. میشه گفت که تابع نیست، چون هیچ پارامتری بین اسمِ مقدار و ‏‎=‎‏ نامگذاری نشده. پس هیچ آرگومانی نمی‌گیره، و مقدارِ ۱ هم تابع نیست.

۲.

اینجا ‏‎f‎‏ اسمِ یه پارامتر از تابعِ ‏‎myVal‎‏ ِه. این پارامتر امکانِ اعمال شدن (یا مقیّد شدن) به یه مقدارِ ورودی رو نشون میده. تایپِ این تابع ‏‎Num a => a -> a‎‏ هست. اگه مثل بالاتر ‏‎myNum‎‏ رو با تایپ ‏‎Integer‎‏ تعریف می‌کردیم، تایپ ‏‎myVal‎‏ هم ‏‎Integer -> Integer‎‏ میشد.

۳.

اینجا ‏‎a‎‏، ‏‎b‎‏، و ‏‎c‎‏ پارامترهای تابع رو نشون میدن. منطق زیرینِ اینطوری "چند پارامتر داشتن،" تودرتو بودنِ چند تابعِ تک پارامتری‌ه، ولی در سطح جمله‌ای اینطور دیده میشه.

ببینین با اضافه کردنِ پارامترها، تایپ‌ها چطور تغییر می‌کنن:

Prelude> let myVal f g = myNum
Prelude> :t myVal
myVal :: t -> t1 -> Integer

Prelude> let myVal f g h = myNum
Prelude> :t myVal
myVal :: t -> t1 -> t2 -> Integer

تایپ‌های ‏‎t‎‏، ‏‎t1‎‏، و ‏‎t2‎‏ ممکنه تایپ‌های مختلفی باشن. می‌تونن فرق کنن، ولی الزام نیست. از اونجا که برای استنتاجِ تایپ راهی نذاشتیم که تایپ‌شون رو پیدا کنه، همه‌شون پلی‌مورفیک شدن، و با همدیگه فرق دارن چون چیزی مانعِ تفاوت‌شون نشده. استنتاج تایپ، پلی‌مورفیک‌ترین تایپی که کار می‌کنه رو نتیجه می‌گیره.

انقیاد ِ متغیرها به مقادیر

ببینیم انقیاد ِ متغیرها چطور کار می‌کنه. اعمال ِ تابع، پارامترهاش رو به مقادیر مقیّد می‌کنه. پارامترهای تایپی به تایپ‌ها، و پارامترهای تابعی به مقادیر مقیّد میشن. انقیاد ِ متغیرها فقط در اعمالِ توابع رخ نمیده، بلکه بیانیه‌های ‏‎let‎‏ و عباراتِ ‏‎where‎‏ هم برای انقیاد ِ متغیرها به کار میرن. تابع زیر رو در نظر بگیرین:

addOne :: Integer -> Integer
addOne x = x + 1

تا وقتی ‏‎addOne‎‏ به یه مقدارِ ‏‎Integer‎‏ اعمال نشه، نتیجه رو نمی‌دونیم. بعد از اعمال ِ ‏‎addOne‎‏ به یه مقدار، میگیم ‏‎x‎‏ به مقدارِ آرگومان مقیّد شده. تا زمانی که همه‌ی آرگومان‌های تابع اعمال (و متعاقباً پارامترهاش مقیّد به مقادیر) نشن، از نتیجه‌ی تابع نمیشه استفاده کرد.

addOne 1 -- به ۱ مقیّد شده x
addOne 1 = 1 + 1
         =     2

addOne 10 -- به ۱۰ مقیّد شده x
addOne 10 = 10 + 1
          =     11

علاوه بر انقیاد ِ متغیرها از طریق اعمال توابع، با استفاده از بیانیه‌ی ‏‎let‎‏ هم میشه متغیرها رو تعریف و مقیّد کرد:

bindExp :: Integer -> String
bindExp x = let y = 5 in
              "the integer was: " ++ show x
              ++ " and y was: " ++ show y

در بیانیه‌ی ‏‎show y‎‏، ‏‎y‎‏ در گستره هست چون ‏‎let‎‏ متغیرِ ‏‎y‎‏ رو به ۵ مقیّد کرد. ‏‎y‎‏ فقط داخل بیانیه‌ی ‏‎let‎‏ در گستره هست. حالا یه مثال که کار نمی‌کنه:

bindExp :: Integer -> String
bindExp x = let z = y + x in
            let y = 5 in "the integer was: "
            ++ show x ++ " and y was: "
            ++ show y ++ " and z was: " ++ show z

به خاطرِ در گستره نبودنِ ‏‎y‎‏ خطا می‌گیرین (‏‎Not in scope: ‘y’‎‏). اینجا سعی کردیم ‏‎z‎‏ رو معادل مقداری بذاریم که از ‏‎x‎‏ و ‏‎y‎‏ ساخته میشه. ‏‎x‎‏ در گستره هست چون پارامترِ تابع در همه جای تابع دیده میشه. اما ‏‎y‎‏ داخل بیانیه‌ای که ‏‎let z = …‎‏ در بَر داره مقیّد شده، پس هنوز تو گستره نیست – یعنی در دیدرسِ تابع اصلی قرار نداره.

در بعضی موارد، اگه آرگومان‌های تابع تیره شده باشن، دیگه در تابع دیده نمیشن. یک مثال از تیرِگی:

bindExp :: Integer -> String
bindExp x = let x = 10; y = 5 in
              "the integer was: " ++ show x
              ++ " and y was: " ++ show y

اگه این تابع رو به یه آرگومان اعمال کنین، می‌بینین که جواب‌ش هیچ وقت تغییر نمی‌کنه:

Prelude> bindExp 9001
"the integer was: 10 and y was: 5"

در این مثال، اشاره‌گر به ‏‎x‎‏ که از آرگومانِ ‏‎x‎‏ اومده بود، توسط ‏‎x‎‏ای که از انقیاد ِ ‏‎let‎‏ اومده بود تیره شد. به خاطر گستره‌ی واژگانی در هسکل، داخلی‌ترین تعریفِ ‏‎x‎‏ (که اسم تابع در چپ، بیرون به حساب میاد) بیشترین اولویت رو داره. منظور از گستره‌ی واژگانی اینه که تعیینِ مقدارِ یه عبارتِ اسم‌دار، به مکانِ اون در کُد بستگی داره، مثلاً داخل عبارات ‏‎let‎‏ و ‏‎where‎‏. به همین خاطر تشخیصِ اینکه چه مقداری از چه جایی اومده راحت‌تر میشه. مثال قبلی رو با شماره‌گذاری بیشتر توضیح میدیم:

bindExp :: Integer -> String
bindExp x = let x = 10
--     [1]     [2]
                y = 5
              in "x: " ++ show x
--                            [3]
              ++ " y: " ++ show y

۱.

پارامترِ ‏‎x‎‏ که در تعریفِ ‏‎bindExp‎‏ معرفی شده. این توسط ‏‎x‎‏ در ‏‎[2]‎‏ تیره میشه.

۲.

این یه انقیاد از ‏‎x‎‏ با ‏‎let‎‏ ِه که روی تعریفِ ‏‎x‎‏ به عنوان آرگومان (اشاره شده با ‏‎[1]‎‏) سایه میندازه.

۳.

استفاده از ‏‎x‎‏ که در ‏‎[2]‎‏ مقیّد شده. با توجه به گستره ِ ایستا (واژگانی) در هسکل، این همیشه به ‏‎x‎‏ ای که با ‏‎x = 10‎‏ در ‏‎let‎‏ تعریف شده اشاره می‌کنه.

در GHCi هم می‌تونین تأثیرِ سایه‌اندازی روی اسم‌های در گستره رو با استفاده از دستور ِ let (که تا الان همینطور می‌نوشتین!) ببینین:

Prelude> let x = 5
Prelude> let y = x + 5
Prelude> y
10
Prelude> y * 10
100
Prelude> let z y = y * 10
Prelude> x
5
Prelude> y
10
Prelude> z 9
90

-- اما
Prelude> z y
100

با اینکه ‏‎y‎‏ در گستره ِ GHCi با ‏‎x + 5‎‏ تعریف شده بود، معرفیِ ‏‎z y = y * 10‎‏ یه گستره ِ داخلی درست کرد که اسم ‏‎y‎‏ رو تیره می‌کنه. حالا وقتی ‏‎z‎‏ رو صدا می‌زنیم، GHCi از مقداری که به عنوانِ ‏‎y‎‏ بهش میدیم برای محاسبه استفاده می‌کنه، که لزوماً مقدار ۱۰ از دستور ِ ‏‎let‎‏ نیست (‏‎y = x + 5‎‏). در مثال آخر، استفاده از ‏‎y‎‏ به عنوان آرگومانِ ‏‎z‎‏، یعنی از مقدارِ ‏‎y‎‏ که در گستره ِ بیرونی هست برای ورودیِ ‏‎z‎‏ به کار رفته. بین متغیرهای هم‌نام، همیشه داخلی‌ترین انقیاد اولویت داره. مهم نیست که پارامترِ ‏‎y‎‏ در ‏‎z‎‏ با ‏‎y‎‏ که قبل‌تر در GHCi تعریف کرده بودیم هم اسمه: ‏‎y‎‏ همیشه به مقداری که ‏‎z‎‏ بهش اعمال میشه انقیاد داده میشه. (لازم به ذکره، این ظاهرِ تسلسلی‌ای که تعریفِ چیزهای مختلف در GHCi به وجود میارن، در واقع پشت پرده یک سریِ بی‌پایان از بیانیه‌های لاندای تودرتو ِه، مشابهِ وقتی که توابع در ظاهر چند آرگومان می‌گیرن، ولی در باطن یک سری توابعِ تودرتو اند).