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

(从诸如此类的帖子中)我了解到,这是将模型与数据访问分开。它与众所周知的存储库模式有何不同?他们似乎具有相同的动机。

#1 楼

实际上,实际模式比仅数据访问要普遍得多。这是一种轻量级的方法,它可以创建一种特定于域的语言,为您提供AST,然后让一个或多个解释器根据您的喜好“执行” AST。

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

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

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

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


next参数使我们可以组合动作。我们可以使用它来编写一个程序,该程序获取“ foo”并设置具有该值的“ bar”:不幸的是,对于有意义的DSL来说,这还不够。由于我们使用next进行合成,因此p1的类型与程序的长度相同(即3个命令):

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


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

请注意,每个动作的next字段如何不同。这暗示我们可以使用它使DSL成为函子:

p1 :: DSL (DSL (DSL next))


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

下一步是Free类型本身。那就是我们用来表示我们的AST结构的基础,它建立在DSL类型的基础上。您可以将其视为类型级别的列表,其中“ cons”只是嵌套函子,如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


所以我们可以使用Free DSL next来提供程序不同大小的相同类型:

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


哪个类型更好:

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


实际的表达式及其所有构造函数仍然很难使用!这就是monad的一部分。正如名称“ free monad”所暗示的,Free是monad,只要f(在本例中为DSL)是一个函子即可:

p2 :: Free DSL a


现在到了:可以使用do表示法使DSL表达式更好。唯一的问题是next应该放入什么?好吧,我们的想法是使用Free结构进行合成,因此我们只需将Return放入每个下一个字段,然后使用do表示法完成所有检查:

>这更好,但是仍然有点尴尬。我们到处都有FreeReturn。令人高兴的是,有一种可以利用的模式:将DSL操作“提升”到Free的方式始终相同-我们将其包装在Free中,然后将Return应用于next

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


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

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


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

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


巧妙的窍门是,虽然p4看起来像一个命令式程序,但实际上它是一个具有值的表达式

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


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

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


现在follow可以在我们的程序中使用了像getset一样:

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


所以我们也为DSL获得了很好的合成和抽象。

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

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


这将很高兴地评估任何DSL片段,甚至没有以end结尾的片段。幸运的是,通过将输入类型签名设置为end,我们可以使该函数的“安全”版本仅接受以(forall a. Free DSL a) -> IO ()关闭的程序。虽然旧签名接受任何Free DSL aa(例如Free DSL StringFree DSL Int等),但此版本仅接受适用于所有可能的Free DSL aa-我们只能使用end来创建。这保证了我们完成操作后不会忘记关闭连接。

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


(我们不能仅仅通过给runIO这种类型来开始,因为它对于我们的递归调用将无法正常工作。但是,我们可以将runIO的定义移至where中的safeRunIO块中,并获得相同的效果而无需公开两个版本的函数。)

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

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

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

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

评论


为什么称其为“免费”单子?

–本杰明·霍奇森(Benjamin Hodgson)
2014年6月3日20:13在

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

–Boyd Stephen Smith Jr.
2014年6月3日在21:04



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

– Tikhon Jelvis
2014年6月3日21:56

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

–乔治
15年1月2日在22:17

只需重新阅读此答案,我就会有一个问题:Free会给您带来什么,而使用显式递归数据类型是您无法获得的?我的粗略理解是Free将类型的递归结构与类型本身分开(有点像Fix)。它使用Free DSL填充下一个类型参数。通过忽略该类型参数并自己编写Monad实例,我们错过了这项技术的优势吗?类似于数据DSL a =获取字符串(字符串->(DSL a))|设置字符串字符串(DSL a)|返回一个

–本杰明·霍奇森(Benjamin Hodgson)
2015年4月6日19:24



#2 楼

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

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

评论


抱歉,我应该对存储库更加清楚。 (我忘记了并不是每个人都有业务系统/ OO / DDD背景!)存储库基本上封装了数据访问并为您重新绑定域对象。它通常与依赖倒置一起使用-您可以“插入”回购的不同实现(用于测试,或者在需要切换数据库或ORM时有用)。域代码仅在不知道从何处获取域对象的情况下调用repository.Get()。

–本杰明·霍奇森(Benjamin Hodgson)
2014年6月2日在21:24