我什么时候想要使用免费的Monad + Interpreter模式? [英] When would I want to use a Free Monad + Interpreter pattern?

查看:135
本文介绍了我什么时候想要使用免费的Monad + Interpreter模式?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在研究一个项目,其中涉及数据库访问层。很正常,真的。在以前的项目中,合作者鼓励我将Free Monad概念用于数据库层,所以我这样做了。现在我正试图在我的新项目中决定我获得的收益。



在之前的项目中,我有一个看起来很像这样的API。 b
$ b

  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)
|返回一个

然后执行monad:

 实例Monad DBAction其中
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 (或者T err IO)a)
派生(Monad,MonadIO,MonadReader DB)

indexImage ::(ImageId,UTCTime) - > Exif - >缩略图 - > DBM SaveError()
removeImage :: DB - > ImageId - > DBM DeleteError()

等等。我认为,最终,我将拥有代表高级概念的公共功能,这些功能都运行在 DBM 上下文中,然后我将完成所有的转换执行SQL / Haskell粘合的函数。总的来说,感觉比自由monad系统好得多,因为我没有写出大量的样板代码,只能替换掉我的解释器。



或者...

我是否可以通过Free Monad + Interpreter模式获得其他功能?如果是这样,什么?

解决方案

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

  class(Monad m)=> MonadImageDB m其中
indexImage ::(ImageId,UTCTime) - > Exif - >缩略图 - > m SaveResult
removeImage :: ImageId - > m DeleteResult

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



你为什么要用free?因为它尽可能释放口译员,这意味着英译者只承诺提供一个monad,而没有别的。这意味着你尽可能不受限制地编写monad实例来处理你的代码。请注意,对于免费的monad,您不需要为 Monad 编写自己的实例,您可以免费获取。你可以写一些类似于

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

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



对于您的示例,它将会是:

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

您也可以获得属性尽可能地释放解释器尽可能为类型课程。如果您对 m 没有其他约束条件,那么 MonadImageDB 以及所有 MonadImageDB 的方法可以是 Functor 的构造函数,那么您将获得相同的属性。你可以通过实现实例MonadImageDB(Free ImageActionF)来实现这一点。



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

选择



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

  { - #LANGUAGE DeriveFunctor# - } 
{ - #LANGUAGE FlexibleInstances# - }

import Control.Monad.Free

我们有一个类型类 p>

  class Monad m => MonadAddDel m其中
add :: String - > m Int
del :: Int - > m()
set :: Int - >字符串 - > m()
add2 :: String - >字符串 - > m(Int,Int)
nop :: m()



  data AddDelF next 
=添加String(Int - > next)
| Del Int(下一个)
|设置Int字符串(下一个)
| Add2 String String(Int - > Int - > next)
| Nop(下一个)
派生(Functor)

从自由表示转换为类型类用,返回免费替换 Pure c>>> = 添加与添加等。

  run :: MonadAddDel m =>免费AddDelF a  - > m a 
run(Pure a)=返回
run(Free(Add x next))= add x>> = run。下一个
run(免费(Del id next))= del id>>运行下一个
run(Free(Set id x next))= set id x>>运行下一个
run(Free(Add2 x y next))= add2 x y>> = \ids - > run(next(fst id)(snd id))
run(Free(Nop next))= nop>>运行下一个

一个 MonadAddDel 使用 Pure 为构造函数的下一个参数构建函数。

 实例MonadAddDel(Free AddDelF)其中
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 - >纯(id1,id2)
nop =免费。 Nop $ Pure()

(这两个模式都有我们可以为生产代码提取的模式,一般来说,编写这些代码将会涉及到不同数量的输入和结果参数)

对类型类的编码只使用 MonadAddDel m => ; 约束,例如:

  example1 :: MonadAddDel m => m()
example1 = do
id< - addHi
del id
nop
(id3,id4)< - add2Hello世界
set id4再次

我懒得写另一个 MonadAddDel 除了我从免费获得的一个,也懒得做一个例子,除了使用 MonadAddDel 类型类。 / p>

如果您喜欢运行示例代码,则可以看到一次解释的示例(将类型表示转换为自由表示形式),然后再将自由表示形式转换回类型类表示再次。再次,我懒得写代码两次。

  debugInterpreter :: Free AddDelF a  - > IO a 
debugInterpreter = go 0
其中
go n(Pure a)=返回一个
go n(Free(Add x next))=
do
打印$添加++ x ++与id++ show n
go(n + 1)(next n)
go n(Free(Del id next))=
do
print $删除++ show id
转到下一个
转到n(免费(设置id x下一个))=
do
print $设置++ show id ++为++显示x
转到n下一个
转到n(免费(Add2 xy next))=
do
print $ 使用id++ show(n + 1)
go(n + 2)(next)添加++ x ++ n(n + 1))
go n(Free(Nop next))=
do
打印Nop
转到下一个

main =
do
debugInterpreter example1
debug口译员。运行$ example1


I'm working on a project that, amongst other things, involves a database access layer. Pretty normal, really. In a previous project, a collaborator encouraged me to use the Free Monads concept for a database layer and so I did. Now I'm trying to decide in my new project what I gain.

In the previous project, I had an API that looked rather like this.

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

etc. About twenty such public functions. To support them, I had the DBAction data structure:

data DBAction a =
      SaveDocument          RawDocument         (DBAction a)
    | GetDocuments          DocumentFilter      ([RawDocument] -> DBAction a)
    | GetDocumentStats                          ([(DocId, DocumentStats)] -> DBAction a)
    | Return a

And then a monad implementation:

instance Monad DBAction where
    return = Return
    SaveDocument doc k >>= f = SaveDocument doc (k >>= f)
    GetDocuments df k >>= f = GetDocuments df (k >=> f)

And then the interpreter. And then the primitive functions that implement each of the different queries. Basically, I'm feeling that I had a huge amount of glue code.


In my current project (in a totally different field), I have instead gone with a pretty ordinary monad for my database:

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 ()

And so on. I figure that, ultimately, I'll have the "public" functions that represent high level concepts all running in the DBM context, and then I'll have the whole slew of functions that do the SQL/Haskell glue. This is, overall, feeling much better than the free monad system because I'm not writing a huge amount of boilerplate code to gains me nothing but the ability to swap out my interpreter.

Or...

Do I actually gain something else with the Free Monad + Interpreter pattern? If so, what?

解决方案

As mentioned in the comments, it is frequently desirable to have some abstraction between code and database implementation. You can get much of the same abstraction as a free monad by defining a class for your DB Monad (I've taken a couple liberties here):

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

If your code is written against MonadImageDB m => instead of tightly coupled to DBM, you will be able to swap out the database and error handling without modifying your code.

Why would you use free instead? Because it "frees the interpreter as much as possible", meaning the intepreter is only committed to providing a monad, and nothing else. This means you are as unconstrained as possible writing monad instances to go with your code. Note that, for the free monad, you don't write your own instance for Monad, you get it for free. You'd write something like

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

derive Functor DBActionF, and get the monad instance for Free DBActionF from the existing instance for Functor f => Monad (Free f).

For your example, it'd instead be:

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

You can also get the property "frees the interpreter as much as possible" for the type class. If you have no other constraints on m than the type class, MonadImageDB, and all of MonadImageDB's methods could be constructors for a Functor, then you get the same property. You can see this by implementing instance MonadImageDB (Free ImageActionF).

If you are going to mix your code with interactions with some other monad, you can get a monad transformer from free instead of a monad.

Choosing

You don't have to choose. You can convert back and forth between the representations. This example shows how to do so for actions with zero, one, or two arguments returning zero, one, or two results. First, a bit of boilerplate

{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE FlexibleInstances #-}

import Control.Monad.Free

We have a type class

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 ()

and an equivalent functor representation

data AddDelF next
    = Add  String        (       Int -> next)
    | Del  Int           (              next)
    | Set  Int    String (              next)
    | Add2 String String (Int -> Int -> next)
    | Nop                (              next)
  deriving (Functor)

Converting from the free representation to the type class replaces Pure with return, Free with >>=, Add with add, etc.

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 instance for the representation builds functions for the next arguments of the constructors using 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 ()

(Both of these have patterns we could extract for production code, the hard part to writing these generically would be dealing with the varying number of input and result arguments)

Coding against the type class uses only the MonadAddDel m => constraint, for example:

example1 :: MonadAddDel m => m ()
example1 = do
    id <- add "Hi"
    del id
    nop
    (id3, id4) <- add2 "Hello" "World"
    set id4 "Again"

I was too lazy to write another instance for MonadAddDel besides the one I got from free, and too lazy to make an example besides by using the MonadAddDel type class.

If you like running example code, here's enough to see the example interpreted once (converting the type class representation to the free representation), and again after converting the free representation back to the type class representation again. Again, I'm too lazy to write the code twice.

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

这篇关于我什么时候想要使用免费的Monad + Interpreter模式?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆