۱۷ - ۶قوانین اپلیکتیو

بعد از بررسی هر قانون، هرکدوم از بیانیه‌ها رو در 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)