۱۵ - ۱۵تمرین‌های فصل

تمرین‌های نیم‌گروه

برای نوع‌داده‌های زیر نمونه ِ ‏‎Semigroup‎‏ تعریف کنین. هرجا لازم بود، برای متغیرهای تایپ محدودیت ِ ‏‎Semigroup‎‏ بذارین. تایپکلاسِ ‏‎Semigroup‎‏ رو از کتابخونه ِ ‏‎semigroups‎‏ بیارین (یا از ‏‎base‎‏، اگه از ۸ GHC استفاده می‌کنین)، یا می‌تونین خودتون تعریف‌ش کنین. هرجا از ‏‎(<>)‎‏ استفاده کردیم، منظورمون ‏‎mappend‎‏ ِ میانوند از تایپکلاسِ ‏‎Semigroup‎‏ ِه.

نکته

برای تایپ‌هایی که تو تمرین‌ها بهتون میدیم، همیشه همه‌ی نمونه‌هایی که ازشون لازم دارین رو مشتق نمی‌گیریم. دیگه تا این نقطه از کتاب ازتون انتظار داریم خودتون تشخیص بدین کدوم‌ها رو لازم دارین و ترتیب‌ش رو بدین.

۱.

همه‌ی نمونه‌هاتون رو با ‏‎QuickCheck‎‏ تست کنین. به خاطر اینکه تنها قانون ِ ‏‎Semigroup‎‏ شرکت‌پذیری ِه، تنها مشخصه‌ای هم که باید استفاده کنین همونه. حواستون باشه که به احتمال زیاد باید ماژولهای ‏‎Monoid‎‏ و ‏‎Semigroup‎‏ رو وارد کنین، پس مراقبِ تلاقی بینِ دوتا ‏‎(<>)‎‏ها باشین (البته بسته به نسخه‌ی GHC که دارین).

data Trivial = Trivial deriving (Eq, Show)

instance Semigroup Trivial where
  _ <> _ = undefined

instance Arbitrary Trivial where
  arbitrary = return Trivial

semigroupAssoc :: (Eq m, Semigroup m)
               => m -> m -> m -> Bool
semigroupAssoc a b c =
  (a <> (b <> c)) == ((a <> b) <> c)

type TrivAssoc =
  Trivial -> Trivial -> Trivial -> Bool

main :: IO ()
main = quickCheck (semigroupAssoc :: TrivAssoc)

۲.

newtype Identity a = Identity a

۳.

data Two a b = Two a b
-- .دیگه هم بخواین Semigroup راهنمایی: یه

۴.

data Three a b c = Three a b c

۵.

data Four a b c d = Four a b c d

۶.

newtype BoolConj =
    BoolConj Bool
--  ^------^ یا عطفِ بولی boolean conjunction .م

کاری که باید انجام بده:

Prelude> (BoolConj True) <> (BoolConj True)
BoolConj True
Prelude> (BoolConj True) <> (BoolConj False)
BoolConj False

۷.

newtype BoolDisj =
    BoolDisj Bool
--  ^------^ یا فصلِ بولی boolean disjunction .م

کاری که باید انجام بده:

Prelude> (BoolDisj True) <> (BoolDisj True)
BoolDisj True
Prelude> (BoolDisj True) <> (BoolDisj False)
BoolDisj True

۸.

data Or a b =
    Fst a
  | Snd b

‏‎Semigroup‎‏ برای ‏‎Or‎‏ باید چنین رفتاری داشته باشه. میشه فرض کنیم یه مقدار ‏‎Snd‎‏ ِ چسبناک داره، یعنی اولین ‏‎Snd‎‏ ای که پیدا کنه رو نگه می‌داره. یه چیزی شبیه مانویدی که با ‏‎First'‎‏ نوشتیم.

Prelude> Fst 1 <> Snd 2
Snd 2
Prelude> Fst 1 <> Fst 2
Fst 2
Prelude> Snd 1 <> Fst 2
Snd 1
Prelude> Snd 1 <> Snd 2
Snd 1

۹.

شرکت‌پذیری برای ‏‎Combine‎‏ رو به سادگی نمیشه تست کرد، چون شاملِ تابع هست.

newtype Combine a b =
  Combine { unCombine :: (a -> b) }

کاری که باید انجام بده:

Prelude> let f = Combine $ \n -> Sum (n + 1)
Prelude> let g = Combine $ \n -> Sum (n - 1)
Prelude> unCombine (f <> g) $ 0
Sum {getSum = 0}
Prelude> unCombine (f <> g) $ 1
Sum {getSum = 2}
Prelude> unCombine (f <> f) $ 1
Sum {getSum = 4}
Prelude> unCombine (g <> f) $ 1
Sum {getSum = 2}

راهنمایی: این تابع نهایتاً به یک مقدار از تایپِ ‏‎a‎‏ اعمال میشه. اما چندتا تابع خواهید داشت که همه‌شون یه مقدار از تایپِ ‏‎b‎‏ تولید می‌کنن. چطور چندتا مقدار رو ترکیب کنیم تا یه ‏‎b‎‏ ِ مجرد بدست بیاریم؟ این یکی شاید یه کم سخت باشه! یادتون باشه که مقداری که داخلِ ‏‎Combine‎‏ هست، یه تابع ِه. اگه از ‏‎CoArbitrary‎‏ سردر نمیارین، لازم نیست ‏‎QuickCheck‎‏ ِش کنین.

۱۰.

newtype Comp a =
  Comp { unComp :: (a -> a) }

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

۱۱.

-- آشنا نیست؟

data Validation a b =
  Failure a | Success b
  deriving (Eq, Show)

instance Semigroup a =>
  Semigroup (Validation a b) where
    (<>) = undefined

۱۲.

-- که Semigroup با یه Validation
-- کار متفاوتی انجام میده

newtype AccumulateRight a b =
  AccumulateRight (Validation a b)
  deriving (Eq, Show)

instance Semigroup b =>
  Semigroup (AccumulateRight a b) where
    (<>) = undefined

۱۳.

-- که Semigroup با یه Validation
-- کار بیشتری انجام میده

newtype AccumulateBoth a b =
  AccumulateBoth (Validation a b)
  deriving (Eq, Show)

instance (Semigroup a, Semigroup b) =>
  Semigroup (AccumulateBoth a b) where
    (<>) = undefined

تمرین‌های مانوید

برای نوع‌داده‌های زیر نمونه ِ ‏‎Monoid‎‏ تعریف کنین. هرجا لازم بود محدودیت ِ ‏‎Monoid‎‏ به متغیرهای تایپ اضافه کنین. برای نوع‌داده‌هایی که براشون ‏‎Semigroup‎‏ تعریف کردین، فقط کافیه مقدارِ همانی‌شون رو پیدا کنین.

۱.

باز هم همه‌ی نمونه‌هاتون رو با ‏‎QuickCheck‎‏ تست کنین. برای تایپِ ‏‎Trivial‎‏ مثال زدیم.

data Trivial = Trivial deriving (Eq, Show)

instance Semigroup Trivial where
  (<>) = undefined

instance Monoid Trivial where
  mempty = undefined
  mappend = (<>)

type TrivAssoc =
  Trivial -> Trivial -> Trivial -> Bool

main :: IO ()
main = do
  let sa = semigroupAssoc
      mli = monoidLeftIdentity
      mri = monoidRightIdentity
  quickCheck (sa :: TrivAssoc)
  quickCheck (mli :: Trivial -> Bool)
  quickCheck (mri :: Trivial -> Bool)

۲.

newtype Identity a =
  Identity a deriving Show

۳.

data Two a b = Two a b deriving Show

۴.

newtype BoolConj =
  BoolConj Bool

کاری که باید انجام بده:

Prelude> (BoolConj True) `mappend` mempty
BoolConj True
Prelude> mempty `mappend` (BoolConj False)
BoolConj False

۵.

newtype BoolDisj =
  BoolDisj Bool

کاری که باید انجام بده:

Prelude> (BoolDisj True) `mappend` mempty
BoolDisj True
Prelude> mempty `mappend` (BoolDisj False)
BoolDisj False

۶.

newtype Combine a b =
  Combine { unCombine :: (a -> b) }

کاری که باید انجام بده:

Prelude> let f = Combine $ \n -> Sum (n + 1)
Prelude> unCombine (mappend f mempty) $ 1
Sum {getSum = 2}

۷.

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

newtype Comp a =
  Comp (a -> a)

۸.

این تمرین شاید یه کم غیرطبیعی به نظر بیاد و ممکنه یه ذره سخت باشه. اگه از قبل تجربه‌ی برنامه‌نویسیِ تابعی یا هسکل ندارین و تونستین این تمرین رو حل کنین، یه شیرینیِ حسابی برا خودتون بگیرین. تعریفِ اولیه‌ی نمونه ِ ‏‎Monoid‎‏ هم نوشتیم.

newtype Mem s a =
  Mem {
    runMem :: s -> (a,s)
  }

instance Monoid a => Monoid (Mem s a) where
  mempty = undefined
  mappend = undefined

با توجه به کُدِ زیر:

f' = Mem $ \s -> ("hi", s + 1)

main = do
  let rmzero = runMem mempty 0
      rmleft = runMem (f' <> mempty) 0
      rmright = runMem (mempty <> f') 0
  print $ rmleft
  print $ rmright
  print $ (rmzero :: (String, Int))
  print $ rmleft == runMem f' 0
  print $ rmright == runMem f' 0

یه ‏‎Monoid‎‏ ِ صحیح برای ‏‎Mem‎‏ باید چنین خروجی‌ای بده:

Prelude> main
("hi",1) 
("hi",1) 
("",0)
True
True

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

کَلَکی در کار نیست، برای ‏‎s‎‏ هم تایپکلاسِ ‏‎Monoid‎‏ لازم ندارین. بله، چنین ‏‎Monoid‎‏ ای می‌تونه وجود داشته باشه. راهنمایی: مقادیرِ ‏‎s‎‏ رو از یه تابع به تابع دیگه به هم زنجیر کنین. معمولاً در اولین تلاش، قانون همانی رعایت نمیشه، پس شاید بهتر باشه اول از همه قانون همانی رو چک کنین.