۲۴ - ۸ترانسفورمرِ Identity یا IdentityT
همونطور که تایپِ Identity
کمک میکرد اساسِ Functor
، Applicative
، و Monad
رو نشون بدیم، تایپِ IdentityT
هم کمک به درکِ موند ترانسفورمرها میکنه. استفاده از این تایپی که هیچ کارِ بخصوصی انجام نمیده، اجازه میده به تایپها و مفاهیمِ مهمِ موند ترانسفورمرها تمرکز کنیم. چیزهایی که اینجا میبینیم برای بقیهی ترانسفورمرها هم صادقاند، اما تایپهایی مثلِ Maybe
و لیست، حالتهای دیگهای (حالتِ شکست، و لیستِ خالی) هم دارند که یه ذره پیچیدهشون میکنن.
اول تایپِ Identity
که تا اینجا دیدین رو با این IdentityT
ِ جدید مقایسه میکنیم:
-- میتونه یه a قدیمی. تایپ Identity
-- تایپِ با ساختار باشه، اما لازم
-- هم نمیتونه چیزی Identity نیست و
-- .ازش بدونه
newtype Identity a =
Identity {runIdentity :: a }
deriving (Eq, Show)
-- ،تنها نقشی که موند ترانسفورمر همانی
-- بازی میکنه identity monad transformer یا
-- .تعیینِ وجودِ ساختار اضافهست
newtype IdentityT f a =
IdentityT { runIdentityT :: f a }
deriving (Eq, Show)
چیزی که اینجا تغییر کرد، اضافه شدنِ یه آرگومانِ تایپیِ دیگهست.
حالا نمونه ِ Functor
برای Identity
و IdentityT
:
instance Functor Identity where
fmap f (Identity a) = Identity (f a)
instance (Functor m)
=> Functor (IdentityT m) where
fmap f (IdentityT fa) = IdentityT (fmap f fa)
این نمونه ِ IdentityT
به نمونه ِ Functor
ِ نوعداده ِ One
که بالاتر دیدیم شباهت داره. اون آرگومانِ fa
، مقدارِ داخلِ IdentityT
هست که یه ساختار (غیرقابل دسترس) دورش پوشونده شده. تنها چیزی که از اون ساختار ِ اضافه دورِ a
میدونیم اینه که یه Functor
ِه.
برای هردوشون نمونه ِ Applicative
هم میخوایم:
instance Applicative Identity where
pure = Identity
(Identity f) <*> (Identity f) =
Identity (f a)
instance (Applicative m)
=> Applicative (IdentityT m) where
pure x = IdentityT (pure x)
(IdentityT fab) <*> (IdentityT fa) =
IdentityT (fab <*> fa)
باز هم نمونه ِ Identity
باید آشنا باشه. در نمونه ِ IdentityT
، متغیرِ fab
، آرگومانِ اول برای (<*>)
با تایپِ f (a -> b)
هست و به نمونه ِ Applicative
برای m
اتکا داره. پس این نمونه، اعمال ِ اپلیکتیوی در حضورِ ساختار ِ IdentityT
رو تعریف کرده.
میرسیم به نمونههای Monad
:
instance Monad Identity where
return = pure
(Identity a) >>= f = f a
instance (Monad m)
=> Monad (IdentityT m) where
return = pure
(IdentityT ma) >>= f =
IdentityT $ ma >>= runIdentity . f
نمونه ِ Monad
ممکنه یه کم پیچیده باشه، ما هم خوردِش میکنیم. به خاطر داشته باشین که Monad
جاییه که واقعاً باید از اطلاعاتِ تایپِ معین ِ IdentityT
استفاده کنیم تا بتونیم تایپها رو جور کنیم.
تشریحِ بایند
با نگاهی دقیقتر به نمونهای که بالا نوشتیم شروع میکنیم:
instance (Monad m)
=> Monad (IdentityT m) where
return = pure
(IdentityT ma) >>= f =
-- [ 1 ] [2] [3]
IdentityT $ ma
-- [8] [4]
>>= runIdentity . f
-- [5] [7] [6]
۱.
اول با دادهساز ِ IdentityT
روی مقدار با تایپِ m a
، از تایپِ IdentityT m a
تطبیق الگو میکنیم (بازش میکنیم). این کار در واقع چنین تایپی داره: IdentityT m a -> m a
و تایپِ ma
هم m a
هست. این حروفی که استفاده کردیم فقط برای یادآوریِ بهترن، به خودیِ خود مفهومی ندارن.
۲.
تایپِ بایندی که داریم تعریف میکنیم، از این قراره:
(>>=) :: IdentityT m a
-> (a -> IdentityT m b)
-> IdentityT m b
این نمونه ایه که داریم تعریف میکنیم.
۳.
تابعی که از روی IdentityT m a
بایند میکنیم، تایپش اینه:
(a -> IdentityT m b)
۴.
این ma
همون مقداریه که از دادهساز ِ IdentityT
درآوردیم و تایپش m a
هست. این m a
که از بافت ِ IdentityT
ش خارج شده، آرگومانِ اول به بایند میشه.
۵.
این یه بایند ِ دیگهست! بایند ِ اول، بایند ایه که داریم تعریف میکنیم؛ این بایند، خودِ اون تعریفه. اینجا داریم از اون محدودیت ِ Monad
ای که در شروعِ تعریفِ نمونه ِ Monad
با Monad m =>
درخواست کردیم استفاده میکنیم. تایپش اینطوریه:
(>>=) :: m a -> (a -> m b) -> m b
این تایپِ مرتبط با m
ایه که در تایپِ IdentityT m a
هست، نه تایپِ کلی برای کلاسِ Monad
. به کلام دیگه، حالا که دیگه IdentityT
رو باز کردیم و به نوعی از سرِ راه بَرش داشتیم، این بایند، بایند ِ تایپِ m
در تایپِ IdentityT m
میشه. هنوز نمیدونیم که اون چه Monad
ایه، نیازی هم نیست که بدونیم؛ از اونجایی که محدودیت تایپکلاسی ِ Monad
روی اون متغیر هست، میدونیم که یه نمونه ِ Monad
براش تعریف شده، و در نتیجه این بایند ِ دوم، بایند ایه که برای اون تایپ تعریف شده. تنها کاری که اینجا انجام میدیم اینه که تعریف کنیم چطور از اون بایند در حضورِ ساختار ِ اضافیِ IdentityT
استفاده بشه.
۶.
این f
میشه همون f
ای که یکی از آرگومانهای نمونه ِ Monad
بود، و تایپش اینه:
(a -> IdentityT m b)
۷.
به دلیل اینکه f
یه IdentityT m b
برمیگردونه، runIdentityT
هم لازم داریم، ولی تایپِ بایند ِ دوم (>>=
مرتبط با Monad m =>
) اینطوریه: m a -> (a -> m b)
. یعنی بدونِ runIdentityT
، نهایتاً تلاش میکنه m (IdentityT m b)
رو متحد کنه، که شدنی نیست چون m
و IdentityT m
یک تایپ نیستن. با runIdentityT
مقدار رو دَر میاریم و این کار تایپِ IdentityT m b -> m b
داره. و اینجا ترکیب ِ runIdentityT . f
تایپِش a -> m b
هست. با استفاده از undefined
، خودتون میتونین در GHCi ببینین:
Prelude> :{
*Main| let f :: (a -> IdentityT m b)
*Main| f = undefined
*Main| :}
Prelude> :t f
f :: (a -> IdentityT m b)
Prelude> :t runIdentityT
runIdentity :: IdentityT f a -> f a
Prelude> :t (runIdentityT . f)
(runIdentityT . f) :: a1 -> f a
قبوله، متغیرها اسمهاشون فرق دارن، اما واضحه که a1 -> f a
و a -> m a
یکساناند.
۸.
برای ارضای بایند ِ بیرونی که برای Monad
ِ تایپِ IdentityT m
تعریف میکنیم، و انتظارِ جواب نهایی با تایپِ IdentityT m b
داره، باید m b
که حاصل از بیانیهی ma >>= runIdentityT . f
هست رو دوباره تو IdentityT
بستهبندی کنیم. دقت کنین:
Prelude> :t IdentityT
IdentityT :: f a -> IdentityT f a
Prelude> :t runIdentityT
runIdentityT :: IdentityT f a -> f a
حالا یه بایندی داریم که میشه با ترکیب ِ IdentityT
و یه Monad
ِ دیگه ازش استفاده کرد – در این مثال با یه لیست:
Prelude> let sumR = return . (+1)
Prelude> IdentityT [1, 2, 3] >>= sumR
IdentityT {runIdentityT = [2,3,4]}
تعریفِ بایند، قدم به قدم
حالا برمیگردیم و اون تعریف رو مجدداً قدم به قدم دوره میکنیم. هدف اینه که هرچی ابهام در کارهایی که انجام دادیم هست رو برطرف کنیم تا بتونین برای هر موند ترانسفورمری که لازم دارین، خودتون نمونهش رو بنویسین. این بار با توسعه ِ InstanceSigs
مینویسیم تا بتونیم تایپها رو ببینیم:
{-# LANGUAGE InstanceSigs #-}
instance (Monad m)
=> Monad (IdentityT m) where
return = pure
(>>=) :: IdentityT m a
-> (a -> IdentityT m b)
-> IdentityT m b
(IdentityT ma) >>= f =
undefined
فعلاً مقدار نهایی رو undefined
نگه میداریم تا بتونیم با استفاده از انقیادهای let
و تناقض، تایپِ تعاریفی که امتحان میکنیم رو ببینیم. در واقع با تهی (undefined
) چیزهایی که مجبور به تأمینشون هستیم رو داریم موکول میکنیم به وقتی که آمادگیِ بیشتری داریم. اول یه انقیاد ِ let
بذاریم تا بارگذاریش رو ببینیم، حتی اگه کار نکنه:
(>>=) :: IdentityT m a
-> (a -> IdentityT m b)
-> IdentityT m b
(IdentityT ma) >>= f =
let aimb = ma >>= f
in undefined
برای یادآوری از اسم aimb
استفاده کردیم تا بدونیم از چه بخشهایی تشکیل شده.
خب یه خطا میگیریم:
Couldn't match type ‘m’ with ‘IdentityT m’
این خطای تایپ، کمک زیادی نمیکنه؛ تشخیص اشکال کار باهاش آسون نیست. یه کم دستکاری کنیم تا خطا ِ بهتری بگیریم.
اول یه کاری که میدونیم جواب میده رو انجام میدیم: از fmap
استفاده میکنیم که تایپچک میشه (اما جوابِ (>>=)
رو نمیده)، پس برای اینکه بتونیم از پیغامِ کامپایلر کمک بگیریم، باید کاری کنیم خطا بده. اینجا با اعلام ِ یه تایپِ تماماً پلیمورفیک برای aimb
خطا رو اجبار میکنیم:
(>>=) :: IdentityT m a
-> (a -> IdentityT m b)
-> IdentityT m b
(IdentityT ma) >>= f =
let aimb :: a
aimb = fmap f ma
in undefined
تایپی که برای aimb
اعلام کردیم غیرممکنه؛ گفتیم میتونه هر تایپی باشه، در صورتی که اینطور نیست. تهی تنها چیزیه که میتونه اون تایپ رو داشته باشه چون تهی جزءِ همهی تایپها هست.
GHC خودش میگه aimb
چیه:
Couldn' match expected type ‘a1’
with actual type ‘m (IdentityT m b)’
با این تعریفی که کردیم، تایپِ aimb
میشه m (IdentityT m b)
. حالا ایرادِ کار معلوم شد: بین دوتا m
که میخوایم متحد کنیم یه لایهی IdentityT
مزاحمه.
(>>=) :: IdentityT m a
-> (a -> IdentityT m b)
-> IdentityT m b
تابعی که تو این تعریف از روی ma
(حاصل از تطبیق الگو روی IdentityT
) لیفت کردیم اینه:
(a -> IdentityT m b)
یعنی تابع رو روی تایپِ
m a
نگاشت کردیم و تایپِ
m (IdentityT m b)
گرفتیم. تابعِ (>>=)
بعد از لیفت کردنِ تابع، دوتا ساختار ِ یکسان رو تلفیق میکنه (یادتون باشه که بایند، ترکیب ِ fmap
و join
ِه). یعنی اگه تایپِ حاصل از نگاشتِ f
روی ma
میشد m (m b)
، اون موقع join
کار میکرد. اما با این وضع، باید یه راهی پیدا کنیم تا اون دوتا m
رو بیاریم کنارِ هم تا دیگه لایهی IdentityT
بینشون نباشه.
برای اینکه پیشرفت مرحلهایمون بهتر دیده بشه، بجای (>>=)
از fmap
و join
استفاده میکنیم. چطور از اون IdentityT
بین دوتا ساختار ِ m
خلاص شیم؟ خب، میدونیم m
یه Monad
ِه، یعنی یه Functor
هم هست. پس با استفاده از runIdentityT
میشه اون IdentityT
وسط دسته ِ تایپها رو حذف کرد:
-- m (IdentitytT m b) تغییر
-- m (m b) به
-- :توجه کنین که
runIdentityT :: IdentityT f a -> f a
fmap runIdentityT :: Functor f
=> f (IdentityT f1 a) -> f (f1 a)
(>>=) :: IdentityT m a
-> (a -> IdentityT m b)
-> IdentityT m b
(IdentityT ma) >>= f =
let aimb :: a
aimb = fmap runIdenitytT (fmap f ma)
in undefined
خطای تایپی که از این کُد میگیریم امیدوارکنندهست:
Couldn't match expected type ‘a1’
with actual type ‘m (m b)’
پس رسیدیم به تایپِ m (m b)
، دیگه میدونیم از اینجا چطور به چیزی که میخوایم برسیم. a1
همون تایپیه که برای aimb
اعلام کردیم، اما میگه تایپِ واقعی مون اونی که اعلام کردیم نیست، بلکه m (m b)
ِه. پس تایپِ واقعی رو کشف کردیم، حالا میتونیم درستش کنیم.
برای تلفیق ِ m
های تودرتو، از join
از ماژول ِ Control.Monad
استفاده میکنیم:
(>>=) :: IdentityT m a
-> (a -> IdentityT m b)
-> IdentityT m b
(IdentityT ma) >>= f =
let aimb :: a
aimb =
join (fmap runIdenitytT (fmap f ma))
in undefined
حالا اگه بارگذاریش کنیم، کامپایلر تایپی رو نشون میده که میخوایم:
Couldn' match expected type ‘a1’
with actual type ‘m b’
قبل از تمیزکاری هم میشه سریع فهمید این درسته:
(>>=) :: IdentityT m a
-> (a -> IdentityT m b)
-> IdentityT m b
(IdentityT ma) >>= f =
let aimb =
join (fmap runIdenitytT (fmap f ma))
in aimb
تایپِ aimb
رو حذف کردیم، اون in undefined
هم تغییر دادیم. دیگه میدونیم که تایپِ واقعی ِ aimb
شده m b
، پس هنوز کار نمیکنه. چرا؟ اگه به خطای تایپ ِ جدیدمون نگاه کنیم:
Couldn't match type ‘m’ with ‘IdentityT m’
تایپِ نتیجهی نهاییِ (>>=)
که داریم تعریف میکنیم، باید IdentityT m b
باشه، پس تایپِ aimb
هنوز باهاش جور نشده. برای تایپچک باید m b
رو ببریم زیرِ IdentityT
:
IdentityT :: f a -> IdentityT f a
instance (Monad m)
=> Monad (IdentityT m) where
return = pure
(>>=) :: IdentityT m a
-> (a -> IdentityT m b)
-> IdentityT m b
(IdentityT ma) >>= f =
let aimb =
join (fmap runIdenitytT (fmap f ma))
in IdentityT aimb
این دیگه کامپایل میشه. m b
رو که گذاشتیم زیر تایپِ IdentityT
، کار میکنه.
تمیزکاری
حالا که رسیدیم به یه چیزی که کار میکنه، بریم سراغ تمیزکاری. میخوایم تعریفی که برای (>>=)
نوشتیم رو ارتقا بدیم. معمولاً بجای اینکه سعی کنین یه دفعه همهش رو بازنویسی کنین، اگه قدمبهقدم پیش برین نتیجهی بهتری میگیرین. این خط رو چطور میشه ارتقا بدیم؟
IdentityT $
join (fmap runIdenitytT (fmap f ma))
خوب یکی از قانونهای Functor
مربوط به دوبار fmap
کردن بود:
-- Functor قانون
fmap (f . g) = fmap f . fmap g
درسته! پس میشه اون خطِ بالا رو با این کُد عوض کنیم و جواب تغییری نکنه:
IdentityT $
join (fmap (runIdenitytT . f) ma)
حالا یه کم مشکوک شد... داریم نتیجهی حاصل از fmap
کردنِ دوتا تابعی که ترکیب کردیم رو join
میکنیم. ترکیب ِ join
با fmap
همون (>>=)
نبود؟
x >>= f = join (fmap f x)
پس تعریفِ نمونه ِ Monad
ِمون رو میشه به زیر تغییر بدیم:
instance (Monad m)
=> Monad (IdentityT m) where
return = pure
(>>=) :: IdentityT m a
-> (a -> IdentityT m b)
-> IdentityT m b
(IdentityT ma) >>= f =
IdentityT $ ma >>= runIdentityT . f
این کار میکنه! یه نوعساز داریم (IdentityT
) که یه موند به عنوانِ آرگومان میگیره و یه موند در جواب برمیگردونه.
این تعریف رو میشه به شکلهای دیگه هم نوشت. برای مثال در کتابخونه transformers
اینطوری نوشته شده:
m >>= k =
IdentityT $ runIdentityT . k
=<< runIdentityT m
یه کم وقت بذارین و خودتون تعادلِ بین این دوتا تعریف رو کامل درک کنین.
اساسِ موند ترانسفورمرها
شاید به نظر نرسه، اما موند ترانسفورمر ِ IdentityT
، اساسِ کلیِ ترانسفورمرها رو نشون میده. دلیلی که همهی این کارها رو کردیم این بود که نمیتونستیم تضمین کنیم از ترکیبِ دوتا تایپ به یه نمونه ِ موند میرسیم. یعنی فهمیدیم که داشتن نمونههای Functor
، Applicative
، و Monad
برای رسیدن به یه نمونه ِ Monad
ِ جدید کافی نیست. خب کدوم بخش از کُدِ زیر تازه بود؟
(>>=) :: IdentityT m a
-> (a -> IdentityT m b)
-> IdentityT m b
(IdentityT ma) >>= f =
IdentityT $ ma >>= runIdentityT . f
تطبیق الگو روی IdentityT
که نبود؛ Functor
برای اون کافیه:
-- این نه
(IdentityT ma) ...
قابلیتِ بایند ِ توابع روی مقدارِ ma
با تایپِ m a
هم نبود؛ اون رو از محدودیت ِ Monad
برای m
هم داشتیم:
-- این هم نه
... ma >>= ...
برای استفاده از runIdentityT
باید ماهیتِ یکی از تایپها رو به صورتِ معیّن میدونستیم (این تابع در واقع fmap
کردنِ یه فولد از ساختار ِ IdentityT
ِه). بعد از اعمالِ runIdentityT
هم تونستیم مقدارِ نهایی رو دوباره بذاریم داخلِ IdentityT
:
-- برای این کار باید ماهیت
-- رو مشخصاً میدونستیم. IdentityT
IdentityT .. runIdentityT ...
همونطور که خاطرتون هست، تا قبل از استفاده از runIdentityT
نمیتونستیم تایپها رو با هم جور کنیم، چون یه IdentityT
بینِ دوتا m
گیر کرده بود. ثابت میشه که فقط با Functor
، Applicative
، و Monad
نمیشه اون مشکل رو حل کرد. این یه مثال از اینه که چرا نمیشه برای تایپِ Compose
نمونه ِ Monad
نوشت، با این حال میشه یه ترانسفورمر مثلِ IdentityT
درست کرد و از اطلاعاتی که مختصِ تایپِش هست بهره برد و با هر تایپ دیگهای که یه نمونه ِ Monad
داره ترکیبش کرد. در کل، برای اینکه بتونیم تایپها رو جور کنیم، باید یه راهی برای فولد و بازسازیِ ساختاری که اطلاعاتِ مشخص ازش داریم داشته باشیم.