Когда я хочу использовать шаблон Free Monad + Interpreter?

Я работаю над проектом, который, среди прочего, включает в себя уровень доступа к базе данных. Довольно нормально, правда. В предыдущем проекте сотрудник предложил мне использовать концепцию Free Monads для уровня базы данных, и поэтому я это сделал. Теперь я пытаюсь решить в своем новом проекте, что я получу.

В предыдущем проекте у меня был API, который выглядел примерно так.

saveDocument :: RawDocument -> DBAction ()
getDocuments :: DocumentFilter -> DBAction [RawDocument]
getDocumentStats :: DBAction [(DocId, DocumentStats)]

и т.д.. Около 20 таких общественных функций. Чтобы поддержать их, у меня была структура данных 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

Как упоминалось в комментариях, часто желательно иметь некоторую абстракцию между реализацией кода и базы данных. Вы можете получить большую часть той же абстракции, что и свободная монада, определив класс для вашего DB Monad (я взял пару привилегий здесь):

class (Monad m) => MonadImageDB m where
    indexImage  :: (ImageId, UTCTime) -> Exif -> Thumbnail -> m SaveResult
    removeImage :: ImageId                                 -> m DeleteResult

Если ваш код написан против MonadImageDB m => вместо плотно связанного с DBM, вы сможете поменять базу данных и обработку ошибок, не изменяя свой код.

Почему вы вместо этого используете бесплатный? Поскольку "освобождает интерпретатора как можно больше" , это означает, что intrereter предназначен только для обеспечения монады, и ничего больше. Это означает, что вы так же безукоризненны, как записываете экземпляры монады, чтобы пойти с вашим кодом. Обратите внимание, что для свободной монады вы не пишете свой собственный экземпляр для Monad, вы получите его бесплатно. Вы пишете что-то вроде

data DBActionF next =
      SaveDocument     RawDocument    (                            next)
    | GetDocuments     DocumentFilter ([RawDocument]            -> next)
    | GetDocumentStats                ([(DocId, DocumentStats)] -> next)

вывести Functor DBActionF и получить экземпляр monad для 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

A 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