۱۶ - ۴بریم سراغ f
همونطور که بالاتر گفتیم، در طولِ تعریفِ تایپکلاسِ Functor
، f
باید همون f
بمونه، و باید به تایپی اشاره کنه که فانکتور داره. در این بخش عواقبِ عملی از این مشخصات رو با جزئیات بررسی میکنیم.
اولین چیزی که میدونیم اینه که f
اینجا باید گونهش * -> *
باشه. در فصلهای قبل از تایپهای گونهبالا صحبت کردیم، و به خاطر داریم که یه ثابتِ تایپ، یا تایپِ معین، گونه ِ *
داره. یه تایپ با گونه ِ * -> *
، منتطرِ اعمال شدن به یه ثابتِ تایپ با گونه ِ *
هست.
به دو دلیل میدونیم که گونه ِ f
در تعریفِ Functor
باید * -> *
باشه، که اول توصیف میکنیم و بعد هم نشون میدیم:
۱.
هر آرگومان ( و جواب) در تایپ سیگنچر ِ یه تابع باید یه تایپِ تماماً اعمالشده باشه. هر آرگومان باید کایند ِ *
داشته باشه.
۲.
تایپِ f
در دوجا به یه آرگومان اعمال شده بود: f a
و f b
. چون f a
و f b
هرکدوم باید کایند ِ *
داشته باشن، پس خودِ f
باید گونه ِ * -> *
باشه.
دیدنِ مفهومِ اینها در عمل خیلی راحتتره، ما هم با یه عالَم کُد نشون میدیم.
ای ستارهی درخشان، پرده ز رخسار بردار
همهی آرگومانهای نوعساز ِ ->
باید از گونه ِ *
باشن. اگه کایند ِ نوعساز ِ تابع رو استعلام کنیم، معلوم میشه:
Prelude> :k (->)
(->) :: * -> * -> *
جواب و همهی آرگومانهای هر تابع باید ثابتِ تایپ باشن، نه یه نوعساز. با توجه به این موضوع، از روی تابعِ fmap
، یه چیزی در موردِ Functor
واضح میشه:
class Functor f where
fmap :: (a -> b) -> f a -> f b
-- کایند: * -> * -> *
تایپ سیگنچر ِ fmap
میگه f
که در تعریفِ کلاسِ Functor
معرفی شده، باید یک آرگومانِ تایپی قبول کنه، و در نتیجه از گونه ِ * -> *
باشه. چنین چیزی رو میشه بدون شناختنِ تایپکلاس هم تشخیص داد. با چندتا تایپکلاسِ بیمعنی میشه نشون داد:
class Sumthin a where
s :: a -> a
class Else where
e :: b -> f (g a b c)
class Biffy where
slayer :: e a b
-> (a -> c)
-> (a -> d)
-> e c d
حالا بازشون میکنیم:
class Sumthin a where
s :: a -> a
-- [1] [1]
۱.
تایپِ آرگومان و جواب هردو یه a
اند. چیزِ دیگهای هم نیست، پس a
دارای گونه ِ *
هست.
class Else where
e :: b -> f (g a b c)
-- [1] [2] [3]
۱.
این b
، مشابهِ a
در مثالِ قبل، به تنهایی اولین آرگومانِ (->)
هست، پس کایند ِش میشه *
.
۲.
اینجا f
بیرونیترین نوعساز برای تایپ ِآرگومانِ دوم (تایپِ جواب) از (->)
هست. یک آرگومان میگیره، همون تایپِ g a b c
که تو پرانتزه. پس گونه ِ f
میشه * -> *
.
۳.
و g
به سه آرگومانِ a
، b
، و c
اعمال شده. در نتیجه کایند ِش میشه * -> * -> * -> *
.
-- استفاده از :: برای کایند سیگنچر
g :: * -> * -> * -> *
-- همه * هست ،c و ،b ،a کایندِ
g :: * -> * -> * -> *
g a b c (g a b c)
class Biffy where
slayer :: e a b
-- [1]
-> (a -> c)
-- [2] [3]
-> (a -> d)
-> e c d
۱.
اول از همه، e
یکی از آرگومانهای (->)
ِه، پس اعمال به آرگومانهای خودش باید گونه ِ *
رو نتیجه بده. با توجه به این موضوع، و همچنین اینکه میبینیم خودش دوتا آرگومان داره، a
و b
، پس مشخص میشه که کایند ِش * -> * -> *
هست.
۲.
این a
یکی از آزگومانهای یه تابعست، و خودش هیچ آرگومانی نمیگیره، پس کایند ش میشه *
.
۳.
داستانِ c
درست مثل a
ِه، فقط تو یه نقطهی دیگه از همون تابع قرار گرفته.
بررسیِ کایندها برای مثالهای بعدی شکست میخوره:
class Impish v where
impossibleKind :: v -> v a
class AlsoImp v where
nope :: v a -> v
اسمِ متغیر در یه تعریفِ تایپکلاس قبل از where
، همهی موردهای دیگه از اون متغیر در تمامِ تعریف رو به اون اسم مقیّد میکنه. GHC متوجه میشه که v
بعضی اوقات آرگومان داره و بعضی اوقات نداره، و اگه چنین چرندیاتی بهش بدیم، مُچمون رو میگیره:
‘v’ is applied to too many type arguments
In the type ‘v -> v a’
In the class declaration for ‘Impish’
Expecting one more argument to ‘v’
Expected a type, but ‘v’ has kind ‘k0 -> *’
In the type ‘v a -> v’
In the class declaration for ‘AlsoImp’
GHC علاوه بر استنتاجِ تایپ، استنتاجِ کایند هم داره. و مشابهِ تایپها، نه تنها کایندها رو استنتاج میکنه، بلکه یکپارچگی و منطقی بودنشون رو هم بررسی میکنه.
تمرینها: مهربون باشین
در هرکدوم از تایپ سیگنچرهای زیر، کایند ِ همهی متغیرهای تایپ رو پیدا کنید:
۱.
کایند ِ a
چیه؟
a -> a
۲.
کایند ِ b
و T
چیاند؟ (T
عمداً بزرگ نوشته شده!)
a -> b a -> T (b a)
۳.
کایند ِ c
چیه؟
c a b -> c b a
ستارهای درخشان برای نگاه تو
خوب اگه تایپمون گونهبالا نباشه چطور؟ با یه ثابتِ تایپ امتحان کنیم ببینیم چی میشه:
-- functors1.hs
data FixMePls =
FixMe
| Pls
deriving (Eq, Show)
instance Functor FixMePls where
fmap =
error "مهم نیست، کامپایل نمیشه"
دقت کنین هیچجا آرگومانِ تایپی وجود نداره– همهشون تک ستارهی درخشان (و مهربون!) اند. اگه این فایل رو از GHCi بارگذاری کنیم، چنین پیغامی میگیریم:
Prelude> :l functors1.hs
[1 of 1] Compiling Main
( functors1.hs, interpreted )
functors1.hs:8:18:
The first argument of ‘Functor’
should have kind ‘* -> *’,
but ‘FixMePls’ has kind ‘*’
In the instance declaration for
‘Functor FixMePls’
Failed, modules loaded: none.
کلاً درخواستِ Functor
برای FixMePls
خیلی منطقی نیست. اگه تایپها رو بررسی کنیم، مشخص میشه چرا:
-- Functor:
fmap :: Functor f => (a -> b) -> f a -> f b
-- جابجا کنیم FixMePls رو با f اگه
(a -> b) -> FixMePls a -> FixMePls b
-- هیچ آرگومان تایپی FixMePls ولی
-- :نمیگیره، پس در واقع اینطوریه
(FixMePls -> FixMePls)
-> FixMePls
-> FixMePls
هیچ نوعساز ِ f
ای وجود نداره! اگه این رو تا آخر پلیمورفیک کنیم، چنین تایپی میشه:
(a -> b) -> a -> b
پس در حقیقت، نبودِ آرگومانِ تایپی یعنی:
($) :: (a -> b) -> a -> b
بدونِ آرگومانِ تایپی، همون اعمالِ تابع میشه.
فانکتور همون اعمالِ تابع هست
الان دیدیم که چطور نتیجهی Functor
دادن به یه ثابتِ تایپ، معادل با اعمالِ تابع شد. اما در حقیقت، fmap
یه حالتِ خاص از همون اعمالِ تابع هست. تایپها رو ببینیم:
fmap :: Functor f => (a -> b) -> f a -> f b
fmap
یه عملگر ِ میانوند هم داره. اگه از یکی از نسخههای قدیمیترِ GHC استفاده میکنین، ممکنه لازم باشه Data.Functor
رو وارد کنین تا ازش در REPL استفاده کنین. واضحه که تایپش عینِ fmap
ِ ِپیشوندی ِه:
-- ه fmap مستعارِ میانوندی برای <$>
(<$>) :: Functor f
=> (a -> b)
-> f a
-> f b
متوجه چیزی شدین؟
(<$>) :: Functor f
=> (a -> b) -> f a -> f b
($) :: (a -> b) -> a -> b
Functor تایپکلاسیه برای اعمال ِ تابع از روی یا بالا سَر ِ یه ساختاری که میخوایم نادیده بگیریم و دست نخورده بمونه. جلوتر که قانونهای Functor
رو بگیم، منظورمون از دست نخورده بمونه رو هم با جزئیات توضیح میدیم.
ستارهای درخشان برای نگاه تو تا حقیقتِ f
رو دریابی
دلیلِ نیاز به گونهبالا بودنِ f
رو یه کم بیشتر بررسی کنیم.
اگه یه آرگومانِ تایپی به نوعدادهای که بالاتر تعریف کردیم اضافه کنیم، FixMePls
تبدیل به یه نوعساز میشه و کُدِ زیر کار میکنه:
-- functors2.hs
data FixMePls a =
FixMe
| Pls a
deriving (Eq, Show)
instance Functor FixMePls where
fmap =
error "مهم نیست، کامپایل نمیشه"
حالا کامپایل میشه!
Prelude> :l code/functors2.hs
[1 of 1] Compiling Main
Ok, modules loaded: Main.
وایسا... اون error رو دیگه لازم نداریم! پس اون نمونه ِ Functor
صحیحش اینطوری میشه:
-- functors3.hs
data FixMePls a =
FixMe
| Pls a
deriving (Eq, Show)
instance Functor FixMePls where
fmap _ FixMe = FixMe
fmap f (Pls a) = Pls (f a)
اگه این نمونه رو با تایپِ fmap
مقایسه کنیم:
fmap :: Functor f
=> (a -> b) -> f a -> f b
fmap f (Pls a) = Pls (f a)
-- (a -> b) f a f b
از حرفِ f
در تایپِ fmap
برای نشون دادنِ Functor
استفاده کردیم، برای آرگومانی که خودش یه تابعه هم استفاده کردیم. هردوی اینها جزء رسم و رسومهای نوشتاریاند. فکر نکنین که f
در نمونه ِ FixMePls
و f
در تعریفِ تایپکلاسِ Functor
یکساناند.
حالا کُدمون خوشحاله!
Prelude> :l code/functors3.hs
[1 of 1] Compiling Main
Ok, modules loaded: Main.
Prelude> fmap (+1) (Pls 1)
Pls 2
ببینید چطور تابع از روی ساختار به داخلش اعمال شد. هسکلنویسهای حرفهای همینطوری تابعهای گُنده و سنگین رو از روی ساختارهای انتزاعی بلند میکنن!
یه اشتباهِ دیگه هم ببینیم. اگه تایپِ نمونه ِ Functor
رو از FixMePls
به FixMePls a
تغییر بدیم چی میشه؟
-- functors4.hs
data FixMePls a =
FixMe
| Pls a
deriving (Eq, Show)
instance Functor (FixMePls a) where
fmap _ FixMe = FixMe
fmap f (Pls a) = Pls (f a)
دقت کنین تایپ رو تغییری ندادیم؛ هنوز فقط یه آرگومان میگیره. اما حالا اون آرگومان جزئی از ساختار ِ f
شده. اگه این کُدِ بدقواره رو بارگذاری کنیم:
Prelude> :l code/functors4.hs
[1 of 1] Compiling Main
functors4.hs:8:19
The first argument of ‘Functor’
should have kind ‘* -> *’,
but ‘FixMePls a’ has kind ‘*’
In the instance declaration for
‘Functor (FixMePls a)’
Failed, modules loaded: none.
خطای مشابه این رو قبلتر هم گرفتیم. بعد از اعمال ِ اون نوعساز، گونه ِش از * -> *
به *
تغییر کرد.
تایپکلاسها و کلاسهای سازنده
شاید اولین بار که نوعساز با گونه ِ * -> *
رو در تعریفِ Functor
دیدین یه کم تعجب کردین– این کاملاً طبیعیه! در واقع نسخههای اولیهی هسکل اصلاً قابلیتِ بیانِ تایپکلاسها با تایپهای گونهبالا رو نداشتن. مارک جونز* در حالی که داشت روی یکی از پیادهسازیهای هسکل به نامِ Gofer کار میکرد، این قابلیت رو طراحی کرد. این کار تایپکلاسها رو از محدودیت به تایپهای با گونه ِ *
(که بهشون ثابت هم میگیم)، به کارا بودن برای تایپهای گونهبالا (نوعسازها) تعمیم داد.
تو هسکل این دو حالت* طوری تلفیق شدن که دیگه کلاسهای سازنده رو متمایز از تایپکلاسها نمیدونیم. این رو فقط به این دلیل گفتیم چون خوبه که بدونین اینجا اتفاق مهمی افتاده. حالا راهی داریم که راجع به محتویاتِ تایپها، مستقل از تایپی که ساختار ِ اون محتویات هست صحبت کنیم. به همین خاطر هم یه چیزی مثل fmap
داریم تا بتونیم محتویاتِ یک مقدار رو، بدون اینکه ساختار ِ بیرونیش ( یه لیست، یا یه Just
) رو دست بزنیم تغییر بدیم.
م. منظور کلاسهایی هست که از دو گونه تایپ استفاده میکنن: یکی برای تایپهای ثابت، و یکی هم برای تایپهای گونهبالا. به کلاسهایی که با ثابتهای تایپ تعریف میشن، کلاسِ تایپ یا تایپکلاس، و به کلاسهایی که با تایپهای گونهبالا تعریف میشن، کلاسِ سازنده گفته میشده.