伪装图:从todo列表到构建系统

在这篇博客文章中,我们将看一个使用图的代数用于操纵序列项,乍一看可能不像图表,但实际上是依赖图伪装。我们将开发一个小型DSL,用于在藻类库并将展示如何将其用于计划假期,更重要的是,用于编写软件构建系统。这篇博客文章将部分回答在这个reddit讨论.

更新:这一系列博客文章是作为功能珍珠在2017年哈斯克尔研讨会上。

Todo列表和图代数

待办事项是序列项目正如人们所期望的那样,这需要完成。序列中项目的顺序很重要,因为某些项目可能依赖于其他项目。最简单的todo列表是空的一个。然后我们有一个包含单个项目的todo列表,从中我们可以使用我们引入的相同操作符来构建更长的列表。

项目将对应于图形顶点。我们将使用重载字符串GHC扩展,因此我们可以创建todo项,而无需将其显式包装为顶点。这也将允许每个人选择他们最喜欢的字符串表示;普通的老字符串适用于我们的示例:

{-#语言重载字符串#-}进口 数据。字符串

进口 代数。图表
进口 代数。图表。Util公司

实例(IsString(IsString) ,订单 )=> IsString(Is字符串)(托多 )哪里来自字符串=顶点.来自字符串购物 :: 托多 字符串购物= “礼物”

在这里托多是一个图表实例,其实现将在稍后显示。可以使用覆盖代数运算符+:

购物 :: 托多 字符串购物= “礼物” + “外套” + “围巾”

这个语义学待办事项列表只是按照可以完成的顺序列出的项目列表,或者没有什么如果不存在满足不同项之间所有依赖约束的可能完成顺序。我们可以使用待办事项具有以下签名的函数:

待办事项 :: 订单  => 托多  -> 也许 吧[]

覆盖操作符是可交换的,因此重新排序购物列表中的项目不会更改语义:

λ>todo购物只是[“外套”,“礼物”,“围巾”]λ>待办事项$ “外套” + “围巾” + “礼物”
只是[“外套”,“礼物”,“围巾”]λ>购物== “外套” + “围巾” + “礼物”
真的

如您所见,这些项只是按字母顺序排列,因为它们之间没有依赖关系。让我们添加一些!为此,我们将使用连接算子→来自代数。当两个待办事项列表与→组合时,其含义是,在开始完成第二个待办事宜列表中的项目之前,必须完成第一个列表中的所有项目。我目前正在计划一次假期旅行,去拜访朋友,因此在旅行之前需要打包所有我买的东西:

假日 :: 托多 字符串假日=购物*“打包”*“旅行”

λ>今日假期只是[“外套”,“礼物”,“围巾”,“打包”,“旅行”]λ>购物==假日False(错误)
λ>购物`是SubgraphOf`假日真的

项目“包装”“旅行”已附加到列表末尾,即使“打包”出现在前面“礼物”按字母顺序,正确地说:我们不能在买礼物之前就把它们打包!

现在,让我们向现有的todo列表添加一个新的依赖项约束。例如,我可能想在买外套之前买一条新围巾,因为我想确保外套与新围巾搭配起来好看:

λ>待办事项$假日+ “围巾”*“外套”
只是[“礼物”,“围巾”,“外套”,“打包”,“旅行”]

看看结果列表是如何变化的:“外套”已在之后移动“围巾”满足新的约束!当然,添加相互矛盾的约束并不太难,这使得todo列表无法安排:

λ>待办事项$假日+ “旅行”*“礼物”
没有什么

如果我们的待办事项列表中存在循环依赖项,我们无法完成所有项目:“礼物” →“打包” “旅行” → “礼物”.

有时,有一些关于项目优先级尽快或尽可能晚地安排一些项目。让我用一个例子来说明这一点,修改我们的todo列表如下:

购物 :: 托多 字符串购物= “礼物” + “外套” + “电话妻子”*“围巾”

假日 :: 托多 字符串假日=购物*“打包”*“旅行” + “围巾”*“外套”

如你所见,我现在想在买围巾之前给我妻子打电话,确保它也与她的一条围巾的颜色相匹配(她有十几条围巾,我不可能记住所有的颜色)。让我们看看这是如何改变结果顺序的:

λ>今日假期只是[“电话妻子”,“礼物”,“围巾”,“外套”,“打包”,“旅行”]

这行得通,但有点不令人满意:理想情况下,我想给我妻子打电话就在之前买围巾。为了达到这个目的,我可以通过改变商品的优先级来修改购物清单“电话妻子”:

--降低给定待办事项列表中项目的优先级
低的 :: 托多  -> 托多 

购物 :: 托多 字符串购物= “礼物” + “外套” +低的“电话妻子”*“围巾”

λ>todo假期只是[“礼物”,“电话妻子”,“围巾”,“外套”,“打包”,“旅行”]

啊哈,这样更好:“电话妻子”计划得越晚越好,现在就在“围巾”,根据需要。但是,等等,如果我妻子发现我给她打电话的优先级很低,我就麻烦了!我需要找到更好的方法来达到同样的效果。本质上,我们希望有一个连接运算符的变体,在调度过程中尽可能地将参数拉到一起(或者,我们也可能希望将参数彼此排斥得尽可能远)。

--把论点尽可能地集中在一起(~*~):: 订单=> 托多-> 托多-> 托多--尽可能地排除参数(>*<):: 订单=> 托多-> 托多-> 托多购物 :: 托多 字符串购物= “礼物” + “外套” + “电话妻子” ~*~ “围巾”

这看起来更好,并导致与上述代码相同的结果。

决赛假日表达式可以可视化如下:
图形表达式

这里,重叠操作符+通过将其参数放在一起显示,连接操作符由箭头显示,带有小三角形的箭头表示紧密连接操作人员~*~。通过遵循代数定律,我们可以将图表达式展平为依赖关系图,如下所示:
依赖关系图

图表如下线性化的到项目列表中待办事项功能。

所以,给你:你可以用藻类图书馆!

在生成系统中构造命令行

以上让我想起构建系统构造用于执行各种外部程序(如编译器、链接器等)的命令行。命令行只是字符串列表,通常包括正在执行的程序的路径、源文件的路径和各种配置标志。其中一些字符串之间可能有顺序约束,这与todo列表非常相似。让我们看看是否可以使用我们用于待办事项列表的小型DSL用于描述命令行。

下面是一个简单的编译命令行“源代码.c”使用GCC编译器:

命令行1 :: 托多 字符串命令行1= “gcc”* (“-c” ~*~ “源代码.c” + “-o” ~*~ “src.o”)λ>待办事项命令行1只是[“gcc”,“-c”,“src.c”,“-o”,“src.o”]

构建系统会定期进行重构,如果需要,可以跟踪构建系统中的更改以自动重建受影响的文件(例如,在新的GHC构建系统中哈德良我们跟踪命令行中的更改,这对其开发有很大帮助)。某些更改不会更改构建系统的语义,因此可以安全地忽略。例如,可以重写命令行1通过交换命令行的源文件和对象文件部分定义:

命令行2 :: 托多 字符串命令行2= “gcc”* (“-o” ~*~ “src.o” + “-c” ~*~ “源代码.c”)λ>命令行1==命令行2真的
λ>待办事项cmdLine2只是[“gcc”,“-c”,“源代码.c”,“-o”,“src.o”]

如您所见,上述更改没有任何影响,正如我们从+的可交换性中所预期的那样。更换~*~另一方面,通常的连接运算符有时会导致语义的变化:

命令行3 :: 托多 字符串命令行3= “gcc”* (“-c”*“源代码.c” + “-o”*“src.o”)λ>命令行1==命令行3False(错误)
λ>待办事项命令行3只是[“gcc”,“-o”,“-c”,“源代码.c”,“src.o”]

从依存关系图的角度来看,结果序列是正确的,但不是有效的命令行:标志对被推开了。代数可以识别语义的变化,重新运行构建系统应该会发现错误。

作为最后一个练习,让我们编写一个转换命令行的函数:

优化 :: 国际 -> 托多 字符串 -> 托多 字符串优化水平=(*标志)哪里旗帜=顶点$ “-O” ++ 显示水平λ>待办事项$优化2命令行1只是[“gcc”,“-c”,“源代码.c”,“-o”,“src.o”,“-O2”]

正如你所见,优化2附加优化标志“-O2”在命令行的末尾,即。优化2==(*“-O2”).

实际构建系统中的命令行包含许多有条件的仅在特定平台等上编译特定文件时才包含的标志。您可以阅读有关我们如何在Hadrian中处理条件标志的信息在这里.

发动机罩下面

调度受依赖约束的项目列表是一个众所周知的问题,可以通过以下方法解决拓扑排序基础依赖关系图的。GHC的容器库在中实现了拓扑排序数据。图表模块。它对邻接列表进行操作,为了重用它,我们可以定义以下内容图表实例:

新类型 相邻地图= 调幅{相邻地图:: 地图一个(设置a) }衍生(等式,显示)实例 订单  => 图表(相邻地图 )哪里
    类型 顶点(相邻地图a)=空的= 调幅 $ 地图.空的顶点x= 调幅 $ 地图.单粒子x设置.空的覆盖x y= 调幅 $ 地图.与联合设置.联盟(相邻地图x)(相邻地图y)连接x y= 调幅 $ 地图.unions与设置.联盟[相邻地图x,相邻地图y,来自集合(常量 .关键帧集$相邻地图y)(按键设置$相邻地图x)]邻接列表 :: 相邻地图  ->[(, [])]邻接列表= 地图(功能性维修计划 设置.到AscList). 地图.到AscList.相邻地图λ>邻接列表$集团[1..4][(1,[2,,4]),(2,[,4]),(,[4]),(4,[])]

托多构建在排序靠前图形实例,它只是一个新类型包装器相邻地图基于图形的表示:

新类型 排名靠前的排序= TS公司{从顶部排序:: 相邻地图一个}衍生(显示,号码)实例 订单  => 等式(排名靠前的排序 )哪里x个===顶部排序x==top排序y

习惯等式实例确保如果图的拓扑排序一致,则认为图是相等的。特别是,所有循环图都属于相同的等价类,对应于 topSort g==无:

λ>路径[1..4]==(集团[1..4]:: 排名靠前的排序 国际)真的
λ>top排序$集团[1..4]只是[1,2,,4]λ>top排序$路径[1..4]只是[1,2,,4]λ>top排序$转置$集团[1..4]只是[4,,2,1]λ>top排序$电路[1..4]没有什么

功能top排序简单地呼叫数据。图.top排序进行必要的管道安装,这并不特别有趣。

当前实现具有两个问题:拓扑排序并不总是按字典顺序排在第一位,如下所示命令行3以上,其中“-o”先于“-c”第二个问题是top排序不满足闭包公理定义于上一篇博客文章。解决此问题的一种可能方法是计算可传递性约简拓扑排序之前的基础依赖关系图。

祝大家假期愉快!

留下回复

您的电子邮件地址将不会被公布。 已标记必填字段*