۱۵ - ۶چرا 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
ِه، ولی برای ضرب.