什么是“免费Monad +口译员”模式?


95

我见过有人在谈论带有Interpreter的Free Monad,尤其是在数据访问方面。这是什么模式?我什么时候可以使用它?它是如何工作的,我将如何实施?

据我所知(从职位如),它是关于从数据访问分离模式。它与众所周知的存储库模式有何不同?他们似乎具有相同的动机。

Answers:


138

实际上,实际模式远不只是数据访问更通用。这是一种创建给您AST的特定于域的语言的轻巧方式,然后让一个或多个解释器根据您的喜好“执行” AST。

免费的monad部分只是获得AST的便捷方法,您可以使用Haskell的标准monad工具(例如do-notation)进行汇编,而无需编写大量的自定义代码。这也确保了DSL是可组合的:您可以将DSL分为几部分进行定义,然后以结构化的方式将各部分组合在一起,从而充分利用Haskell的正常抽象(如函数)。

使用免费的monad可以为您提供可组合DSL 的结构。您要做的就是指定零件。您只需编写一个数据类型,其中包含DSL中的所有操作。这些动作可以做任何事情,而不仅仅是数据访问。但是,如果将所有数据访问都指定为操作,则将获得AST,该AST指定对数据存储区的所有查询和命令。然后,您可以按自己喜欢的方式进行解释:对实时数据库运行它,对模拟运行它,仅记录调试命令,甚至尝试优化查询。

让我们看一个非常简单的示例,例如键值存储。现在,我们只将键和值都视为字符串,但是您可以花一点精力来添加类型。

data DSL next = Get String (String -> next)
              | Set String String next
              | End

next参数使我们可以组合动作。我们可以使用它来编写一个程序,该程序获取“ foo”并使用该值设置“ bar”:

p1 = Get "foo" $ \ foo -> Set "bar" foo End

不幸的是,这对于有意义的DSL来说还不够。由于我们用于next合成,因此的类型与p1程序的长度相同(即3个命令):

p1 :: DSL (DSL (DSL next))

在这个特定的示例中,这样使用next似乎有些奇怪,但是如果我们希望我们的动作具有不同的类型变量,则这一点很重要。例如,我们可能需要输入type getset

请注意next,每个操作的字段都不同。这暗示我们可以用它来做DSL一个函子:

instance Functor DSL where
  fmap f (Get name k)          = Get name (f . k)
  fmap f (Set name value next) = Set name value (f next)
  fmap f End                   = End

实际上,这是使其成为Functor 的唯一有效方法,因此我们可以deriving通过启用DeriveFunctor扩展来自动创建实例。

下一步是Free类型本身。这就是我们用来表示AST 结构的基础,该结构建立在该DSL类型之上。您可以将其视为类型级别的列表,其中“ cons”只是嵌套如下函子DSL

-- compare the two types:
data Free f a = Free (f (Free f a)) | Return a
data List a   = Cons a (List a)     | Nil

因此,我们可以Free DSL next为不同大小的程序提供相同的类型:

p2 = Free (Get "foo" $ \ foo -> Free (Set "bar" foo (Free End)))

哪个类型好得多:

p2 :: Free DSL a

但是,实际的表达式及其所有构造函数仍然很难使用!这就是monad的一部分。正如名称“ free monad”所暗示的那样,Free它是monad-只要f(在这种情况下DSL)是仿函数:

instance Functor f => Monad (Free f) where
  return         = Return
  Free a >>= f   = Free (fmap (>>= f) a)
  Return a >>= f = f a

现在我们到了一个地方:我们可以使用do符号使DSL表达式更好。唯一的问题是要投入什么next?好吧,我们的想法是使用Free结构进行合成,因此我们将为Return每个下一个字段添加内容,并使用do-notation做所有的工作:

p3 = do foo <- Free (Get "foo" Return)
        Free (Set "bar" foo (Return ()))
        Free End

这样比较好,但是还是有点尴尬。我们FreeReturn所有的地方。令人高兴的是,有我们可以利用这样一个规律:我们的“升降机”一个DSL行动统一到方式Free总是相同的,我们把它包装Free和应用Returnnext

liftFree :: Functor f => f a -> Free f a
liftFree action = Free (fmap Return action)

现在,使用此代码,我们可以为每个命令编写漂亮的版本并拥有完整的DSL:

get key       = liftFree (Get key id)
set key value = liftFree (Set key value ())
end           = liftFree End

使用此代码,我们可以编写程序:

p4 :: Free DSL a
p4 = do foo <- get "foo"
        set "bar" foo
        end

巧妙的技巧是,尽管p4看起来像一个命令式程序,它实际上是一个具有以下价值的表达式:

Free (Get "foo" $ \ foo -> Free (Set "bar" foo (Free End)))

因此,该模式的免费monad部分已使我们获得了DSL,该DSL可以生成语法优美的语法树。我们也可以通过不使用来编写可组合的子树End。例如,我们可以follow让它获取一个键,获取它的值,然后将其用作键本身:

follow :: String -> Free DSL String
follow key = do key' <- get key
                get key'

现在follow可以像get或那样在我们的程序中使用set

p5 = do foo <- follow "foo"
        set "bar" foo
        end

因此,我们也为DSL获得了一些不错的组合和抽象。

现在我们有一棵树,我们进入模式的下半部分:解释器。我们可以通过只对它进行模式匹配来解释它,但是我们喜欢它。这将使我们能够针对中的实际数据存储IO以及其他内容编写代码。这是一个假设的数据存储示例:

runIO :: Free DSL a -> IO ()
runIO (Free (Get key k)) =
  do res <- getKey key
     runIO $ k res
runIO (Free (Set key value next)) =
  do setKey key value
     runIO next
runIO (Free End) = close
runIO (Return _) = return ()

这将很乐意评估任何DSL片段,即使不是以结尾的片段end。幸运的是,我们可以end通过将输入类型签名设置为来创建该函数的“安全”版本,使其仅接受关闭的程序(forall a. Free DSL a) -> IO ()。虽然旧签名接受一个Free DSL a任何 a(如Free DSL StringFree DSL Int等),该版本只接受Free DSL a对于作品每个可能的a哪位,我们只能创造end。这保证了我们在完成操作后不会忘记关闭连接。

safeRunIO :: (forall a. Free DSL a) -> IO ()
safeRunIO = runIO

(我们不能仅仅从提供runIO此类型开始,因为它不能在我们的递归调用中正常工作。但是,我们可以将in 的定义runIO移入一个where块中safeRunIO并获得相同的效果,而无需公开该函数的两个版本。)

运行代码IO不是我们唯一能做的。为了进行测试,我们可能想针对纯运行它State Map。编写该代码是一个很好的练习。

这就是免费的monad +解释器模式。我们利用免费的monad结构来制作DSL,以完成所有工作。我们可以在DSL中使用do-notation和标准monad功能。然后,要实际使用它,我们必须以某种方式进行解释。由于树最终只是一个数据结构,因此我们可以根据不同的目的对其进行解释。

当我们使用它来管理对外部数据存储的访问时,它的确类似于存储库模式。它介于我们的数据存储和代码之间,将两者分开。但是,在某些方面,它更具体:“存储库”始终是带有显式AST的DSL,然后我们可以根据需要使用它。

但是,模式本身比这更笼统。它可以用于很多事情,这些事情不一定涉及外部数据库或存储。无论您希望对DSL的效果或多个目标进行精细控制,这都是有意义的。


6
为什么称其为“免费”单子?
本杰明·霍奇森

14
“免费”的名称来自类别理论:ncatlab.org/nlab/show/free+object,但这有点意味着它是“最小” monad -仅对其有效的操作是monad操作,因为它具有“忘记了”这是其他结构。
博伊德·史密斯·史密斯

3
@BenjaminHodgson:Boyd是完全正确的。除非您只是好奇,否则我不会担心太多。丹·皮波尼(Dan Piponi)谈到了“免费”在BayHac中的含义,这值得一看。请尝试跟随他的幻灯片,因为视频中的视觉效果完全没有用。
Tikhon Jelvis,2014年

3
一句话:“免费的monad部分只是 [我的重点],是一种便捷的方式来获取AST,您可以使用Haskell的标准monad工具(例如do-notation)进行组装,而无需编写大量的自定义代码。” (不仅仅是我知道)(我敢肯定,您知道)。Free monad也是规范化的程序表示形式,它使解释器无法区分- do表示法不同但实际上“均值相同”的程序。
sacundim 2014年

5
@sacundim:您能详细说明一下吗?尤其是句子“ Free monads也是规范化的程序表示形式,这使解释程序无法区分do-notation不同但实际上”平均”的程序。
乔治

15

一个免费的monad基本上是一个monad,它以与计算相同的“形状”构建数据结构,而不是执行任何更复杂的操作。(有一些示例可以在网上找到。)然后,此数据结构传递给一段使用它并执行操作的代码。*我并不完全熟悉存储库模式,但是从我的阅读看来成为更高级别的体系结构,可以使用免费的monad +解释器来实现它。另一方面,免费的monad +解释器也可以用于实现完全不同的事物,例如解析器。

*值得注意的是,这种模式不是monad独有的,实际上可以使用免费的应用程序或免费的箭头来产生更有效的代码。(解析器是另一个例子。


抱歉,我应该对存储库更加清楚。(我忘了并不是每个人都有业务系统/ OO / DDD背景!)存储库基本上封装了数据访问并为您重新绑定域对象。它通常与依赖倒置一起使用-您可以“插入”回购的不同实现(用于测试,或者在需要切换数据库或ORM时有用)。域代码只是repository.Get()在不知道从何处获取域对象的情况下进行调用。
本杰明·霍奇森
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.