本系列前面的文章:

第二天,好为人师的老明继续开讲他的私人课堂。

“今天讲NMiniKanren的运行原理。”老明敲了敲白板,开始涂画代码,“我们从一个喜闻乐见的例子开始。”

KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
var x = k.Fresh();
var y = k.Fresh();
return k.All(
k.Any(k.Eq(x, 1), k.Eq(x, 2)),
k.Any(k.Eq(y, x), k.Eq(y, "b")),
k.Eq(q, k.List(x, y)));
}));

“这题我会了!”小皮在例子下边写下答案:

[(1 1), (1 b), (2 2), (2 b)]

看到小皮没把昨天的知识忘光,老明略感欣慰:“不错。你这个答案是怎么算出来的呢?”

“呃……就是那个……”小皮忽然卡壳了。这种问题就好比几何证明题,明明一眼就能看出来的两条垂直线,真下手证明却发现还挺不容易。小皮抓了几把头发,总算理出一缕思绪:“大概就是找出所有条件可能的组合……然后算一下解……”小皮一边说,一边在白板上写着:

  • x == 1

    • y == x => (x y) == (1 1)
    • y == "b" => (x y) == (1 "b")
  • x == 2
    • y == x => (x y) == (2 2)
    • y == "b" => (x y) == (2 "b")

“嗯,其实你已经知道怎么算出答案来了。只是对于其中的细节还不甚明了。我们接下来要做的事要理清楚这个计算过程,得到一个每一步都可以由计算机明确执行的算法。

“这个算法其实就是你所说这样,找出所有可能的条件组合。每组条件组合可以求出一个解,也可能自相矛盾从而无解。由于NMiniKanren中的条件都是相等条件,所以一组条件组合可以看作一个替换(Substitution)。一个替换能产生一个解,或者无解。

“因此,只需解决下面两个问题:

  1. 要在什么数据结构上按照什么顺序遍历替换
  2. 如何从替换中算出一个解,或者判断其无解。”

遍历分支

首先,我们要从代码构造出一个数据结构(其实就是一张图)。这个数据结构能够按照一定的顺序进行遍历,并依次生成替换。

例子中的代码使用到了EqAnyAll这三种构造目标的方法。下面分别探讨怎样从这三种方法构造出我们需要的数据结构来。

Eq

k.Eq(a, b)构造的目标是什么意思呢?”老明以一个看似平凡的问题开头。

“简单,意思就是a要等于b这个条件。”

“孤立地看,是这样。但是考虑到上下文,更精确地说应该是,在上下文的基础上追加a等于b这个条件。”

小皮有点不解:“emm……多了‘追加’有什么不同呢?”

“从文字上看,多了‘追加’后,目标的解释从一种名词(一组条件)变成了动词(追加条件)。这样一来,目标不仅表达了一组条件,同时也表达了这些条件如何跟上下文结合。就Eq的情况来说,这个结合方式是‘追加’。而AnyAll会有其他结合方式。”

“虽然还不是很明白,我想这个要等AnyAll的情况一起对比才能清晰起来。我还另外有个问题,上下文指的是什么?”

“狭义地说,上下文是解释器运行到这一条代码时,已执行的代码生成的替换。

上下文 <-> 一个替换 <-> 一组条件

“广义上看,上下文还应该包含回溯分支等控制信息,不过目前我们先忽略这些。

“综合起来,按照对Eq目标的解释,我们可以用下图来表示这个目标。”

Any(或)

“接着看Any。按照上面的讨论,我们要怎么解释Any目标呢?”老明继续发问。

解释目标要说清楚两个方面:名词(什么条件)和动词(如何与上下文结合)。以一开始的例子中的k.Any(k.Eq(x, 1), k.Eq(x, 2))为例。名词方面自然就是x等于1和x等于2两个条件了,不过这两个条件是‘或’的关系。动词方面,应该是从上下文分岔出两个分支,一个分支追加x等于1这个条件,另一个分支追加x等于2这个条件。”

“很好。也就是说,和Eq不同,Any操作和上下文结合后,会生成多个替换。”老明赞许地点点头,“它把参数的分支都放在一起,就像加法似的。用图表示的话,就像下面这样。”

All(与)

“最后是All……”

“这个我也会了!”小皮打断老明,“k.All(a, b)名词上表示条件a且条件b;动词上表示上下文先追加a,再追加b。”

“你说的太笼统了。ab可能都有多个分支,这种情况下怎么做?”老明接着问道。

小皮想了想一开始做的例子,答道:“这种情况要取所有组合,也就是a的分支和b的分支两两组合!最后分支数量等于a分支数量乘以b分支数量。”

“很好。如果Any类比加法,那么All类比的是乘法。下面这图描述了开头例子中的All方法的结合过程。

这是个有向图,每条边表示一次追加条件的过程。每条从开始节点(上下文)到结尾的路径,上面的节点组合起来就是一个替换。遍历所有路径,我们就遍历了所有替换。而遍历的顺序,就是解释器输出结果的顺序。

Anyi

接下来我们还可以来看看Anyi

普通的Any使用的普通的树结构遍历顺序:

Anyi以交替的顺序遍历分支:

Alli类似采用交替的顺序遍历,这里就不再画了(主要是不好画,懒)。

再看目标(Goal)

上一篇主要从构造目标的角度出发,介绍了不同方式构造出来的目标。为了实现NMiniKanren的解释器,我们需要更加深入地了解在解释器的实现中,Goal是什么类型。

在前面的讨论中,我们知道,目标的含义是对上下文/一个替换按照某种方式追加一些条件,返回零个、一个或多个替换——Eq返回一个;AnyAll可能返回多个;另外前面没讨论到的Fail会返回零个。

从这个描述不难看出,最方便表述目标类型的是一个单参数函数,其参数是一个替换,返回值是替换的枚举,相当于C#中的Enumerable<替换>,也可以说是一个替换的流(Stream)。

Goal: (替换) -> Stream<替换>

Goal(替换)这个函数调用的含义是把Goal包含的条件,追加到替换上,返回一系列(因为可能有分支,就会变成多个)的替换。

“为什么不直接用List呢?”小皮又发问了。

“因为很多情况下,分支数量会很多,甚至是无穷多,而我们只需要挨个取前面几个结果就够了。这种情况下使用List会极大降低解释器效率,甚至造成死循环。”

递归的情况

“略。”

“啥?”小皮瞪了下眼。

“懒得画,留着思考吧。”

替换求解

“生成替换后,剩下的就是求解了。

“替换求解的方法很简单,就是应用一下小学时学过的代入消元法。来,看看这个怎么解。”老明一边说一边写下例题:

(1) y == x
(2) q == (x y)
(3) x == 1

毕竟是小学难度的题目,小皮看了一眼,马上就有了解法:“x等于1是确定的了,把(3)代入(1)后,y也等于1。把(1)和(3)都代入(2),得到q等于(1 1)。”

“解是求出来了,不过你觉得你这个步骤有通用性吗?”老明虚着眼说,“计算机能自觉地使用你这个蛇皮顺序吗?”

“呃……”小皮陷入沉思。判断代入顺序的规则似乎还挺麻烦的。或者简单粗暴按照所有顺序都代入一遍?

“其实没想象中复杂,按顺序代入一遍,再反过来代入一遍,就OK了。”

按顺序代入

把(1)代入(2)(3):

(1) y == x
(2) q == (x x)
(3) x == 1

把(2)代入(3):

(1) y == x
(2) q == (x x)
(3) x == 1

在解释器实现中,条件是一条一条追加上来的。可以每次追加条件的时候,将已有的条件代入新条件,这样就把这一步化解到生成替换的过程中了。

加入条件(1) y == x:

(1) y == x

加入条件(2) q == (x y):

(1) y == x
(2) q == (x x)

加入条件(3) x == 1:

(1) y == x
(2) q == (x x)
(3) x == 1

按相反顺序代入

把(3)代入(2)(1):

(1) y == 1
(2) q == (1 1)
(3) x == 1

把(2)代入(1):

(1) y == 1
(2) q == (1 1)
(3) x == 1

搞定!

这只是个简单的例子。实际情况还可能会出现无解、自由变量以及死循环等情况。这里就不多赘述了。

再议“非”运算

“现在能看出NMiniKanren为什么不支持‘非’运算了吗?”

小皮认真想了一会,说:“岂止不支持‘非’,‘大于’和‘小于’这些也不行吧。按照代入消元法,NMiniKanren只支持相等条件。”。

“那如果要支持这些运算应该怎么做呢?”

“要拓展条件的类型。除了相等条件,还要有不相等条件等。响应的求解算法也要有所变化。”

“没错。改动虽然不大,但是代码看起来会混乱得多。所以以教学为目的的话,就不支持这些了。”

小结

不知不觉时间已到了喜闻乐见的午餐时间,于是老明总结道:“虽然还没有落地成代码,但运行原理算是弄清楚了。关键点就两个:

  1. 要在什么数据结构上按照什么顺序遍历替换。
  2. 如何从替换中算出一个解,或者判断其无解。

“第一点,我们从代码构造了一张图。该图的每条路径对应一个替换,遍历路径的顺序就是遍历替换的顺序。同时也明确了目标Goal的类型。

“第二点,我们使用代入消元法,来回两遍代入解出了所有未知量。”

“接下来可以写代码实现NMiniKanren解释器了吧。”理解了原理后,小皮的十条手指已经饥渴难耐,蚯蚓似的扭动着。

“不着急,下午还要先讲一个编程小技巧,然后就可以开搞了。”

逻辑式编程语言极简实现(使用C#) - 3. 运行原理的更多相关文章

  1. 逻辑式编程语言极简实现(使用C#) - 2. 一道逻辑题:谁是凶手

    本系列前面的文章: 逻辑式编程语言极简实现(使用C#) - 1. 逻辑式编程语言介绍 这是一道Prolog经典的练习题,中文翻译版来自阮一峰的文章<Prolog 语言入门教程>. 问题 B ...

  2. 逻辑式编程语言极简实现(使用C#) - 4. 代码实现(完结)

    本文是本系列的完结篇.本系列前面的文章: 逻辑式编程语言极简实现(使用C#) - 1. 逻辑式编程语言介绍 逻辑式编程语言极简实现(使用C#) - 2. 一道逻辑题:谁是凶手 逻辑式编程语言极简实现( ...

  3. 逻辑式编程语言极简实现(使用C#) - 1. 逻辑式编程语言介绍

    相信很多朋友对于逻辑式编程语言,都有一种最熟悉的陌生人的感觉.一方面,平时在书籍.在资讯网站,偶尔能看到一些吹嘘逻辑式编程的话语.但另一方面,也没见过周围有人真正用到它(除了SQL). 遥记当时看&l ...

  4. Docker极简入门:使用Docker运行Java程序

    运行简单的Java程序 先在当前目录创建App.java文件 public class App{ public static void main(String[] args){ String os = ...

  5. Docker极简入门:使用Docker-Compose 运行网站浏览量统计Demo

    Docker-Compose 是一个可以对 Docker 容器集群的快速编排的工具,能够减轻您心智和手指的负担. 简单的来说 Docker-Compose 就是将你运行多个容器的命令编写到了一起,类似 ...

  6. Atitit.编程语言的主要的种类and趋势 逻辑式语言..函数式语言...命令式语言

    Atitit.编程语言的主要的种类and趋势 逻辑式语言..函数式语言...命令式语言 1. 编程语言的主要的种类 逻辑式语言..函数式语言...命令式语言 1 2. 逻辑式语言,,不必考虑实现过程而 ...

  7. 原生JS轮播-各种效果的极简实现

    寒假持续摸鱼中~此为老早以前博客的重写,当时还是分开写的,这里汇总重写,正好复习一遍~ 春招我来了! 所有有意思的,一股脑先扔进收藏,然后再也不看哈哈,真是糟糕. 今日事,今日毕,说起来容易. 当时竟 ...

  8. HTML5 极简的JS函数

    页面初始化 mui框架将很多功能配置都集中在mui.init方法中,要使用某项功能,只需要在mui.init方法中完成对应参数配置即可,目前支持在mui.init方法中配置的功能包括:创建子页面.关闭 ...

  9. 【股票盯盘软件】01_程序员炒股之开发一款极简风格的股票盯盘软件StockDog_V1.0.0.1

    1.前言 话说最近一段时间受疫情的影响,股市各种妖魔横行.本人作为一个入股市不满三年的小韭菜,就有幸见证了好几次历史,也是满心惊喜,就权当是接受资本市场的再教育了吧. 小韭菜的炒股方法其实很简单,这两 ...

随机推荐

  1. Java实现固定长度得01子串

    固定位数得01子串 Description 对于长度为n的一个01串,每一位都可能是0或1,一共有2 ^n 种可能.请按从小到大的顺序输出这2^n种01串. Input 包含多组数据,每组数据占一行, ...

  2. Java实现 LeetCode 218 天际线问题

    218. 天际线问题 城市的天际线是从远处观看该城市中所有建筑物形成的轮廓的外部轮廓.现在,假设您获得了城市风光照片(图A)上显示的所有建筑物的位置和高度,请编写一个程序以输出由这些建筑物形成的天际线 ...

  3. Java实现 蓝桥杯VIP 算法提高 彩票

    算法提高 彩票 时间限制:1.0s 内存限制:256.0MB 问题描述 为丰富男生节活动,贵系女生设置彩票抽奖环节,规则如下: 1.每张彩票上印有7个各不相同的号码,且这些号码的取值范围为[1, 33 ...

  4. Java中System的详细用法

    System.arraycopy System.arraycopy的函数原型是: public static void arraycopy(Object src, int srcPos, Object ...

  5. Java实现第九届蓝桥杯乘积为零

    乘积为零 如下的10行数据,每行有10个整数,请你求出它们的乘积的末尾有多少个零? 5650 4542 3554 473 946 4114 3871 9073 90 4329 2758 7949 61 ...

  6. 洛谷P1255 数楼梯

    题目描述   楼梯有N阶,上楼可以一步上一阶,也可以一步上二阶.   编一个程序,计算共有多少种不同的走法. 分析与代码   走n阶楼梯,无论是走一次走1阶还是2阶,总得迈出一步,   所以求n阶楼梯 ...

  7. iOS-线程&&进程的深入理解

    进程基本概念 进程就是一个正在运行的一个应用程序; 每一个进度都是独立的,每一个进程均在专门且手保护的内存空间内; iOS是怎么管理自己的内存的,见博客:博客地址 在Linux系统中,想要新开启一个进 ...

  8. Kubernetes日志的6个最佳实践

    本文转自Rancher Labs Kubernetes可以帮助管理部署在Pod中的上百个容器的生命周期.它是高度分布式的并且各个部分是动态的.一个已经实现的Kubernetes环境通常涉及带有集群和节 ...

  9. ubuntu12.04 qtcreate支持中文输入

    1.sudo apt-get install ibus-qt4 2.重启电脑 reboot

  10. pom.xml 文件详解

    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/20 ...