۱۱ - ۹نیوتایپ یا 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)‎‏. هر معنی‌ای می‌تونه داشته باشه، مثلاً دو عدد رو با هم جمع کنه.