۱۵ - ۱۳نیمگروه
اونطوری که ریاضیدانها با جبر بازی میکنن، آدم رو یاد اون همکلاسی دوران مدرسه میندازه که پای حشرهها رو میکَند. بعضی وقتا هم پاهاشون رو میچسبونن سر جاش، اما در این مورد که از Monoid
به سمتِ Semigroup
پیش میریم، داریم یه پا رو میکَنیم. برای رسیدن به نیمگروه از مانوید، فقط کافیه مقدارِ همانی رو حذف کنیم. عملیات ِ اصلی هنوز دوتایی و شرکتپذیر باقی میمونه.
در نتیجه تعریفِ Semigroup
از این قراره:
class Semigroup a where
(<>) :: a -> a -> a
فقط هم یک قانون داریم:
(a <> b) <> c == a <> (b <> c)
عملیاتی که Semigroup
ارائه میده هنوز یه عملیات ِ دوتایی و شرکتپذیر ِه، که دوتا چیز رو به هم متصل میکنه (مثل الحاق یا جمع)، ولی یه مقدارِ همانی نداره. از این لحاظ، جبر ِ ضعیفتری به حساب میاد.
هنوز جزئی از base
نیست
تا ۸ GHC، تایپکلاس Semigroup
توی base
هست، اما جزئی از Prelude
نیست. اگه عملیاتهاش رو لازم داشته باشین باید Data.Semigroup
رو وارد کنین. حواستون باشه که برای خودش یه نسخهی جامعتر از (<>)
رو داره که فقط یه محدودیت ِ Semigroup
لازم داره تا محدودیت ِ Monoid
.
نوعداده ِ NonEmpty
که الان میخوایم بگیم رو میتونین با وارد کردنِ Data.List.NonEmpty
بیارین تو REPL.
NonEmpty
، یه نوعداده ِ بهدردبخور
یه نوعداده ِ مفید که نمیتونه نمونه ِ Monoid
داشته باشه، اما Semigroup
میتونه داشته باشه، تایپِ لیستِ NonEmpty
ِه. یه نوعداده ِ لیستیه که هیچوقت نمیتونه خالی باشه:
data NonEmpty a = a :| [a]
deriving (Eq, Ord, Show)
-- نمونههاش رو ننوشتیم
این :|
یه دادهساز ِ میانوند ِه که دو آرگومان (تایپی) میگیره. حاصلضرب ِ a
و [a]
هست. تضمین میکنه که همیشه حداقل یک مقدار با تایپ a
داریم، تضمینی که با [a]
نداره، چون ممکنه خالی باشه.
با اینکه برخلافِ اکثرِ دادهسازهای دیگهای که دیدین، :|
الفباعددی نیست، اسمِ یه دادهساز ِ میانوند ِه. دادهسازهایی که اسمشون فقط با علامتهای غیرالفباعددی که با دونقطه شروع میشن تعریف شده، به صورتِ پیشفرض میانوندی اند؛ اونهایی هم که الفباعددی اند بطور پیشفرض پیشوندی اند:
-- یا پیشوندی prefix
data P =
Prefix Int String
-- یا میانوندی infix
data Q =
Int :!!: String
از اونجا که دادهساز ِ دوم الفباعددی نیست، نمیشه پیشوندی ازش استفاده کرد:
data R =
:!!: Int String
خطای گرامری میده:
parse error on input ‘:!!:’
Failed, modules loaded: none.
برعکسش هم هست، از یه دادهساز ِ پیشوندی نمیشه میانوندی استفاده کرد:
data S =
Int Prefix String
یه خطای دیگه میده:
Not in scope: type constructor or class ‘Prefix’
A data constructor of that name is in scope;
did you mean DataKinds?
Failed, modules loaded: none.
برگردیم سرِ اصل مطلب، NonEmpty
. از اونجا که NonEmpty
ضرب ِ دو آرگومانه، اینطور هم میشد بنویسیمش:
newtype NonEmpty a =
NonEmpty (a, [a])
deriving (Eq, Ord, Show)
برای NonEmpty
نمیشه یه Monoid
نوشت، چون اصلاً طوری طراحی نشده که مقدار همانی داشته باشه. هیچ لیست خالیای وجود نداره که عملیات روی یه لیستِ NonEmpty
رو تبدیل به تابع همانی کنه، ولی عملیات ِ دوتایی ِ شرکتپذیر داره: دوتا لیستِ NonEmpty
رو میشه به هم الحاق داد. چنین تایپی که یه عملیات ِ باینری ِ شرکتپذیر ِ کانونیک داره ولی مقدارِ همانی نداره با Semigroup
خیلی خوب جور درمیاد. یه مثال اجمالی برای استفاده از NonEmpty
از کتابخونه ِ semigroups
با mappend
ِ نیمگروه (تا GHC ۸٫۰٫۱، Semigroup
و NonEmpty
هردو در base
هستن، اما در Prelude
نیستن):
-- رو نصب کنین semigroups ممکنه لازم باشه
Prelude> import Data.List.NonEmpty as N
Prelude N> import Data.Semigroup as S
Prelude N S> 1 :| [2, 3]
1 :| [2,3]
Prelude N S> :t 1 :| [2, 3]
1 :| [2,3] :: Num a => NonEmpty a
Prelude N S> :t (<>)
(<>) :: Semigroup a => a -> a -> a
Prelude N S> let xs = 1 :| [2, 3]
Prelude N S> let ys = 4 :| [5, 6]
Prelude N S> xs <> ys
1 :| [2,3,4,5,6]
Prelude N S> N.head xs
1
Prelude N S> N.length (xs <> ys)
6
غیر از اینها، استفاده از NonEmpty
مشابهِ لیسته، فقط صراحتاً اعلام کردین که برای کاری که دارین انجام میدین، نداشتنِ هیچ مقداری قابل قبول نیست. به خاطر تعریفِ نوعداده که نمیذاره بدون مقدار یه NonEmpty
بسازین، چنین محدودیتی تحمیل میشه.