۱۵ - ۶چرا 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)]
18Product هم مشابه Sum ِه، ولی برای ضرب.