Когда я захочу использовать шаблон 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
Другие вопросы по тегам