۱۷ - ۶قوانین اپلیکتیو
بعد از بررسی هر قانون، هرکدوم از بیانیهها رو در REPL تست کنین.
۱.
همانی
این تعریفِ قانون همانی ِه:
pure id <*> v = vبرای دیدن مثالهایی از این قانون، این بیانیهها رو محاسبه کنین.
pure id <*> [1..5]
pure id <*> Just "Hello Applicative"
pure id <*> Nothing
pure id <*> Left "Error'ish"
pure id <*> Right 8001
-- هم یه نمونه داره ((->) a)
pure id <*> (+1) $ 2اگه یادتون باشه، Functor هم یه قانون همانی ِ مشابه داره، و شاید مقایسهشون با هم به درکِ این قانون کمک کنه:
id [1..5]
fmap id [1..5]
pure id <*> [1..5]بر طبقِ قانون همانی، هر سهتای اونها باید برابر باشن. تساویشون رو تو REPL میتونین امتحان کنین، یا میتونین یه تست ِ ساده بنویسین تا به جواب برسین. خب pure چه کاری انجام میده؟ تابعِ id رو داخلِ یه ساختاری میپوشونه تا بتونیم بجای fmap از اَپلای استفاده کنیم.
۲.
ترکیبپذیری
این تعریفِ قانونِ ترکیبپذیری برای اپلیکتیوهاست:
pure (.) <*> u <*> v <*> w =
u <*> (v <*> w)شاید گرامرِش یه کم نامأنوس باشه، ولی در واقع شبیهِ قانون ترکیبپذیری برای Functor ِه. این قانون چنین چیزی میگه: اگه اول توابع رو با هم ترکیب کنیم، بعد تابعِ حاصل از ترکیب ِ اونها رو اعمال کنیم، باید همون جوابی رو بده که اگه اول توابع رو اعمال کنیم و بعد با هم ترکیبِشون کنیم. اینجا از عملگر ِ ترکیب، بجای گرامر ِ میانوندی که خیلی رایجتره، بصورت پیشوندی استفاده کردیم، و به کمکِ pure اون عملگر رو بردیم زیرِ ساختار ِ متناسب تا بشه ازش با اَپلای استفاده کنیم.
pure (.)
<*> [(+1)]
<*> [(*2)]
<*> [(1, 2, 3)]
[(+1)] <*> ([(*2)] <*> [(1, 2, 3)])
pure (.)
<*> Just (+1)
<*> Just (*2)
<*> Just 1
Just (+1)
<*> (Just (*2) <*> Just 1)هدفِ این قانون، تضمینِ خروجیِ قابل پیشبینی از ترکیب کردنِ اعمالِ تابع هاست.
۳.
هومومورفیسم
هومومورفیسم یا homomorphism یه نگاشت بین دو ساختار ِ جبری با حفظِ ساختار ِه. تأثیرِ ناشی از اعمال ِ یه تابع که داخلِ یه جور ساختار پوشونده شده به یه مقدار که اون هم داخلِ یه ساختار پوشونده شده، باید با اعمال ِ یه تابع به یه مقدار بدونِ تأثیرگذاری روی ساختار ِ بیرونی یکسان باشه:
pure f <*> pure x = pure (f x)اون تعریفِ قانون ِه. در عمل اینطور میشه:
pure (+1) <*> pure 1
pure ((+1) 1)اون دو خط کُد باید یک جواب بدن. در حقیقت، جوابی که از اونها میگیریم، نباید فرقی با جوابِ این داشته باشه:
(+1) 1چون ساختاری که pure تأمین میکنه مفهومی نداره. پس میشه این قانون رو مرتبط با بخشِ مانویدی ِ اپلیکتیو فرض کرد: جواب باید معادلِ نتیجهی اعمالِ تابع، بدونِ انجام کاری با ساختارها به غیر از ترکیبِشون باشه. درست مثلِ fmap که در واقع یه حالتِ خاص از اعمال تابع بود که ساختار ِ اطراف یا بافت رو نادیده میگرفت، اپلیکتیو هم یه نوع اعمالِ تابع هست که ساختار رو حفظ میکنه. فقط به خاطرِ اینکه در اپلیکتیو، خودِ تابع هم ساختار داره، اون ساختارها باید مانویدی باشن تا بشه به نحوی با هم قاطی بشن.
pure (+1) <*> pure 1 :: Maybe Int
pure ((+1) 1) :: Maybe Intاون دوتا هم باز باید یه جواب بدن، اما اینجا ساختار با Maybe تعیین شده، پس آیا جوابِ:
(+1) 1اینبار هم برابر میشه؟
چندتا مثالِ دیگه هم میشه امتحان کرد:
pure (+1) <*> pure 1 :: [Int]
pure (+1) <*> pure 1 :: Either a Intایدهی کلی از قانون هومومورفیسم اینه که اعمال تابع، ساختار ِ اطرافِ مقادیر رو تغییر نمیده.
۴.
جایگزینی
قانون جایگزینی رو هم اول با تعریفش شروع میکنیم:
u <*> pure y = pure ($ y) <*> uاگه خُردِش کنیم شاید بهتر باشه. در سمتِ چپِ <*>، همیشه باید یه تابع، داخلِ یه جور ساختار باشه. در تعریفِ بالا، u چنین تابعی رو نشون میده. مثلاً:
Just (+2) <*> pure 2
-- u <*> pure y
-- برابر است با
Just 4سمت راستِ تعریف شاید کمتر واضح باشه. توسطِ بخشبندی ِ عملگر ِ اعمالِ تابع ($) با y، کاری کردیم که y منتظرِ یه تابع باشه که بهش اعمال بشه. تایپها رو هم مینویسیم تا شاید کمک کنه:
pure ($ 2) <*> Just (+ 2)
-- به خاطر داشته باشین که
-- رو مشخصتر کرد ($ 2) میشه
($ 2) :: Num a => (a -> b) -> b
Just (+ 2) :: Num a => Maybe (a -> b)اگه ($ 2) یه کم گیجتون کرده، به خاطر داشته باشین که این بخشبندی ِ عملگر ِ دلار ِه که فقط به آرگومان دومش اعمال شده. در نتیجه تایپش اینطوری تغییر میکنه:
-- این دوتا یکساناند
($ 2)
\f -> f $ 2
($) :: (a -> b) -> a -> b
($ 2) :: (a -> b) -> bاگه تایپ متودهای Applicative رو اختصاصی کنیم:
mPure :: a -> Maybe a
mPure = pure
embed :: Num a => Maybe ((a -> b) -> b)
embed = mPure ($ 2)
mApply :: Maybe ((a -> b) -> b)
-> Maybe (a -> b)
-> Maybe b
mApply = (<*>)
myResult = pure ($ 2) `mApply` Just (+2)
-- myResult == Just 4حالا متغیرهای تایپ رو با حروفِ متفاوت جایگزین میکنیم تا واضحتر بتونیم تایپهای اصلی رو با این تایپهای اختصاصی شده مقایسه کنیم:
(<*>) :: Applicative f
=> f (x -> y)
-> f x
-> f y
mApply :: Maybe ((a -> b) -> b)
-> Maybe (a -> b)
-> Maybe b
f ~ Maybe
x ~ (a -> b)
y ~ b
(x -> y) ~ (a -> b) -> bبر طبقِ قانون جایگزینی، این باید صادق باشه:
(Just (+2) <*> pure 2)
== (pure ($ 2) <*> Just (+2))گرامر ِ عجیبش به کنار، تقریباً هم واضحه که چرا باید برابر باشن، چون دو تابع در واقع یه کار انجام میدن. چندتا مثال دیگه هم نوشتیم که امتحان کنین:
[(+1), (*2)] <*> pure 1
pure ($ 1) <*> [(+1), (*2)]
Just (+3) <*> pure 1
pure ($ 1) <*> Just (+3)