Когда я захочу использовать шаблон Free Monad + Interpreter?
Я работаю над проектом, который, помимо прочего, включает в себя слой доступа к базе данных. Довольно нормально, правда. В предыдущем проекте соавтор рекомендовал мне использовать концепцию Free Monads для слоя базы данных, и я так и сделал. Теперь я пытаюсь решить, что получу в своем новом проекте.
В предыдущем проекте у меня был API, который выглядел примерно так.
saveDocument :: RawDocument -> DBAction ()
getDocuments :: DocumentFilter -> DBAction [RawDocument]
getDocumentStats :: DBAction [(DocId, DocumentStats)]
и т. д. Около двадцати таких публичных функций. Чтобы поддержать их, я имел DBAction
структура данных:
data DBAction a =
SaveDocument RawDocument (DBAction a)
| GetDocuments DocumentFilter ([RawDocument] -> DBAction a)
| GetDocumentStats ([(DocId, DocumentStats)] -> DBAction a)
| Return a
И затем реализация монады:
instance Monad DBAction where
return = Return
SaveDocument doc k >>= f = SaveDocument doc (k >>= f)
GetDocuments df k >>= f = GetDocuments df (k >=> f)
А потом переводчик. А затем примитивные функции, которые реализуют каждый из различных запросов. По сути, я чувствую, что у меня было огромное количество клеевого кода.
В моем текущем проекте (в совершенно другой области) я использовал обычную монаду для своей базы данных:
newtype DBM err a = DBM (ReaderT DB (EitherT err IO) a)
deriving (Monad, MonadIO, MonadReader DB)
indexImage :: (ImageId, UTCTime) -> Exif -> Thumbnail -> DBM SaveError ()
removeImage :: DB -> ImageId -> DBM DeleteError ()
И так далее. Я полагаю, что, в конечном счете, у меня будут "публичные" функции, которые представляют концепции высокого уровня, все работающие в DBM
контекст, и тогда у меня будет целый ряд функций, которые делают клей SQL/Haskell. В целом, это намного лучше, чем у бесплатной монады, потому что я не пишу огромное количество стандартного кода, чтобы получить ничего, кроме возможности поменять моего переводчика.
Или же...
Действительно ли я получаю что-то еще с паттерном Free Monad + Interpreter? Если да, то?
1 ответ
Как упоминалось в комментариях, часто желательно иметь некоторую абстракцию между реализацией кода и базы данных. Вы можете получить большую часть той же абстракции, что и свободная монада, определив класс для своей монады БД (я взял здесь несколько свобод):
class (Monad m) => MonadImageDB m where
indexImage :: (ImageId, UTCTime) -> Exif -> Thumbnail -> m SaveResult
removeImage :: ImageId -> m DeleteResult
Если ваш код написан против MonadImageDB m =>
вместо того, чтобы тесно связаны с DBM
, вы сможете поменять базу данных и обработать ошибки без изменения вашего кода.
Почему бы вместо этого использовать бесплатный? Потому что это "освобождает интерпретатора настолько, насколько это возможно", означая, что интерпретатор стремится только обеспечить монаду и ничего больше. Это означает, что вы максимально ограничены в написании экземпляров монады для своего кода. Обратите внимание, что для бесплатной монады вы не пишете свой собственный экземпляр для Monad
Вы получаете это бесплатно. Вы бы написали что-то вроде
data DBActionF next =
SaveDocument RawDocument ( next)
| GetDocuments DocumentFilter ([RawDocument] -> next)
| GetDocumentStats ([(DocId, DocumentStats)] -> next)
выводить Functor DBActionF
и получить экземпляр монады для Free DBActionF
из существующего экземпляра для Functor f => Monad (Free f)
,
Для вашего примера это будет:
data ImageActionF next =
IndexImage (ImageId, UTCTime) Exif Thumbnail (SaveResult -> next)
| RemoveImage ImageId (DeleteResult -> next)
Вы также можете получить свойство "максимально освобождает интерпретатор" для класса типа. Если у вас нет других ограничений на m
чем класс типа, MonadImageDB
и все MonadImageDB
методы могут быть конструкторами для Functor
, тогда вы получите ту же недвижимость. Вы можете увидеть это, реализовав instance MonadImageDB (Free ImageActionF)
,
Если вы собираетесь смешивать свой код с взаимодействиями с какой-либо другой монадой, вы можете получить монадный преобразователь из бесплатной, а не из монады.
Выбор
Вам не нужно выбирать. Вы можете конвертировать туда и обратно между представлениями. В этом примере показано, как это сделать для действий с нулем, одним или двумя аргументами, возвращающими ноль, один или два результата. Во-первых, немного шаблонного
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE FlexibleInstances #-}
import Control.Monad.Free
У нас есть тип класса
class Monad m => MonadAddDel m where
add :: String -> m Int
del :: Int -> m ()
set :: Int -> String -> m ()
add2 :: String -> String -> m (Int, Int)
nop :: m ()
и эквивалентное представление функтора
data AddDelF next
= Add String ( Int -> next)
| Del Int ( next)
| Set Int String ( next)
| Add2 String String (Int -> Int -> next)
| Nop ( next)
deriving (Functor)
Преобразование из свободного представления в класс типа заменяет Pure
с return
, Free
с >>=
, Add
с add
, так далее.
run :: MonadAddDel m => Free AddDelF a -> m a
run (Pure a) = return a
run (Free (Add x next)) = add x >>= run . next
run (Free (Del id next)) = del id >> run next
run (Free (Set id x next)) = set id x >> run next
run (Free (Add2 x y next)) = add2 x y >>= \ids -> run (next (fst ids) (snd ids))
run (Free (Nop next)) = nop >> run next
MonadAddDel
экземпляр для представления строит функции для next
аргументы конструкторов, использующих Pure
,
instance MonadAddDel (Free AddDelF) where
add x = Free . (Add x ) $ Pure
del id = Free . (Del id ) $ Pure ()
set id x = Free . (Set id x) $ Pure ()
add2 x y = Free . (Add2 x y) $ \id1 id2 -> Pure (id1, id2)
nop = Free . Nop $ Pure ()
(У обоих из них есть шаблоны, которые мы могли бы извлечь для производственного кода, трудная часть их общего написания была бы связана с различным количеством входных и конечных аргументов)
Кодирование против класса типа использует только MonadAddDel m =>
ограничение, например:
example1 :: MonadAddDel m => m ()
example1 = do
id <- add "Hi"
del id
nop
(id3, id4) <- add2 "Hello" "World"
set id4 "Again"
Мне было лень писать еще один экземпляр для MonadAddDel
кроме того, который я получил от бесплатных, и ленив, чтобы сделать пример, кроме того, используя MonadAddDel
тип класс.
Если вам нравится запускать пример кода, этого достаточно, чтобы увидеть пример, интерпретируемый один раз (преобразование представления класса типа в свободное представление), и снова после преобразования свободного представления обратно в представление класса типа снова. Опять же, мне лень писать код дважды.
debugInterpreter :: Free AddDelF a -> IO a
debugInterpreter = go 0
where
go n (Pure a) = return a
go n (Free (Add x next)) =
do
print $ "Adding " ++ x ++ " with id " ++ show n
go (n+1) (next n)
go n (Free (Del id next)) =
do
print $ "Deleting " ++ show id
go n next
go n (Free (Set id x next)) =
do
print $ "Setting " ++ show id ++ " to " ++ show x
go n next
go n (Free (Add2 x y next)) =
do
print $ "Adding " ++ x ++ " with id " ++ show n ++ " and " ++ y ++ " with id " ++ show (n+1)
go (n+2) (next n (n+1))
go n (Free (Nop next)) =
do
print "Nop"
go n next
main =
do
debugInterpreter example1
debugInterpreter . run $ example1