۱۱ - ۹نیوتایپ یا newtype
تو این بخش نحوهی تعریف تایپی که میتونه فقط یک دادهساز ِ یگانی داشته باشه رو توضیح میدیم. برای تعریف چنین تایپهایی از کلیدواژه ِ newtype استفاده میکنیم، چراکه از تایپهایی که با data تعریف میشن، و از تایپهای مترادف که با type مشخص میشن تفاوت دارن. مثل بقیهی نوعدادههایی که فقط یک سازنده ِ یگانی دارن، کاردینالیتی ِ یه newtype هم برابرِ کاردینالیتی ِ تایپیه که در خودش داره.
یه newtype نمیتونه تایپ ضرب یا تایپ جمع باشه، سازنده ِ پوچگانه هم نمیتونه داشته باشه، اما مزیتهایی نسبت به تعاریف data داره. یکی اینکه بار اضافی در زمان اجرا نداره، چرا که از همون ارائهای که برای تایپ مشمولش وجود داره استفاده میکنه. دلیل اینکه میتونه چنین کاری بکنه، اینه که حق نداره یه رکورد (تایپ ضرب) یا تجمیع علامتدار (تایپ جمع) باشه. وقتی کامپایلر زبانِ مقصد رو ایجاد میکنه، دیگه تفاوتِ بینِ newtype و تایپی که داخلش هست از بین رفته.
برای اینکه نشون بدیم، فرض کنین یه تابع با تایپِ Int -> Bool داریم که میگه آیا زیادی بزغاله داریم یا نه:
tooManyGoats :: Int -> Bool
tooManyGoats n = n > 42با این تابع اگه برای دامهای مختلف حدهای مختلف هم میخواستیم، به مشکل میخوردیم. اگه اشتباهاً بجای تعداد بزغالهها، تعداد گاوها رو وارد کنیم چی؟ خوشبختانه این مشکل رو با استفاده از سازندههای یگانی میشه برطرف کرد:
newtype Goats =
Goats Int deriving (Eq, Show)
newtype Cows =
Cows Int deriving (Eq, Show)حالا میتونیم تایپِ تابعمون رو بازنویسی کنیم تا مطمئنتر باشه، و با تطبیق الگو به Int ِ داخلِ دادهساز ِ Goat هم دسترسی پیدا کنیم:
tooManyGoats :: Goats -> Bool
tooManyGoats (Goats n) = n > 42حالا دیگه شمارشِ دامها رو قاطی نمیکنیم:
Prelude> tooManyGoats (Goats 43)
True
Prelude> tooManyGoats (Cows 43)
Couldn't match expected type
‘Goats’ with actual type ‘Cows’
In the first argument of
‘tooManyGoats’, namely ‘(Cows 43)’
In the expression: tooManyGoats (Cows 43)در رابطه با نمونههای تایپکلاسی هم newtypeها مزایایی دارن. برای دیدنِ اون مزایا، باید newtypeها رو با تایپهای مترادف و تعریف دادههای معمولی مقایسه کنیم. با یه مقایسهی مختصر بینِ newtype و تایپ مترادف شروع میکنیم.
یه newtype از این لحاظ مشابهِ تایپِ مترادف ِه که در واقع با تایپی که داخلش هست یکسان ِه و هر تفاوتی بین تایپ جدید و تایپ داخلش وجود داره، در لحظهی کامپایل حذف میشه. پس یه String در حقیقت یه [Char] ِه، و Goats هم در بالا در واقع یه Int ِه. این تفاوتِ ظاهری برای انسانهایی که کُد رو میخونَن یا مینویسن میتونه مفید باشه، اما تأثیری به حالِ کامپایلر نداره.
با همهی اینها، یه تفاوت کلیدی بینِ newtype و تایپ مستعار اینه که با newtype، امکان تعریفِ نمونههای تایپکلاسیای که با نمونه ِ تعریف شده برای تایپِ مشمولشون فرق داشته باشه، وجود داره. با تایپهای مترادف نمیشه چنین کاری کرد. با یه مثال ببینیم چطوری میشه. اول یه تایپکلاس به اسمِ TooMany، و یه نمونه ازش برای Int تعریف میکنیم:
class TooMany a where
tooMany :: a -> Bool
instance TooMany Int where
tooMany n = n > 42میتونیم از اون نمونه در REPL استفاده کنیم، اما به خاطرِ پلیمورفیک بودنِ لفظهای عددی، حتماً باید تایپ Int رو به لفظ ِ عددیای که به عنوان آرگومان میدیم تخصیص بدیم. اینطور میشه:
Prelude> tooMany (42 :: Int)یه کم با اینها بازی کنین – آرگومانهای مختلف رو امتحان کنین و ببینین اگه تعریف تایپ رو حذف کنین چی میشه.
حالا فرض کنیم برای بزغالهشماری یه نمونه ِ خاص از TooMany لازم داریم که رفتارش با نمونه ِ Int فرق داره. پشت پرده، Goats هنوز Int ِه، اما تعریف با newtype این امکان رو میده که یه نمونه ِ دلخواه تعریف کنیم:
newtype Goats = Goats Int deriving Show
instance TooMany Goats where
tooMany (Goats n) = n > 43این رو بارگذاری کنین و با آرگومانهای مختلف امتحان کنین. آیا رفتارش با نمونه ِ Int که بالاتر داشتیم فرقی داره؟ آیا هنوز هم باید صراحتاً یه تایپ برای لفظهای عددی بدین؟ تایپِ tooMany چیه؟
اینجا تونستیم یه نمونه ای از TooMany برای Goats (که با newtype درست شده بود) تعریف کنیم که رفتار متفاوتی نسبت به Int داشت. اگه یه مترادف تایپ بود نمیشد چنین کاری کرد. باور نمیکنین؟ خودتون امتحان کنین.
اما اگه بخوایم از همون نمونههای تایپکلاس که تایپِ داخلِ newtype داره استفاده کنیم چطور؟ برای تایپکلاسهای رایج در GHC مثل Eq، Ord، Enum، و Show میشه با deriving خودبهخود نمونههاشون رو داشته باشیم؛ که قبلاً هم دیدیم.
اما برای تایپکلاسهایی که خودمون تعریف کردیم، میتونیم از یه توسعهی زبانی به اسمِ GeneralizedNewtypeDeriving استفاده کنیم. پراگما* ِ LANGUAGE در GHC، با در اختیار گذاشتنِ توسعههای زبانی، راههایی برای پردازشِ ورودیها به کامپایلر میگه که فراتر از چیزیاند که به طور استاندارد در اختیارِ کامپایلر هست. کاری که این توسعه انجام میده، اینه که به کامپایلر میگه امکانِ اتکای newtype به نمونههای تایپکلاسیِ تایپی که داخلش هست رو فراهم کنه. با این حال، چنین چیزی خارج از رفتارِ استانداردِ کامپایلر ِه، و باید اون دستورِ خاص رو بهش بدیم تا بتونیم چنین کاری بکنیم.
پراگما یه دستور خاص به کامپایلر ِه که در فایل منبع نوشته میشه. پراگما ِ LANGUAGE بیشترین استفاده رو بین بقیهی پراگماها در GHC Haskell داره. چندتا از اون پراگماهای دیگه رو بعداً در کتاب میبینیم.
اول ببینیم بدونِ قابلیتِ مشتقگیری ِ عمومی برای newtype (م. یعنی بدونِ استفاده از اون توسعهی زبانی) باید چی کار میکردیم:
class TooMany a where
tooMany :: a -> Bool
instance TooMany Int where
tooMany n = n > 42
newtype Goats =
Goats Int deriving (Eq, Show)
instance TooMany Goats where
tooMany (Goats n) = tooMany nاین نمونه برای Goats همون کاری رو میکنه که نمونه ِ Int انجام میده، اما باز هم باید جداگانه مینوشتیمش.
خودتون هم میتونین این رو امتحان کنین و ببینین که جوابها یکسان میشن.
حالا اون پراگما رو به بالای فایل منبع اضافه میکنیم:
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
class TooMany a where
tooMany :: a -> Bool
instance TooMany Int where
tooMany n = n > 42
newtype Goats =
Goats Int deriving (Eq, Show, TooMany) دیگه لازم نیست یه نمونه از TooMany برای Goats که دقیقاً عینِ نمونه ِ Int هست تعریف کنیم. میشه از همون نمونه ای که داریم دوباره استفاده کنیم.
چنین چیزی برای مواقعی که میخوایم همهی تایپکلاسها، به غیر از یکی، یکسان باشن کاربرد داره.
تمرینها: بزغالههای منطقی
۱.
یه نمونه از تایپکلاسِ TooMany (که بالاتر نوشتیم) برای تایپِ (Int,String) بنویسین. اگه از newtype استفاده نکنین، به یه پراگما ِ LANGUAGE به اسمِ FlexibleInstance احتیاج دارین. GHC خودش راهنمایی میکنه.
۲.
یه نمونه ِ دیگه از TooMany برای (Int,Int) بنویسین. با فرض اینکه اینها تعدادِ بزغالهها از دو مزرعه هستن، اونها رو جمع کنین.
۳.
یه نمونه ِ دیگه از TooMany تعریف کنین، این بار برای (Num a, TooMany a) => (a,a). هر معنیای میتونه داشته باشه، مثلاً دو عدد رو با هم جمع کنه.