面向Haskell的策略元编程

2020-10-13 20:16:14

我们把源代码当作文本,这不是很奇怪吗?也就是说,我们有这个结构非常好、类型非常强的对象-抽象语法树-它在概念上存在于我们的头脑中,实际上存在于我们的编译器中,但由于某些原因,我们假装它只是一堆字节,并逐个字节地编辑它,而不是在语义上编辑它?

当你停下来想一想,这就像是有史以来最愚蠢的想法。作为作者,我们不认为我们的代码是字节,我们的解释器或编译器也不认为我们的代码是字节。但是取而代之的是,我们将语义理解放在我们的头脑中,将其序列化为字节,然后让编译器解析并重新发现我们头脑中的想法。真是白费力气。

取而代之的是,你可以为Haskell语言服务器使用令人难以置信的TOTBWF和我的新策略插件,它将自动智能地填补你的Haskell程序中的漏洞。

这篇博客文章描述了什么是策略引擎,以及你为什么想要一个,并且是一个很好的介绍,让你知道我们是如何在地狱里自动为你编写代码的。

想象一下,您正在与一名初级工程师进行结对编程。在导航员位置上,您将指导您的合作伙伴完成实现,引导他们完成高级笔划,并让他们实际完成编码部分。例如,为了实施Foldr::(a->;b->;b)->;b->;[a]->;b,您给合作伙伴的指导可能是:

这些说明无论如何都不是一个真正的程序,但您可以称它们为“程序草图”。编程的困难部分(思考做什么)在这里被捕获,但是实际操作留给读者作为练习。

战术引擎将类似上面的程序草图转换成实际的程序。策略将我们从文本编辑的专制和迂腐的细节中解放出来,使我们能够在更高的编程语义级别上工作。

策略对应于我们程序中的语义操作。就像文本编辑器中的原始命令(删除到行尾、插入圆括号等)如何组合以将一个程序的文本表示细化为另一个程序的文本表示一样,我们可以编写小策略来构建更大的想法。

数据ID a=ID a实例函数器ID,其中FMAP::(a->;b)->;ID a->;ID b FMAP=_。

我们可以一次一个想法地构建它,而不是一次编写所有这个函数。显然,第一步是绑定函数参数(Intros策略),这将产生精炼的表达式:

FMAP::(a->;b)->;ID a->;ID b FMAP=\fab ida->;_。

我们剩下一个新洞,但这个洞比旧洞“小”;我们改进了前一个洞,填补了它的一些结构。因此,我们的新洞的类型是ID b,现在范围中既有fab::a->;b,也有ida::id a。到目前为止,我们可以进一步简化对ida的模式匹配(destruct ida策略):

FMAP::(a-&>b)->;ID a->;ID b FMAP=\fab ida->;案例ida,ID a->;_。

得到的洞仍然具有类型ID b,但是我们现在已经在范围中引入了a::a。我们的下一步是构建ID值,这可以通过生成其数据构造函数(拆分策略)来实现:

FMAP::(a-&>b)->;ID a->;ID b FMAP=\fab ida->;案例ida,ID a->;ID_。

我们再次缩小了问题-现在我们的洞具有类型b。此时,我们可以调用fab函数来生成b(通过应用fab策略):

FMAP::(a-&>b)->;ID a->;ID b FMAP=\fab ida->;案例ida,ID a->;ID(Fab_)。

剩下的只是一个a类型的洞。幸运的是,我们的作用域中有一个::a,所以我们可以通过假设策略将其放入洞中:

FMAP::(a-&>b)->;ID a->;ID b FMAP=\fab ida->;案例ida,ID a->;ID(Fab A)。

就像这样,我们已经生成了我们想要的函数的实现!通过考虑我们希望在每个洞执行的语义操作(而不是如何操作文本字节),我们已经改变了考虑编辑的抽象级别。这一点的含义可能不会立即显现出来,所以让我们一起来探讨它们。

直到Alpha重命名,这种策略组合足以为任何对其类型变量不做任何“令人兴奋”的事情的SUM或PRODUCT类型派生FMAP。通过运行相同的步骤,我们可以为以下任何类型实现FMAP:

让我们通过快速遍历a的派生来使自己确信这一点。我们再次从FMAP及其类型开始:

FMAP::(a->;b)->;可能a->;可能b FMAP=\fab ma->;_。

FMAP::(a-&>b)->;可能a->;可能b FMAP=\fab ma->;大小写不存在->;_只是a->;_

在这里应用Split有点棘手;从技术上讲,它将迫使我们在一种奇怪的量子叠加中的每个空穴上既不尝试任何东西,也只尝试。让我们暂时忽略这个细节,并在完成推导后立即返回。假设我们选择了正确的数据骗局,拆分后的程序如下所示:

FMAP::(a-&>b)->;可能a->;可能b FMAP=\fab ma->;大小写不存在->;只有a->;只是_。

现在我们运行Apply Fab。因为任何东西都不需要任何论证,也不会产生任何漏洞,所以我们只需要看一下正义的情况:

FMAP::(a-&>b)->;可能a->;可能b FMAP=\fab ma->;case ma of Nothing->;Nothing Only a-&>;Just(Fab_)。

FMAP::(a-&>b)->;也许a->;也许b FMAP=\fab ma->;case ma of Nothing->;Nothing Only a-&>;Just(Fab A)。

看那儿!。尽管编写这两个函数器实例的语法需要截然不同的编辑命令,但它们都由相同的策略组合描述。这就是我所说的“语义编辑”,我们正在将生成函数器实例的算法从我们的头脑中移出,并将其具体化为计算机能够理解的东西。从本质上说,只需编写一次FMAP,我们就可以教计算机将来如何为我们编写它。

我之前提到过,拆分给我们带来了一些问题。仔细阅读,您会注意到我们的策略中没有任何内容说我们需要拆分我们刚刚销毁的同一个数据构造函数。实际上,有四个不同的、有效的程序可以由上述一套策略产生:

FMAP=\fab ma->;Case ma of Nothing->;Nothing只是a->;Nothing FMAP=\fab ma->;Case ma of Nothing->;Nothing只是(Fab A)FMAP=\fab ma-&>;Case ma of Nothing->;Nothing FMAP=\fab ma->;case ma of Nothing->;Case ma of Nothing->;Nothing FMAP=\fab ma->;case ma of Nothing->;只是_只是-&>;只是(制造)

选择这些可能性的“最佳”实现在很大程度上是一个启发式的问题,我计划在稍后的帖子中描述这一点。现在,让我们假设我们的战术引擎足够聪明,可以想出您想要的。

当然,这里真正的问题是,没有什么能强迫我们的析构和拆分策略使用相同的数据构造函数。我们可以通过注意,在FMAP中,我们实际上并不是试图析构然后拆分,而是试图实现同态(一个保持结构的函数)来消除这种歧义。为了保持结构,我们最好将数据构造函数映射到自身。所以取而代之的是,让我们使用同性恋策略,而不是破坏和分裂。我们用于编写函数器实例的新策略元程序如下:

这个新版本现在可能不能再生成任何病态的FMAP实现,因为它们不是结构保持的。我们只剩下良好的实现。让我们再做一次推导,这一次是针对任何一个c a。在介绍和Homo ECA之后,我们只剩下:

FMAP::(a->;b)->;或者c a->;或者c b FMAP=\fab ma->;案例左侧c->;Left_Right a->;Right_。

第一次,我们现在只剩下两个洞了。默认行为是将策略应用于所有孔(尽管有用于“压缩”孔的组合符),这意味着将在两个孔上运行应用制造策略。对于左边的情况,我们的洞的类型是c,但是fab_的类型是b,所以这个策略在这里不适用。战术失败是每个洞的,所以我们仍然可以将其应用到另一个洞,结果是:

FMAP::(a-&>b)->;或者c a->;或者c b FMAP=\fab ma->;案例左侧c->;左_右a->;右(Fab_)。

最后,假设用任何可能检查的类型来填补这个漏洞。在第一个洞里是c,在第二个洞里是a,跟以前一样。

FMAP::(a-&>b)->;或者c a->;或者c b FMAP=\fab ma->;case ECA左c->;左c右a->;右(Fab A)

太神奇了!三种不同的函数实现,具有不同数量的数据构造函数、类型变量和基数。通过在策略级别而不是字节级别进行编程,我们可以忽略这些实现之间的表面差异,转而关注它们都以相同的方式派生的事实。

希望这篇文章能让你深入了解什么是战术,以及它们为什么有价值。在下一篇文章中,我们将看看这些东西是如何在幕后实现的,以及将其集成到语言服务器中所遇到的困难。敬请关注!