我正在做一个项目,其中包括一个数据库访问层。很正常,真的。在之前的一个项目中,一位合作者鼓励我在数据库层中使用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
然后是monad实现:
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粘合的所有函数。总的来说,这感觉比自由的monad系统好得多,因为我没有编写大量的样板代码,除了能够替换解释器之外什么都没有。
还是……
我实际上获得了其他与自由Monad +解释器模式?如果有,那是什么?
正如在注释中提到的,通常需要在代码和数据库实现之间有一些抽象。您可以通过为您的DB monad定义一个类(我在这里采取了一些自由)来获得与自由monad相同的抽象:
class (Monad m) => MonadImageDB m where
indexImage :: (ImageId, UTCTime) -> Exif -> Thumbnail -> m SaveResult
removeImage :: ImageId -> m DeleteResult
如果您的代码是针对MonadImageDB m =>
而不是与DBM
紧密耦合编写的,那么您将能够在不修改代码的情况下交换数据库和错误处理。
为什么要用free来代替呢?因为它"尽可能地释放解释器",这意味着解释器只致力于提供单子,而不是其他。这意味着您可以尽可能不受约束地编写与代码一起使用的单子实例。注意,对于免费monad,您不需要为Monad
编写自己的实例,您可以免费获得它。你可以这样写
data DBActionF next =
SaveDocument RawDocument ( next)
| GetDocuments DocumentFilter ([RawDocument] -> next)
| GetDocumentStats ([(DocId, DocumentStats)] -> next)
派生Functor DBActionF
,从已有的Functor f => Monad (Free f)
实例中得到Free DBActionF
的monad实例
对于你的例子,它应该是:
data ImageActionF next =
IndexImage (ImageId, UTCTime) Exif Thumbnail (SaveResult -> next)
| RemoveImage ImageId (DeleteResult -> next)
您还可以为类型类获得属性"尽可能地释放解释器"。如果在m
上除了类型类MonadImageDB
之外没有其他约束,并且MonadImageDB
的所有方法都可以是Functor
的构造函数,那么您将获得相同的属性。您可以通过实现instance MonadImageDB (Free ImageActionF)
来看到这一点。
如果你打算将你的代码与其他单子进行交互,你可以从free中获得一个单子转换器,而不是一个单子。
选择你不必选择。您可以在表示形式之间来回转换。这个示例展示了如何为带有0、1或2个参数的操作执行此操作,这些参数返回0、1或2个结果。首先,一些样板文件
{-# 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
实例使用Pure
为构造函数的next
参数构建函数。
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
编写一个实例,除了我从free中得到的那个,也懒得做一个例子,除了使用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