۵ - ۳تایپ سیگنچرها چطور خونده میشن
در فصلهای قبل دیدیم که با دستورِ :type
یا :t
در REPL اطلاعات تایپها رو پیدا میکنیم. استعلام ِ تایپ رو میشه برای توابع، توابعِ نیمه-اعمالشده، و مقادیر (که به نحوی توابعِ تماماً اعمال شدهاند) انجام داد.
وقتی تایپِ مقادیر رو میپُرسیم، چنین چیزی میبینیم:
Prelude> :t 't'
't' :: Char
-- است Char دارای تایپ 't'
Prelude> :t "julie"
"julie" :: [Char]
-- است String دارای تایپ "julie"
Prelude> :t True
True :: Bool
-- است Bool دارای تایپ True
استعلام ِ تایپ برای اعداد، بجای یه تایپِ معین، تایپکلاسِ اونها رو میده، چون تا وقتی که تایپِ عدد برای کامپایلر تعیین یا مجبور به استنتاج ِ اون بر اساس تابع مربوطه نشده باشه، نمیدونه یه مقدار عددی چه تایپِ معیّنای داره. برای مثال، ۱۳ ممکنه یه عددِ صحیح به نظر برسه، ولی چنین تایپی ما رو محدود به استفاده ار توابعی میکنه که فقط عددِ صحیح میگیرن (یعنی مثلاً از تابعی مثل تقسیمِ کسری نمیتونیم استفاده کنیم). به همین خاطر، کامپایلر تایپی رو اختصاص میده که بیشترین کارایی رو داره (بیشترین پلیمورفیسم)، و اون رو یه مقدارِ پلیمورفیک محدود به Num a => a
معرفی میکنه:
Prelude> :type 13
13 :: Num a => a
-- میتونیم یه تایپ معین
-- براش تعریف کنیم
Prelude> let x = 13 :: Integer
Prelude> :t x
x :: Integer
تایپِ توابع رو هم میشه استعلام کرد:
Prelude> :t not
not :: Bool -> Bool
پس این تابع یه مقدار Bool
میگیره و یه مقدار Bool
ِ دیگه برمیگرونه. این تابع با این تایپ، اصلاً کارهای زیادی نمیتونه انجام بده.*
دقیقتر بگیم، چهار کار میتونه انجام بده. ولی اگه فرض کنیم که اکثر توابع Prelude
ِ استاندارد، کارِ مفید انجام میدن، بین گزینههای خیلی کمتری باید حدس بزنیم.
شناختن و درکِ تایپِ تابع
اون فِلِشی که بالاتر دیدیم (->
)، نوعساز برای توابع در هسکله. با اینکه جزئی از خودِ برنامهست، ولی از لحاظ گرامری مثل بقیهی تایپهایی که تا الان دیدین کار میکنه. مثل Bool
یک نوعساز ِه، با این تفاوت که ->
آرگومان میگیره، و هیچ دادهساز ای هم نداره:
Prelude> :info (->)
data (->) a b
-- بقیهی اطلاعات رو حدف کردیم
اگه این رو با نوعساز ِ توپل ِ دوتایی مقایسه کنین، شباهتشون رو میبینین:
Prelude> :info (,)
data (,) a b = (,) a b
قبلتر دیدیم که سازندهی (,)
باید به دو مقدار اعمال بشه تا یه توپِل بِسازه. یه تابع هم دو آرگومان لازم داره – یک ورودی و یک نتیجه – تا یه تابع باشه. اما برخلاف سازنده ِ توپل، تابع هیچ دادهساز ای نداره. اون مقداری که در سطح جملهای دیده میشه، خودِ تابعه. توابع خودشون مقدار اند.
نقطهی نمادینِ تابع اینه که میتونه اعمال بشه، و ساختار تایپِش هم این رو نشون میده. اون فلش یک عملگر ِ میانوند با دو پارامتر و شرکتپذیری از راسته (البته اعمال تابع شرکتپذیری ِ چپ داره). این پارامترها نشون میدن که با اعمالِ تابع به یه آرگومان، اون آرگومان به اولین پارامتر مقیّد میشه، و پارامتر دوم، b
، تایپ جواب رو ارائه میده. این موارد رو در طول این فصل با جزئیات بیشتر توضیح میدیم.
برگردیم به خوندنِ تایپ سیگنچرها. تابعِ fst
یک مقدار با تایپِ (a, b) -> a
ِه، و ->
یه نوعساز ِ میانوند ِه که دو آرگومان میگیره:
fst :: (a, b) -> a
-- [1] [2] [3]
۱.
اولین پارامترِ fst
تایپِ (a,b)
داره. دقت کنین که خودِ تایپِ توپِل ((,)
) دو تا آرگومانِ a
و b
رو داره.
۲.
تایپِ تابع، (->)
، دو پارامتر داره. یکی (a,b)
ِه، و اون یکی a
، یعنی جواب ِه.
۳.
جواب ِ تابع، که تایپِ a
داره. این همون a
در توپل ِ (a,b)
ِه.
از کجا میدونیم که اون دو تا a
یکساناند؟ اینکه a
در ورودی و a
در خروجی از یک تایپ هستن مشخصِه، از طرف دیگه میبینیم که بین ورودی و خروجی هیچ اتفاقی نمیوفته؛ یعنی هیچ عملیاتی وجود نداره که بتونه بیاد بین ورودی و خروجی و اون a
رو به یه مقدار دیگه از همون تایپ تبدیل کنه.
یه تابعِ دیگه رو ببینیم:
Prelude> :type length
length :: [a] -> Int
تابعِ length
یه آرگومان با تایپ لیست میگیره (به کروشهها دقت کنین) و یک جوابِ Int
برمیگردونه. در این مورد، مقدارِ Int
تعداد المانهای اون لیست میشه. تایپِ مقادیرِ داخلِ لیست نامشخص مونده؛ این تابع اهمیتی به تایپِ سَکَنهی لیست نمیده – در واقع نمیتونه اهمیت بده.
متغیرهای تایپ با محدودیتِ تایپکلاسی
حالا به تایپِ چندتا از توابعِ حساب نگاه میندازیم. اگه یادتون باشه، با گذاشتنِ یه عملگر ِ میانوند بین پرانتز، میشد از اون تابع به صورت پیشوندی استفاده کنیم (درست مثلِ یه تابع معمولی). با اینکار میتونستیم تایپش رو هم بپرسیم:
Prelude> :type (+)
(+) :: Num a => a -> a -> a
Prelude> :type (/)
(/) :: Fractional a => a -> a -> a
به زبان ساده بخوایم بگیم، تابعِ جمع یه آرگومان عددی میگیره، با یه آرگومان عددی دیگه از همون تایپ جمع میکنه، و یه مقدار عددی از همون تایپ برمیگردونه. به همین ترتیب، تابعِ تقسیمِِ کسری یک مقدار کسری میگیره، بر یه مقدارِ کسری دیگه تقسیم میکنه، و یه مقدارِ کسری سوم هم به عنوان جواب برمیگردونه. این توصیف دقیق نیست، ولی برای فعلاً خوبه.
کامپایلر، نامعیّنترین و کلّیترین تایپی که بتونه رو میده. بجای محدود کردنِ این تابع به یه تایپِ معین، یه متغیرِ تایپ ِ پلیمورفیک ِ با محدودیتِ تایپکلاسی به ما میده. توضیح کامل تایپکلاسها رو برای فصل بعد نگه میداریم. چیزی که اینجا لازمه بدونیم، اینه که هر تایپکلاس، مجموعهای از توابعی که برای بسیاری از تایپهای معین کاربرد دارن رو ارائه میده. وقتی یه تایپکلاس، متغیرِ تایپ ای مثل بالا رو محدود میکنه، اون متغیر میتونه هر کدوم از تایپهای معین ای که نمونه ای از اون تایپکلاس دارن باشه، تا تابع بتونه از متودهایی که توی اون تایپکلاس تعریف شدن، به درستی استفاده کنه. با اینکه تایپ معین ِ a
رو نمیدونیم، ولی میدونیم که فقط میتونه یکی از تایپهایی باشه که تایپکلاسِ لازم رو دارن؛ به همین دلیل میگیم محدود شده.
به خاطرِ همین عمومی کردنِ اعداد، میتونیم با اعدادِ یکسان، اعداد تایپهای مختلف رو ارائه بدیم. میتونیم با یه مقدارِ Num a => a
شروع کنیم و بعد نسخههای متنوعی با تایپهای معین ازَش درست کنیم (با استفاده از ::
برای تخصیصِ تایپ به مقادیر):
Prelude> let fifteen = 15
Prelude> :t fifteen
fifteen :: Num a => a
Prelude> let fifteenInt = fifteen :: Int
Prelude> let fifteenDouble = fifteen :: Double
Prelude> :t fifteenInt
fifteenInt :: Int
Prelude> :t fifteenDouble
fifteenDouble :: Double
از Num a => a
به Int
و Double
رسیدیم. Int
و Double
هرکدوم یک نمونه از تایپکلاسِ Num
دارن، به همین دلیل هم تونستیم اون کار رو انجام بدیم:
Prelude> :info Num
[... نوشتههای بیربط حذف شدن ...]
instance Num Int -- Defined in ‘GHC.Num’
instance Num Double -- Defined in ‘GHC.Float’
حالا که هردوی این تایپها یه نمونه از تایپکلاسِ Num
دارن، پس متودهای Num
، مثل جمع، برای هردوشون تعریف شده:
Prelude> fifteenInt + fifteenInt
30
Prelude> fifteenDouble + fifteenDouble
30.0
کار دیگهای که میتونیم انجام بدیم، مقدار fifteen
با تایپ Num a => a
رو به گونهای استفاده کنیم که یه تایپِ معین براش الزام بشه:
Prelude> fifteenDouble + fifteen
30.0
Prelude> fifteenInt + fifteen
30
کاری که نمیشه انجام داد:
Prelude> fifteenDouble + fifteenInt
Couldn't match expected type ‘Double’
with actual type ‘Int’
In the second argument of ‘(+)’,
namely ‘fifteenInt’
In the expression: fifteenDouble + fifteenInt
جمع این دو مقدار ممکن نیست، چون نه تایپشون پلیمورفیک ِه و نه تایپ یکسانی دارن. هر کدوم هم تعریف متفاوتی برای پیادهسازی ِ تابعِ جمع دارن. پیغام خطا به دو تایپِ واقعی و تایپِ مورد انتظار اشاره کرده. تایپِ واقعی تایپیه که ما تأمین کردیم؛ و تایپِ مورد انتظار تایپیه که کامپایلر انتظارش رو داشته. وقتی fifteenDouble
اولین آرگومانمون بود، کامپایلر منتظر بود که مقدار دوم هم تایپِ Double
باشه، ولی در واقع تایپ Int
داشت.
یه تایپ سیگنچر میتونه چندتا محدودیتِ تایپکلاسی روی یک یا چندتا از متغیرها تعریف کنه. گاهی چنین تایپ سیگنچرهایی میبینین (یا مینویسین):
(Num a, Num b) => a -> b -> b
-- یا
(Ord a, Num a) => a -> a -> Ordering
محدودیتها شبیه یه توپل شدن، ولی دقت کنین که یه پارامتر برای تایپِ تابع نیستن، به عنوان توپل هم در سطح جملهای ظاهر نمیشن. هیچ چیز سمت چپِ فلشِ تایپکلاس، =>
، در سطح جملهای نمیاد. توپل ِ محدودیتها در واقع یه ضرب، یا عطف، از محدودیتهاست.
مثال اول در بالا، دو تا محدودیت داره، یکی برای هر متغیر. هم a
و هم b
باید یک نمونه از تایپکلاس Num
داشته باشن. در مثال دوم، هر دو محدودیت روی یک متغیرِ a
اعمال شدن – یعنی a
باید تایپی باشه که از Num
و Ord
نمونه داشته باشه.
تمرینها: تطبیق تایپ
چندتا از توابعی که تا حالا دیدیم رو این پایین لیست کردیم. زیر اون هم تایپ سیگنچرهاشون رو نوشتیم. تایپ سیگنچرها رو با توابع منطبق کنید (اول بدون کمک از کتاب یا GHCi!) بعد هم جوابتون رو چک کنید. شاید آسونتر باشه که از روی تایپ سیگنچر تابعش رو حدس بزنید.
۱.
توابع:
a)
not
b)
length
c)
concat
d)
head
e)
(<)
۲.
تایپ سیگنچرها:
a)
_ :: [a] -> a
b)
_ :: [[a]] -> [a]
c)
_ :: Bool -> Bool
d)
_ :: [a] -> Int
e)
_ :: Ord a => a -> a -> Bool