Constraint Handling Rules 从入门到精通(一) - 如何用三行代码写一个最短路径
本文根据笔者2017年的一次讲座整理。
那今天我就给大家介绍一门新的编程语言,叫Constraint Handling Rules
(缩写CHR
)。CHR和C/C++等主流语言一样,是一门general purpose编程语言,换句话说,就是什么都能写。然而大多数时候,这个语言被实现为一个库。最主流的实现,是Leuven CHR System
,被集成于各大主流Prolog实现中。下文若没有特别说明,例子代码都是在Prolog实现中自带的Leuven CHR System中执行。
在讲CHR之前,先让我装装逼,同时也让你们对CHR有个高大上的印象。
CHR被称为“超高级”的编程语言。一个语言越“高级”,通常也就带来越高的表达能力和更短的代码。同时,CHR的效率是非常高的!
“超高级”的CHR
下面借用Jon Sneyers的PPT,从一个简单的例子讲起。
先看这个图。
图中,每两个点(专业叫法是“顶点”)之间的箭头叫做“边”,箭头表示可以从一个点到另一个点,箭头上的数字表示从一个点到另一个点的距离(专业叫法是“权”)。
那么,在这个图中,如果找出从a点到e点最短的路径,会是什么呢?
显然,有两条路径,a->d->e或者a->b->c->d->e, 如图所示。每一条路径的长度都是5,图中每一种颜色表示一条最短路径。
如果用计算机来计算,经典有效的算法就是dijkstra算法了。就是你们数据结构课上讲的那些咯。(没上过课的,可以看下图体验下)
对应不同语言的话,实现这个算法的程序的长度也不同。如下图。
从图中我们看到,等级越高,代码量也越短,而最后的CHR版本只需要3行代码!下面解释这段CHR代码。
如果使用
Leuven CHR System
,那么每一个CHR程序,同时也是PROLOG程序。这些CHR程序会被编译成Prolog程序再执行。CHR的语法其实也是Prolog语法,互相调用非常简单直接,可以直接混合两种语言的代码。
第一个CHR程序
下面解释下如何理解图中的程序。
1 | % 将 source(V) 替换成 distance(V, 0)。 |
在这个程序中,我们用e(X,Y,W)
表示可以从点X移动到点Y,且距离为W,例如,e(a,b,1)
表示从a到b的距离为1。用source(V)
表示开始点,例如,source(a)
表示起始点为a。用distance(V,D)
表示从起始点到点V的距离为D。将图的信息用这些符号罗列出来,程序会被自动执行。如下所示。
大写字母开头的是变量。
执行的过程其实很简单,就是按照给出的这些初始数据,反复套用上面的几条规则。“运气好”就出结果了。这里先卖个关子,后续的文章会深入讲解。
1 | ?- e(a, b, 1), e(b, e, 5), e(b, c, 1), e(a, c, 3), e(c, e, 6), e(c, d, 2), e(a, d, 4), e(d, e, 1), source(a). |
结果列出了所有从a开头所到达的点的最短距离。这正是dijkstra算法的中间过程和结果。其中,distance(e, 0+4+1)
显示了从a到e的最短距离是0+4+1
。
第一次改进
尽管我们已经得到了想要的结果,然而,这段程序有个问题:所有的距离都以每条经过的边的距离相加的式子来表示。这是因为,在Prolog中所做的一切,都是在对源代码进行操作,也就是所谓的“元编程”。和Lisp不同,Prolog并不区分代码和数据。所以,当我们写1+2
来计算距离的时候,并不会被当成是计算1+2
的值,而是保留了源码1+2
所应该具有的原始形式。源码中使用D+W
计算距离,所以结果也保留了相应的源码形式。
有的CHR实现,特别是那些不是在Prolog系统中运行的CHR实现,有的会自动计算表达式的值。
若要求和,必须调用Prolog的is/2
,这个谓词(相当于“函数”)可以把数学式子的结果算出来。改进后的代码如下。
1 | source(V) <=> distance(V, 0). |
执行结果如下。
1 | ?- e(a, b, 1), e(b, e, 5), e(b, c, 1), e(a, c, 3), e(c, e, 6), e(c, d, 2), e(a, d, 4), e(d, e, 1), source(a). |
大功告成!distance(e, 5)
就是我们想要的结果。
第二次改进
接下来,我们让程序在找出最短距离的同时,记录这条最短路径都经过哪些点。同时,我们把D1 =< D2
改成D1 < D2
,使得相同最短距离的两条路径不会被删除。
这里我们引入了Prolog中list的语法,先不要急着看懂,不过相信接触过函数式编程语言的通常也能看懂。
下划线开头的也是变量,不过这个变量是我们不关心的,只是必须写出来。通俗点说,就是起到个占位符的作用。
1 | source(V) <=> distance([V], 0). |
执行结果如下。
1 | ?- e(a, b, 1), e(b, e, 5), e(b, c, 1), e(a, c, 3), e(c, e, 6), e(c, d, 2), e(a, d, 4), e(d, e, 1), source(a). |
在这个执行结果中,从a到e的所有最短路径都被找出来了。distance([e, d, c, b, a], 5)
和distance([e, d, a], 5)
展示了到终点e距离最短的两条线路。
第三次改进
前边的程序,都有个问题,就是把中间结果都显示出来了。如果我们不想要看到中间结果呢?用destination(X)
表示终点为X,再加一条规则,删除这些中间结果。
1 | source(V) <=> distance([V], 0). |
执行结果如下。
1 | ?- e(a, b, 1), e(b, e, 5), e(b, c, 1), e(a, c, 3), e(c, e, 6), e(c, d, 2), e(a, d, 4), e(d, e, 1), source(a), destination(e). |
这样,就只保留了到终点的距离和路径。我们再加一条规则,把destination(e)
也删除掉。程序如下。
1 | source(V) <=> distance([V], 0). |
执行结果如下。
1 | ?- e(a, b, 1), e(b, e, 5), e(b, c, 1), e(a, c, 3), e(c, e, 6), e(c, d, 2), e(a, d, 4), e(d, e, 1), source(a), destination(e). |
大功告成!
类型系统
和Prolog不同,使用 Leuven CHR System 写的CHR程序,每一个约束都必须被声明以区别于通常的Prolog谓词。 Leuven CHR System 也有自己一套独立于Prolog的类型系统。下面演示一下如何声明这个程序。
1 | % 每一个约束都要声明。声明的同时,可以指定类型和mode(例如这里的“+”号)。 |
这里有几点必须说明。
-
类型声明不是必须的,但是指定了的话,编译后程序的速度会快很多。
-
有一点和erlang和prolog很不同。在erlang和prolog中,指定了类型也没什么实际的作用,那主要是给人看的,编译的时候并不做检查。但是CHR如果指定了类型就会进行静态检查,类型错误是无法编译过去的。
-
诸如
source(V)
,distance([V], 0)
的所有的这种形式的东西,在chr中叫做约束(constraint)。
完整的程序
下面给出完整的最终程序[1]。
1 | % CHR作为Prolog的一个模块出现,开头还是要引入这个模块的。 |