۱۵ - ۶چرا Integer یه Monoid نداره

تایپِ ‏‎Integer‎‏ نمونه ِ ‏‎Monoid‎‏ نداره. هیچ کدوم از تایپ‌های عددی ندارن. اما واضح‌ه که اعداد عملیات‌های مانویدی دارن. قضیه چیه هسکل؟

با اینکه در ریاضیات مانوید ِ اعداد جمع هست، دلیلِ واضحی برای اینکه مانوید ِشون ضرب نباشه وجود نداره. هردوی این عملیات‌ها مانویدی هستن (دوتایی، شرکت‌پذیر، و دارای یک مقدارِ همانی اما هر تایپ به ازای هر تایپکلاس باید فقط یک نمونه ِ یکتا داشته باشه، نه دوتا (یه نمونه برای جمع، و یه نمونه برای ضرب).

این کار نمی‌کنه:

Prelude> let x = 1 :: Integer
Prelude> let y = 3 :: Integer
Prelude> mappend x y

<interactive>:6:1: error
    • No instance for (Monoid Integer)
      arising from a use of ‘mappend’
    • In the expression: mappend x y
      In an equation for ‘it’:
      it = mappend x y

مشخص نیست که منظور از ‏‎mappend‎‏ جمع ِه یا ضرب. به همین دلیل هم میگه که نمونه ِ ‏‎Monoid‎‏ برای اون ‏‎Integer‎‏ها وجود نداره. واضح‌ه.

برای حلِ این تناقض، ‏‎newtype‎‏های ‏‎Sum‎‏ و ‏‎Product‎‏ رو داریم که مشخص می‌کنن از کدوم نمونه ِ ‏‎Monoid‎‏ استفاده بشه. این ‏‎newtype‎‏ها در ماژول ِ ‏‎Data.Monoid‎‏ تعریف شدن. با اینکه دو نمونه ِ ممکن از ‏‎Monoid‎‏ برای مقادیر عددی وجود داره، ما از ترفندهای گستره‌بندی دوری می‌کنیم، و اون قانون که نمونه‌های تایپکلاسی برای هر تایپ یکتا هستن رو رعایت می‌کنیم:

Prelude> mappend (Sum 1) (Sum 5)
Sum {getSum = 6}
Prelude> mappend (Product 5) (Product 5)
Product {getProduct = 25}
Prelude> mappend (Sum 4.5) (Sum 3.4)
Sum {getSum = 7.9}

دقت کنین که میشه از مقادیرِ غیرِ ‏‎Integral‎‏ هم استفاده کرد. در واقع میشه از این ‏‎newtype‎‏های ‏‎Monoid‎‏ برای همه‌ی تایپ‌هایی که نمونه ِ ‏‎Num‎‏ دارن استفاده کنیم.

اعدادِ صحیح تحت جمع و ضرب یک مانوید رو شکل میدن. به طورِ مشابه، لیست‌ها هم تحتِ الحاق، یک مانوید رو شکل میدن.

جا داره بگیم اعداد تنها مجموعه‌ای نیستن که بیشتر از یک مانوید ِ ممکن دارن. لیست‌ها هم بیشتر از یک مانوید دارن، البته تا اینجا فقط با الحاق کار کردیم (در یه فصل دیگه یکی دیگه از مانویدهای لیست رو می‌بینیم). خیلی تایپ‌های دیگه هم اینطوراند. اکثر مواقع هم با استفاده از ‏‎newtype‎‏ قاعده‌ی یکتا بودنِ نمونه رو اعمال می‌کنیم تا رفتارهای مانویدی ِ متفاوت رو متمایز کنیم.

چرا ‏‎newtype‎‏؟

توجیهِ استفاده از ‏‎newtype‎‏ برای کسانی که هنوز درک عمیقی از نحوه‌ی کامپایل شدنِ کُدِ هسکل و نحوه‌ی ارائه‌ی داده‌هایی که در طول اجرا ِ برنامه توسط کامپیوتر استفاده میشن ندارن، کمی سخته. با این حال ما تمام تلاش‌مون رو می‌کنیم، و برای دو نوع مخاطب، به دو نحو توضیح میدیم. بیشتر که در کتاب پیش بریم، با جزئیات بیشتری به مبحثِ ‏‎newtype‎‏ برمی‌گردیم.

اول از همه، بینِ دوتا کُدِ زیر، اختلافِ مفهومیِ زیادی وجود نداره (به غیر از مواقعی که به تهی مربوط میشن... باشه برا بعد):

data Server = Server String

newtype Server' = Server' String

اختلاف‌های اصلی بینِ این دوتا، یکی اینه که استفاده از ‏‎newtype‎‏ نوع‌داده رو محدود به داشتنِ یک داده‌ساز ِ یگانی می‌کنه، و یکی هم اینکه ‏‎newtype‎‏ نبودِ بار ِ اضافی در زمان اجرا به خاطرِ "پوشوندنِ" تایپ اصلی رو تضمین می‌کنه. یعنی ارائه‌ی ‏‎newtype‎‏ در زمان اجرا، و تایپی که زیرش هست دقیقاً یکسان‌اند – زحمت اضافی به خاطر "بسته‌بندی" نداره، در صورتی که تایپ‌های ضرب و تایپ‌های جمع اینطور نیستن.

برای برنامه‌نویس‌های کهنه‌کار که اشاره‌گرها رو بلدن

‏‎newtype‎‏ مثل یک اتحاد تک-عضوی در C هست که بدون ساخت یه اشاره‌گر ِ اضافی، یه نوع‌ساز و داده‌ساز ِ جدید بهتون میده تا با بقیه‌ی چیزهایی که با یک چیز ارائه میشن (که خییییلی زیادن) قاطی نکنین.

دلیل استفاده از ‏‎newtype‎‏ بطور خلاصه

۱.

نشون دادنِ نیّت: استفاده از ‏‎newtype‎‏ نشون میده که قصدتون از استفاده‌ش فقط به عنوان یه پوشنده برای تایپِ زیریش بوده. ‏‎newtype‎‏ نمی‌تونه نهایتاً به یه تایپ جمع یا تایپ ضرب ِ پیچیده رشد کنه، در صورتی که یه نوع‌داده ِ معمولی می‌تونه.

۲.

افزایشِ امنیت تایپی: از قاطی شدنِ مقادیرِ خیلی زیادی که به یک شکل ارائه میشن، مثل ‏‎Text‎‏ و ‏‎Integer‎‏ جلوگیری می‌کنه.

۳.

اضافه کردن نمونه‌های تایپکلاسی‌ای که به غیر از رفتارِ متفاوت، تفاوتِ دیگه‌ای ندارن، مثل ‏‎Sum‎‏ و ‏‎Product‎‏.

توضیح بیشتر برای ‏‎Sum‎‏ و ‏‎Product‎‏

برای اعداد، بیشتر از یک نمونه ِ ‏‎Monoid‎‏ میشه نوشت، به همین خاطر با استفاده از ‏‎newtype‎‏ مشخص می‌کنیم کدوم رو لازم داریم. اگه ‏‎Data.Monoid‎‏ رو وارد کنین، ‏‎newtype‎‏های ‏‎Sum‎‏ و ‏‎Product‎‏ رو می‌بینین:

Prelude> import Data.Monoid
Prelude> :info Sum
newtype Sum a = Sum {getSumm :: a}
...بیشتر نمونه‌ها حذف شدن...
instance Num a => Monoid (Sum a)

Prelude> :info Product
newtype Product a =
   Product {getProduct :: a}
...بیشتر نمونه‌ها حذف شدن...
instance Num a => Monoid (Product a)

از این نمونه‌‌ها می‌فهمیم که برای استفاده از ‏‎Sum‎‏ و ‏‎Product‎‏ به عنوانِ یه ‏‎Monoid‎‏ فقط کافیه شاملِ یه مقدارِ عددی باشن. برای مثال‌های زیر از عملگر ِ میانوندی ِ ‏‎mappend‎‏ استفاده می‌کنیم. همون تایپ رو داره و همون کار رو می‌کنه، فقط جمع‌وجورتره:

Prelude Data.Monoid> :t (<>)
(<>) :: Monoid m => m -> m -> m

Prelude Data.Monoid> Sum "Frank" <> Sum "Herbert"

No instance for (Num [Char]) ...

این مثال به این خاطر کار نکرد که ‏‎a‎‏ در ‏‎Sum a‎‏ یه ‏‎String‎‏ بود (که نمونه ِ ‏‎Num‎‏ نداره).

‏‎Sum‎‏ و ‏‎Product‎‏ کاری رو می‌کنن که انتظار دارین؛ البته با یه کم سورپرایزِ گرامری:

Prelude Data.Monoid> (Sum 8) <> (Sum 9)
Sum {getSum = 17}

Prelude Data.Monoid> mappend mempty Sum 9
Sum {getSum = 9}

اما ‏‎mappend‎‏ فقط دوتا چیز رو با هم متحد می‌کنه، پس این کار رو نمیشه کرد:

λ> mappend (Sum 8) (Sum 9) (Sum 10)

یه پیغام خطای طولانی می‌گیرین که این خط هم شامل‌شه:

Possible cause:
  ‘Sum’ is applied to too many arguments
-- به آرگومان‌های زیادی اعمال شده ‘Sum’ م.
  In the first argument of ‘mappend’,
    namely ‘(Sum 8)’

البته با تودرتو کردن به سادگی حل میشه:

λ> mappend (Sum 1) (mappend (Sum 2) (Sum 3))
Sum {getSum = 6}

با میانوندی کردنِ تابعِ ‏‎mappend‎‏، از اون هم بهتر میشه:

λ> Sum 1 <> Sum 1 <> Sum 1
Sum {getSum = 3}

یا میشه همه‌ی ‏‎Sum‎‏ها رو تو یه لیست بنویسیم و از ‏‎mconcat‎‏ استفاده کنیم:

λ> mconcat [Sum 8, Sum 9, Sum 10]
Sum {getSum = 27}

به لطفِ گرامر ِ ‏‎Sum‎‏ و ‏‎Product‎‏، از اسمِ فیلدهاشون هم برای بیرون کشیدنِ مقدارِ داخل‌شون می‌تونیم استفاده کنیم:

λ> getSum $ mappend (Sum 1) (Sum 1)
2
λ> getProduct $ mappend (Product 5) (Product 5)
25
λ> getSum $ mconcat [(Sum 5), (Sum 6), (Sum 7)]
18

‏‎Product‎‏ هم مشابه ‏‎Sum‎‏ ِه، ولی برای ضرب.