39

我正在做一个项目,其中包括一个数据库访问层。很正常,真的。在之前的一个项目中,一位合作者鼓励我将 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)

然后是口译员。然后是实现每个不同查询的原始函数。基本上,我觉得我有大量的胶水代码。


在我当前的项目中(在一个完全不同的领域),我为我的数据库使用了一个非常普通的 monad:

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 粘合的所有函数。总的来说,这比免费的 monad 系统感觉要好得多,因为我并没有编写大量样板代码,只是为了换掉我的解释器。

或者...

我真的通过 Free Monad + Interpreter 模式获得了其他东西吗?如果是这样,是什么?

4

1 回答 1

41

正如评论中提到的,经常需要在代码和数据库实现之间进行一些抽象。通过为 DB Monad 定义一个类,您可以获得与免费 monad 相同的抽象(我在这里采取了一些自由):

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

如果您的代码是针对MonadImageDB m =>而不是紧密耦合到编写的DBM,您将能够在不修改代码的情况下换出数据库和错误处理。

你为什么要使用免费的呢?因为它“尽可能地释放解释器”,这意味着解释器只致力于提供一个 monad,而不是其他任何东西。这意味着您尽可能不受约束地编写 monad 实例来配合您的代码。请注意,对于免费的 monad,您无需为 编写自己的实例Monad而是免费获得它。你会写类似的东西

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

派生,并从现有实例中Functor DBActionF获取 monad 实例for 。Free DBActionFFunctor f => Monad (Free f)

对于您的示例,它应该是:

data ImageActionF next =
      IndexImage  (ImageId, UTCTime) Exif Thumbnail (SaveResult   -> next)
    | RemoveImage ImageId                           (DeleteResult -> next)

您还可以获得类型类的“尽可能多地释放解释器”的属性。m如果您对类型类没有其他约束MonadImageDB,并且所有MonadImageDB的方法都可以是 a 的构造函数Functor,那么您将获得相同的属性。你可以通过实现来看到这一点instance MonadImageDB (Free ImageActionF)

如果您打算将代码与与其他 monad 的交互混合在一起,您可以免费获得 monad 转换器而不是 monad。

选择

你不必选择。您可以在表示之间来回转换。此示例说明如何对返回零、一个或两个结果的零、一个或两个参数的操作执行此操作。首先,一些样板文件

{-# 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)

从自由表示转换为类型类替换Purereturn、、等。Free>>=Addadd

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
于 2014-05-20T18:53:08.530 回答