Answers:
实际上,实际模式远不只是数据访问更通用。这是一种创建给您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 get
和set
。
请注意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
这样比较好,但是还是有点尴尬。我们Free
和Return
所有的地方。令人高兴的是,有我们可以利用这样一个规律:我们的“升降机”一个DSL行动统一到方式Free
总是相同的,我们把它包装Free
和应用Return
为next
:
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 String
,Free 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的效果或多个目标进行精细控制,这都是有意义的。
do
表示法不同但实际上“均值相同”的程序。
一个免费的monad基本上是一个monad,它以与计算相同的“形状”构建数据结构,而不是执行任何更复杂的操作。(有一些示例可以在网上找到。)然后,此数据结构传递给一段使用它并执行操作的代码。*我并不完全熟悉存储库模式,但是从我的阅读看来成为更高级别的体系结构,可以使用免费的monad +解释器来实现它。另一方面,免费的monad +解释器也可以用于实现完全不同的事物,例如解析器。
*值得注意的是,这种模式不是monad独有的,实际上可以使用免费的应用程序或免费的箭头来产生更有效的代码。(解析器是另一个例子。)
repository.Get()
在不知道从何处获取域对象的情况下进行调用。