۱۲ - ۴گونهها، هزار ستاره در تایپها
کایندها، یه سطح بالاتر از تایپها اند، و برای توصیفِ تایپِ نوعسازها به کار میرن. یکی از قابلیتهای حائزِ اهمیتِ هسکل، دارا بودنِ تایپهای گونهبالا هست. لغتِ گونهبالا در واقع از توابع سطح بالا گرفته شده: توابعی که تابعهای بیشتری به عنوان آرگومان میگیرن. و در مقابل، نوعسازها (یعنی تایپهای گونهبالا) تایپهایی هستن که تایپهای بیشتری به عنوان آرگومان میگیرن. در گزارشِ هسکل، به تایپهایی که هیچ آرگومانی نمیگیرن و بهخودیِخود تایپ هستن، ثابتِ تایپ گفته میشه. اونجا نوعساز به تایپهایی گفته میشه که حتماً باید آرگومان بگیرن تا تایپ بشن.
همونطور که فصل قبل گفتیم، اینها مثالهایی از ثابتهای تایپ اند:
Prelude> :kind Int
Int :: *
Prelude> :k Bool
Bool :: *
Prelude> :k Char
Char :: *
گرامر ِ ::
معمولاً دارای تایپِ ... هست خونده میشه، اما علاوه بر تایپ سیگنچرها، برای کایند سیگنچرها هم موردِ استفادهست.
مثال زیر یه نمونه از تایپیه که بجای ثابت تایپ، یه نوعساز داره:
data Example a = Blah | RoofGoats | Woot a
Example
یه نوعساز ِه، و نه یه ثابت تایپ، به این خاطر که یه آرگومان تایپیِ a
میگیره که برای دادهساز ِ Woot
استفاده میشه. در GHCi کایندها رو با دستورِ :k
استعلام میکنیم:
Prelude> data Example a = Blah | RoofGoats | Woot a
Prelude> :k Example
Example :: * -> *
Example
یک پارامتر داره، پس برای اینکه یه تایپ مشخص بشه (و متعاقباً با یه *
نشون داده بشه) باید به یک تایپ اعمال بشه. توپل ِ دوتایی دو آرگومان میگیره، در نتیجه برای تبدیل شدن به یه تایپِ معیّن باید به دو تایپ اعمال بشه:
Prelude> :k (,)
(,) :: * -> * -> *
Prelude> :k (Int, Int)
(Int, Int) :: *
نوعدادههای Maybe
و Either
هم که تو این فصل دیدیم نوعساز دارن (نه ثابت تایپ). یعنی برای معیّن شدن اول باید به یه آرگومان اعمال بشن. مشابهِ تأثیرِ کاری کردن در تایپ سیگنچرها، اعمال ِ Maybe
به یه نوعساز ِ a
ما رو از یه دونه فِلِش خَلاص میکنه و به یه ستارهی مهربون میرسونه:
Prelude> :k Maybe
Maybe :: * -> *
Prelude> :k Maybe Int
Maybe Int :: *
از طرف دیگه، Either
باید به دو آرگومانِ a
و b
اعمال شه، پس کایند ِ Either
میشه ستاره به ستاره به ستاره:
Prelude> :k Either
Either :: * -> * -> *
تأثیرِ اعمال شدنش به آرگومانها رو هم میشه دید:
Prelude> :k Either Int
Either Int :: * -> *
Prelude> :k Either Int String
Either Int String :: *
همونطور که گفتیم، کایند ِ *
یه تایپِ مشخص رو نشون میده. هیچ چیزی منتظرِ اعمال شدن نَمونده.
تایپهای لیفتشده و لیفتنشده
بخوایم دقیقتر بگیم، کایند ِ *
کایند ِ همهی تایپهای لیفتشده میشه، اما تایپهایی که کایند ِ #
دارن، لیفتنشده هستن. یه تایپِ لیفتشده، که شامل هر نوعداده ای که خودتون هم میتونین تعریف کنین میشه، هر تایپیه که یکی از اعضاش ممکنه تهی باشه. تایپهای لیفتشده با یه اشارهگر نشون داده میشن، و شاملِ بیشتر نوعدادههایی که تا الان دیدیم (و بیشتر تایپهایی که میبینین و استفاده میکنین) میشه. تایپهای لیفتنشده، تایپهایی اند که نمیتونن در اعضاشون تهی داشته باشن. تایپهای با کایند ِ #
معمولاً تایپهای سطح ماشینی و اشارهگرهای خام هستن. newtype
ها یه مورد خاصاند که کایند ِشون *
ِه، اما لیفتنشده اند چون دقیقاً عینِ تایپِ زیرشون ارائه میشن، پس newtype
بهخودیِخود هیچ اشارهگری به غیر از تایپی که شاملش میشه درست نمیکنه. پس فقط چیزی که داخل newtype
هست میتونه مقدارِ تهی داشته باشه، خودِ newtype
نمیتونه، در نتیجه newtype
ها لیفتنشده هستن. در GHC، تایپهای معیّن و تماماً اعمالشده، بطورِ پیشفرض کایند ِ *
هستن.
حالا چی میشه اگه نوعساز ِمون یه آرگومان بگیره؟
Prelude> data Identity a = Identity a
Prelude> :k Identity
Identity :: * -> *
همونطور که در فصل قبل گفتیم، فِلِشی که در کایند سیگنچر هست نشان از نیاز به اعمال شدن ِه (مثل فِلِشِ تابع در تایپ سیگنچرها). در این مورد، تایپ رو با اعمال ِ اون به یه تایپ دیگه میسازیم.
Maybe
رو در نظر بگیریم:
data Maybe a = Nothing | Just a
تایپِ Maybe
برای تبدیل به تایپِ معیّن یک آرگومان میگیره، پس یه نوعساز ِه:
Prelude> :k Maybe
Maybe :: * -> *
Prelude> :k Maybe Int
Maybe Int :: *
Prelude> :k Maybe Bool
Maybe Bool :: *
Prelude> :k Int
Int :: *
Prelude> :k Bool
Bool :: *
اما مثالِ زیر کار نمیکنه چون کایندها جور نیستن:
Prelude> :k Maybe Maybe
Expecting one more argument to ‘Maybe’
The first argument of ‘Maybe’ should have kind ‘*’,
but ‘Maybe’ has kind ‘* -> *’
In a type in a GHCi command: Maybe Maybe
Maybe
انتظارِ یک آرگومان تایپی با کایند ِ *
داره، که Maybe
کایند ِش فرق داره.
اگه به Maybe
یه آرگومان تایپی با کایند ِ *
بدین، اون موقع خودش هم *
میشه و برای یه Maybe
ِ دیگه قابل استفاده میشه:
Prelude> :k Maybe Char
Maybe Char :: *
Prelude> :k Maybe (Maybe Char)
Maybe (Maybe Char) :: *
تایپِ Example
که بالاتر تعریف کردیم هم نمیشه آرگومانِ Maybe
باشه:
Prelude> :k Maybe Example
Expecting one more argument to ‘Example’
The first argument of ‘Maybe’ should have kind ‘*’,
but ‘Example’ has kind ‘* -> *’
In a type in a GHCi command: Maybe Example
مجدداً اگه نوعساز ِ Example
رو اعمال کنیم درست میشه، و میتونیم یه مقدار از اون تایپ درست کنیم:
Prelude> :k Maybe (Example Int)
Maybe (Example Int) :: *
Prelude> :t Just (Woot n)
Just (Woot n) :: Maybe (Example Int)
نوعساز ِ لیست []
هم کایند ِ * -> *
داره. دقت کنین که به لطفِ شکر گرامری، میشه بجای [] a
و [] Int
، از گرامر ِ گویاترِ [a]
و [Int]
استفاده کنیم:
Prelude> :k []
[] :: * -> *
Prelude> :k [] Int
[] Int :: *
Prelude> :k [Int]
[Int] :: *
پس به همون دلیل که Maybe Maybe
نمیشد داشته باشیم، Maybe []
هم نمیشه داشته باشیم، اما Maybe [Bool]
موردی نداره:
Prelude> :k Maybe []
Expecting one more argument to ‘[]’
The first argument of ‘Maybe’ should have kind ‘*’,
but ‘[]’ has kind ‘* -> *’
In a type in a GHCi command: Maybe []
Prelude> :k Maybe [Bool]
Maybe [Bool] :: *
اگه به خاطر دارین، اولین باری که از Maybe
استفاده کردیم یه نسخهی مطمئن از تابعِ tail
در فصل لیستها نوشتیم:
safeTail :: [a] -> Maybe [a]
safeTail [] = Nothing
safeTail (x:[]) = Nothing
safeTail (_:xs) = Just xs
به محض اعمال ِ این تابع به یه مقدار، تایپهای پلیمورفیک به تایپهای محدود یا معیّن تبدیل میشن:
Prelude> safeTail "julie"
Just "ulie"
Prelude> :t safeTail "julie"
safeTail "julie" :: Maybe [Char]
Prelude> safeTail [1..10]
Just [2,3,4,5,6,7,8,9,10]
Prelude> :t safeTail [1..10]
safeTail [1..10] :: (Num a, Enum a) => Maybe [a]
Prelude> :t safeTail [1..10 :: Int]
safeTail [1..10 :: Int] :: Maybe [Int]
میشه تغییرِ کایندها با افزایش آرگومانهایی که نوعسازها میگیرن رو هم ببینیم:
Prelude> data Trivial = Trivial
Prelude> :k Trivial
Trivial :: *
Prelude> data Unary a = Unary a
Prleude> :k Unary
Unary :: * -> *
Prelude> data TwoArgs a b = TwoArgs a b
Prelude> :k TwoArgs
TwoArgs :: * -> * -> *
Prelude> data ThreeArgs a b c = ThreeArgs a b c
Prelude> :k ThreeArgs
ThreeArgs :: * -> * -> * -> *
شاید الان به نظر نرسه که دونستنِ اینها فایدهای بیشتر از کمک به درک خطاهای تایپی داشته باشه. در یکی از فصلهای آتی مفهوم گونهبالا بودن واضحتر میشه.
دادهسازها تابعاند
در فصل قبل به تفاوت بین دادههای ثابت و دادهسازها اشاره کردیم، و گفتیم که دادهسازهایی که کاملاً اعمال نشدن، فِلِشِ تابعی دارن. بعد از اعمال ِ اونها به آرگومانهاشون، یه مقدار با تایپِ متناسب برمیگردونن. به کلام دیگه، دادهسازها تابعاند. تازه کاری هم میشن.
اول ببینیم که دادهسازهای پوچگانه (که مقادیری بدونِ آرگومان هستن)، مثل توابع نیستن:
Prelude> data Trivial = Trivial deriving Show
Prelude> Trivial 1
Couldn't match expected type ‘Integer -> t’
with actual type ‘Trivial’
(... و غیره ...)
اما دادهسازهایی که آرگومان میگیرن، رفتارِ مشابه توابع دارن:
Prelude> data UnaryC = UnaryC Int deriving Show
Prelude> :t UnaryC
UnaryC :: Int -> UnaryC
Prelude> UnaryC 10
UnaryC 10
Prelude> :t UnaryC 10
UnaryC 10 :: UnaryC
تایپِ آرگومانهاشون هم بر اساسِ تعریف، تایپچِک میشه، مثلِ توابع:
Prelude> UnaryC "blah"
Couldn't match expected type ‘Int’
with actual type ‘[Char]’
اگه میخواستیم دادهساز ِ یگانیمون شامل هر تایپی بشه، باید پارامتردار تعریف میکردیم:
Prelude> data Unary a = Unary a deriving Show
Prelude> :t Unary
Unary :: a -> Unary a
Prelude> :t Unary 10
Unary 10 :: Num a => Unary a
Prelude> :t Unary "blah"
Unary "blah" :: Unary [Char]
باز هم مثلِ توابع کار میکنه، فقط تایپِ آرگومان هر چیزی میتونه باشه.
دقت کنین که اگه بخوایم نمونه ِ Show
رو برای Unary
مشتق بگیریم، GHC باید نشون دادنِ مقدارِ a
ِ داخلِ Unary
رو هم بَلَد باشه:
Prelude> :info Unary
data Unary a = Unary a
instance Show a => Show (Unary a)
اگه برای a
از تایپی استفاده کنیم که نمونه ِ Show
نداره، تا وقتی که نخوایم مقدار رو چاپ کنیم به مشکلی نمیخوریم:
Prelude> :t (Unary id)
(Unary id) :: Unary (t -> t)
-- نداره Show نمونهی id
Prelude> show (Unary id)
<interactive>:53:1:
No instance for (Show (t0 -> t0))
...
تنها راهِ جلوگیری از چنین مشکلی اینه که نمونه ِ Show
رو طوری بنویسیم که مقدارِ زیرِ دادهساز ِ Unary
رو نشون نده، اما چنین کاری خیلی رایج نیست.
یه چیز دیگه که باید در نظر داشت اینه که بطورِ معمول نمیشه تایپهای پلیمورفیک رو از نوعساز ِتون مخفی کنین، پس این مثال کار نمیکنه:
Prelude> data Unary = Unary a deriving Show
Not in scope: type variable ‘a’
برای آوردنِ a
تو گستره، میتونیم همراه با نوعساز ِمون معرفیش کنیم. راههای دیگه هم وجود دارن، اما به ندرت لازم میشن و مناسبِ هسکلنویس مبتدی نیستن.
با مثال زیر که از fmap
و دادهساز ِ Just
از Maybe
استفاده میکنه، رفتارِ تابع-مانندِ Just
رو نشون میدیم:
Prelude> fmap Just [1, 2, 3]
[Just 1,Just 2,Just 3]
اهمیتِ چنین چیزی در فصلهای آینده خیلی واضحتر میشه.