Пакет Control.Monad.Writer
не експортує конструктор даних Writer
. Я думаю, це було інакше, коли писали LYAH.
Використання класу типів MonadWriter у ghci
Натомість ви створюєте автори за допомогою writer
функції. Наприклад, на сеансі ghci я можу це зробити
ghci> import Control.Monad.Writer
ghci> let logNumber x = writer (x, ["Got number: " ++ show x])
Тепер logNumber
це функція, яка створює авторів. Я можу запитати його тип:
ghci> :t logNumber
logNumber :: (Show a, MonadWriter [String] m) => a -> m a
Що говорить мені, що виведений тип - це не функція, яка повертає певного автора, а все, що реалізує MonadWriter
клас типу. Тепер я можу ним користуватися:
ghci> let multWithLog = do { a <- logNumber 3; b <- logNumber 5; return (a*b) }
:: Writer [String] Int
(Введення фактично вводиться все в одному рядку). Тут я вказав тип multWithLog
бути Writer [String] Int
. Тепер я можу запустити його:
ghci> runWriter multWithLog
(15, ["Got number: 3","Got number: 5"])
І ви бачите, що ми реєструємо всі проміжні операції.
Чому код пишеться так?
Навіщо взагалі турбуватися про створення MonadWriter
класу типу? Причиною є монадні трансформатори. Як ви правильно зрозуміли, найпростіший спосіб реалізації Writer
- це обгортка нового типу поверх пари:
newtype Writer w a = Writer { runWriter :: (a,w) }
Ви можете оголосити для цього екземпляр монади, а потім написати функцію
tell :: Monoid w => w -> Writer w ()
який просто реєструє свої дані. Тепер припустимо, ви хочете монаду, яка має можливості реєстрації, але також робить щось інше - скажімо, вона також може читати з середовища. Ви б реалізували це як
type RW r w a = ReaderT r (Writer w a)
Тепер, оскільки запис знаходиться всередині ReaderT
монадного трансформатора, якщо ви хочете реєструвати вихідні дані, які ви не можете використовувати tell w
(оскільки це працює лише з нерозгорнутими записами), але ви повинні використовувати lift $ tell w
, що "піднімає" tell
функцію через, ReaderT
щоб вона могла отримати доступ до внутрішня письменницька монада. Якщо вам потрібні двошарові трансформатори (скажімо, ви також хочете додати обробку помилок), вам доведеться використовувати lift $ lift $ tell w
. Це швидко стає громіздким.
Натомість, визначивши клас типу, ми можемо зробити будь-яку обмотку трансформатора монади навколо записувача в екземпляр самого записувача. Наприклад,
instance (Monoid w, MonadWriter w m) => MonadWriter w (ReaderT r m)
тобто якщо w
є моноїдом і m
є a MonadWriter w
, то ReaderT r m
також є a MonadWriter w
. Це означає, що ми можемо використовувати tell
функцію безпосередньо на трансформованій монаді, не мучачись явним підйомом її через монадний трансформатор.