۱۵ - ۱۰استفاده از جبر با درخواست جبر
اشاره کردیم که Monoidهای بیشتری برای Maybe به غیر از First و Last وجود دارن. حالا اون یکی نمونه ِ Monoid رو مینویسیم. اینجا دنبالِ انتخابِ یک مقدار بین یه مجموعه از مقادیر نیستیم، بلکه میخوایم مقادیرِ a که مشمولِ تایپِ Maybe a هستن رو ترکیب کنیم.
اول سعی کنین ببینین متوجه یه الگو میشین:
instance Monoid b => Monoid (a -> b)
instance (Monoid a, Monoid b)
=> Monoid (a, b)
instance (Monoid a, Monoid b, Monoid c)
=> Monoid (a, b, c)همهی این Monoidها برای یه تایپ بزرگتر، با استفاده از نمونه ِ Monoidهای تایپهایی که اون تایپ بزرگتر رو تشکیل میدن، Monoid ِشون رو درست میکنن.
حتی اگه فقط یکی از مقادیرِ اون تایپ بزرگتر شامل مقداری از تایپِ بستهبندیشده (مثلِ a در Maybe a) باشه و بقیهی مقادیرش از اون تایپ نداشته باشن، باز هم برای چنین نمونه ِ Monoid ای برای اون تایپ بزرگتر، لازمه که همهی آرگومانهای تایپیش یه نمونه از Monoid داشته باشن. برای مثال، Nothing هیچ مقدارِ a ای (که ازش انتظارِ یه Monoid داریم) نداره، اما Just a داره. پس همهی مقادیرِ ممکن از تایپ Maybe، آرگومان تایپی a رو ندارن. برای یه Maybe Monoid که یک عملیات ِ mappend برای مقادیر a داره، برای اون a حتماً یه Monoid لازم داریم. Monoidهایی مثل First و Last، تایپ Maybe a رو دربرمیگیرن، اما چون مقادیر a رو mappend نمیکنن یا براشون هیچ mempty ای ارائه نمیدن، از اون a انتظار Monoid هم ندارن.
با این حال اگه یه نوعدادهای دارین که آرگومان تایپیای داره که در هیچ کدوم از جملاتش ظاهر نمیشه (یه تایپ تخیلی داره)، تایپچکر هم از شما تقاضای نمونه ِ Monoid نمیکنه. برای مثال:
data Booly a =
False'
| True'
deriving (Eq, Show)
-- (conjunction) عطف
instance Monoid (Booly a) where
mappend False' _ = False'
mappend _ False' = False'
mappend True' True' = True'هیچ محدودیت ِ Monoid برای a لازم نداشتیم چون هیچ وقت mappend ِش نمیکنیم (نمیتونیم؛ وجود نداره)، هیچ mempty ای هم از تایپِ a نخواستیم. دلیل اصلیای که محدودیت لازم نیست همینه، البته بسته به تعریفِ نمونه، با وجود اینکه تایپ بستهبندیشده در مقادیر هم ظاهر بشه، ممکن هست این محدودیت لازم نباشه.
تمرین: Optional Monoid
نمونه ِ Monoid رو برای تایپِ Optional که در واقع همون Maybe هست بنویسین.
ata Optional a =
Nada
| Only a
deriving (Eq, Show)
nstance Monoid a
=> Monoid (Optional a) where
mempty = undefined
mappend = undefinedخروجیهای مورد نظر:
relude> Only (Sum 1) `mappend` Only (Sum 1)
nly (Sum {getSum = 2})
relude> Only (Product 4) `mappend` Only (Product 2)
nly (Product {getProduct = 8})
relude> Only (Sum 1) `mappend` Nada
nly (Sum {getSum = 1})
relude> Only [1] `mappend` Nada
nly [1]
relude> Nada `mappend` Only (Sum 1)
nly (Sum {getSum = 1})شرکتپذیری
این بخش بیشترش دورهست، اما میخوایم شرکتپذیری رو با دقتِ بیشتری بگیم. شرکتپذیری میگه ممکنه آرگومانهای عملیاتتون رو به انواع مختلف شرکت بدین (یا گروهبندی کنین)، و جواب همون میمونه.
چندتا از مثالهایی که میشه دوباره شرکت داده بشن رو دوره کنیم:
Prelude> (1 + 9001) + 9001
18003
Prelude> 1 + (9001 + 9001)
18003
Prelude> (7 * 8) * 3
168
Prelude> 7 * (8 * 3)
168چندتا مثالی که بدون تغییر جوابشون نمیشه پرانتزهاشون رو جابجا کرد هم ببینیم:
Prelude> (1 – 10) – 100
-169
Prelude> 1 – (10 – 100)
91این مشخصه به قدرتمندیِ جابجاییپذیری نیست. جابجاییپذیری یعنی بدون تغییر در نتیجهی نهایی، میشه ترتیب آرگومانها رو عوض کرد. جمع و ضرب جابجاییپذیر اند، اما (++) برای لیستها فقط شرکتپذیر ِه.
این رو با یه نسخهی شیطون از جمع که ترتیبِ آرگومانهاش رو عوض میکنه نشون میدیم:
Prelude> let evilPlus = flip (+)
Prelude> 76 + 67
143
Prelude> 76 `evilPlus` 67
143شواهدی داریم که (+) جابجاییپذیری داره، اما اثبات نداریم.
با این حال همون کار رو با (++) هم میشه انجام بدیم:
Prelude> let evilPlusPlus = flip (++)
Prelude> let oneList = [1..3]
Prelude> let otherList = [4..6]
Prelude> oneList ++ otherList
[1,2,3,4,5,6]
Prelude> oneList `evilPlusPlus` otherList
[4,5,6,1,2,3]در مورد لیستها، با مثال نقض ثابت کردیم که (++) جابجاییپذیری نداره. مهم نیست اگه حتی برای همهی ورودیهای دیگه جابجاییپذیری داشته باشه؛ همین که برای یکی از اونها اینظور نیست، پس قانون جابجاییپذیری براش صادق نیست.
جابجاییپذیری مشخصه ِ مفیدیه، و برای مواقعی که به منظور ارتقاء عملکرد و بازدهی ِ بیشتر، نیاز به تغییر در ترتیبِ محاسبات باشه به درد میخوره، بدون نگرانی برای تغییرِ جواب. از مانویدهای جابجاییپذیر در طراحی سیستمهای گسترده و محدودیتهاشون (که همین مانویدهاییاند که جابجاییپذیری ِ عملیاتشون رو تضمین میکنن) هم استفاده میشه.
با این حال برای ما Monoid از قانون شرکتپذیری تبعیت میکنه اما قانون جابجاییپذیری براش صادق نیست. البته بعضی عملیاتهای مانویدی (جمع و ضرب) جابجاییپذیر هستن.
همانی
همانی یک مقداریه که با هر عملیاتی یه رابطهی خاص داره: اون عملیات رو تبدیل به یه تابع همانی میکنه. اگه تو مدرسه ریاضی خوندین، چندتا از این مقادیرِ همانی رو دیدین:
Prelude> 1 + 0
1
Prelude> 521 + 0
521
Prelude> 1 * 1
1
Prelude> 521 * 1
521صفر مقدارِ همانی برای جمع، و ۱ مقدار همانی برای ضرب ِه. همونطور که گفتیم، اشاره به صفر و ۱ به عنوان مقدارِ همانی، خارج از عملیاتهای خودشون منطقی نیست. یعنی به هیچ عنوان صفر مقدار همانی برای بقیهی عملیاتها نیست. این مشخصه رو با یه تستِ تساوی ِ ساده میشه چک کرد:
Prelude> let myList = [1..424242]
-- صفر مقدار همانی برای جمعه
Prelude> map (+0) myList == myList
True
-- اما برای ضرب نیست
Prelude> map (*0) myList == myList
False
-- مقدار همانی برای ضربه 1
Prelude> map (*1) myList == myList
True
-- اما برای جمع نیست
Prelude> map (+1) myList == myList
Falseاین اون یکی قانون برای Monoid ِه: عملیات ِ دوتایی باید شرکتپذیر باشه و باید یک مقدارِ همانی ِ معقول داشته باشه.
مشکلِ نمونههای یتیم
هم در این فصل و هم در فصل تایپکلاسها گفتیم که جفت تایپکلاس و نمونهش برای یک تایپ، یکتاست.
با این حال وقتی نمونههای یتیم نوشته میشن، امکان داره برای یک تایپ، چند نمونه داشته باشیم. اما به هیچ عنوان نباید نمونههای یتیم نوشت. اگه GHC هشدار ِ نمونههای یتیم بهتون داد، سریع درستش کنین!
حتی اگه چندتا نمونه برای یه تایپ لازم دارین، باز هم از newtype استفاده کنین، نه نمونههای یتیم. با Sum و Product هم دیدیم که بدون توسل به نمونههای یتیم یا هتکِ یکتا بودن نمونههای تایپکلاسی، تونستیم دوتا Monoid برای اعداد تعریف کنیم.
یه مثال از یک نمونهی یتیم و نحوهی درست کردنش ببینیم. اول یه پوشه برای پروژه درست کنین و واردِ اون پوشه بشین:
$ mkdir orphan-instance && cd orphan-instanceبعد دوتا فایل میسازیم و تو هر کدوم یه ماژول میذاریم:
module Listy where
newtype Listy a =
Listy [a]
deriving (Eq, Show)
module ListyInstances where
import Data.Monoid
import Listy
instance Monoid (Listy a) where
mempty = Listy []
mappend = (Listy l) (Listy l') =
Listy $ mappend l l'پس آدرس ِ پروژهمون این شکلی میشه:
$ tree
.
├── Listy.hs
└── ListyInstances.hsحالا برای اینکه ListyInstances رو طوری بسازیم که Listy رو ببینه، باید از پرچم ِ -I برای شامل کردن پوشهی فعلی، و متعاقباً مرئی کردنِ ماژولهای توش، استفاده کنیم. اون نقطه جلوی I در سیستمهای یونیکس یعنی "این پوشه." اگه موفق بشین، باید چنین چیزی ببینین:
$ ghc -I. --make ListyInstances.hs
[2 of 2] Compiling ListyInstancesدقت کنین که تنها خروجی این کار یه شیئِ فایلی میشه؛ یه ماژولی که میشه ازش به عنوان یه کتابخونه استفاده بشه، دلیلش هم اینه که هیچ main ای برای ساختن فایل قابل اجرا تعریف نکردیم. به این خاطر از این روش ساختیم تا از گرفتاریهای شروع یه پروژه (با دستوری مثل stack new) دوری کنیم. برای هر پروژهای که یه کم پیچیدهتره یا قراره مدت زمان بیشتری کار کنه، از یه ابزار مدیریت ِ ساخت و وابستگی مثل Cabal استفاده کنین (اگه دارین از Stack استفاده میکنین، از Cabal هم استفاده میکنین).
حالا یه مثال از دلیل مشکلزا بودن نمونههای یتیم. اگه نمونه ِ Monoid از ListyInstances رو تو Listy کپی کنیم و بعد ListyInstances رو دوباره بسازیم، خطای زیر رو میگیریم:
$ ghc -I. --make ListyInsatnces.hs
[1 of 2] Compiling Listy
[2 of 2] Compiling ListyInstances
Listy.hs:7:10:
Duplicate instance declarations:
-- :م. تعریفِ نمونههای تکراری
instance Monoid (Listy a)
-- Defined at Listy.hs:7:10
instance Monoid (Listy a)
-- Defined at ListyInstances.hs:5:10چنین تداخلِ تعریف نمونهها برای هرکسی که از نسخهی قبلیِ کُدمون استفاده کنه ممکنه پیش بیاد. و این یه مشکله.
حتی اگر هم نمونههای متداخل با هم به یه ماژول وارد نشن باز هم مشکل به حساب میاد، چون باعث میشه بسته به اینکه چه ماژولهایی وارد شدن، متودهای تایپکلاسی رفتارِ متفاوتی داشته باشن، که چنین چیزی مزیتها و پیشفرضهای اساسی تایپکلاسها رو نقض میکنه.
چندتا راه حل برای نمونههای یتیم وجود دارن:
۱.
تایپ رو تعریف کردین ولی تایپکلاس رو تعریف نکردین؟ نمونه رو تو همون ماژولی که تایپ رو تعریف کردین بنویسین تا نشه تایپ رو بدونِ نمونههاش وارد کرد.
۲.
تایپکلاس رو تعریف کردین اما تایپ رو نه؟ نمونه رو در همون ماژولی بذارین که تایپکلاس تعریف شده تا نشه تایپکلاس رو بدون نمونههاش وارد کرد.
۳.
نه تایپ و نه تایپکلاس مالِ خودتونه؟ با یه newtype تایپ اصلی رو بپوشونین تا یه تایپی که متعلق به خودتونه داشته باشین، و بتونین براش نمونههای تایپکلاسی داشته باشین. بعداً راههایی رو توضیح میدیم که این پروسه رو سادهتر میکنن.
برای اینکه بتونیم از مزایای تایپکلاسها و همچنین مشخصههای استدلالیای که دارن بیشترین بهره رو ببریم، باید این محدودیتها رو رعایت کنیم. یک تایپ فقط باید یک پیادهسازیِ یکتا (مجرد) از یه تایپکلاسِ در گستره داشته باشه، و دوری از نمونههای یتیم هم راه جلوگیری از نمونههای متداخله. بدونید که کتابخونهنویسها خیلی بیشتر به دوری از نمونههای یتیم پایبنداند تا کسانی که برنامه درست میکنن، با این حال اهمیتِ چنین چیزی در برنامهها اصلاً کمتر نیست.