۷ - ۲آرگومانها و پارامترها
اگه از حرفهای قبلی یادتون باشه، به خاطرِ کاری کردن، توابعِ هسکل ممکنه در ظاهر چند پارامتر داشته باشن؛ ولی در واقع همهی توابع فقط یه آرگومان میگیرن و یه خروجی میدن. در هسکل توابع رو از راههای گرامری ِ متنوعی میسازیم، و مبنای همهی این راههای گرامری یه بیانیهایه که آرگومان میگیره. توابع کلاً بر این پایه تعریف میشن که قابلیتِ اعمال شدن به یه آرگومان و برگردوندنِ یه جواب رو دارن.
هر مقداری در هسکل میتونه آرگومانِ توابع باشه. به مقداری که بشه ازش به عنوانِ آرگومان تابع استفاده کرد، مقدارِ ممتاز میگیم؛ که در هسکل به توابع هم اطلاق میشه، یعنی یه تابع میتونه آرگومانِ یه تابعِ دیگه بشه. هر زبان برنامهنویسیای اجازهی چنین چیزی رو نمیده، ولی امیدواریم توضیحاتی که از تایپِ تابع و کاری کردن دادیم، دلیل و طرز کار این قابلیت رو تا حدی روشن کرده باشه.
تعیین پارامترها
در هسکل، اسم پارامترها بین اسم تابع (که همیشه سمت چپ قرار داره) و علامت مساوی تعریف میشن (اسم پارامتر، هم از اسمِ تابع و هم از علامت مساوی با فاصلهی سفید جدا میشه). این اسمْ یه متغیره، و هر وقت تابع رو به یه آرگومان اعمال میکنیم مقدار آرگومان به اون پارامتر مقیّد میشه.
اول یه مقدار بدون پارامتر تعریف میکنیم:
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 به وجود میارن، در واقع پشت پرده یک سریِ بیپایان از بیانیههای لاندای تودرتو ِه، مشابهِ وقتی که توابع در ظاهر چند آرگومان میگیرن، ولی در باطن یک سری توابعِ تودرتو اند).