۲۴ - ۸ترانسفورمرِ 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 داره ترکیبش کرد. در کل، برای اینکه بتونیم تایپها رو جور کنیم، باید یه راهی برای فولد و بازسازیِ ساختاری که اطلاعاتِ مشخص ازش داریم داشته باشیم.