۱۲ - ۴گونه‌ها، هزار ستاره در تایپ‌ها

کایندها، یه سطح بالاتر از تایپ‌ها اند، و برای توصیفِ تایپِ نوع‌سازها به کار میرن. یکی از قابلیت‌های حائزِ اهمیتِ هسکل، دارا بودنِ تایپ‌های گونه‌بالا هست. لغتِ گونه‌بالا در واقع از توابع سطح بالا گرفته شده: توابعی که تابع‌های بیشتری به عنوان آرگومان می‌گیرن. و در مقابل، نوع‌سازها (یعنی تایپ‌های گونه‌بالا) تایپ‌هایی هستن که تایپ‌های بیشتری به عنوان آرگومان می‌گیرن. در گزارشِ هسکل، به تایپ‌هایی که هیچ آرگومانی نمی‌گیرن و به‌خودیِ‌خود تایپ هستن، ثابتِ تایپ گفته میشه. اونجا نوع‌ساز به تایپ‌هایی گفته میشه که حتماً باید آرگومان بگیرن تا تایپ بشن.

همونطور که فصل قبل گفتیم، اینها مثال‌هایی از ثابت‌های تایپ اند:

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] 

اهمیتِ چنین چیزی در فصل‌های آینده خیلی واضح‌تر میشه.