逻辑式编程语言极简实现(使用C#) - 3. 运行原理
本系列前面的文章:
第二天,好为人师的老明继续开讲他的私人课堂。
“今天讲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)。一个替换能产生一个解,或者无解。
“因此,只需解决下面两个问题:
- 要在什么数据结构上按照什么顺序遍历替换。
- 如何从替换中算出一个解,或者判断其无解。”
遍历分支
首先,我们要从代码构造出一个数据结构(其实就是一张图)。这个数据结构能够按照一定的顺序进行遍历,并依次生成替换。
例子中的代码使用到了Eq
、Any
和All
这三种构造目标的方法。下面分别探讨怎样从这三种方法构造出我们需要的数据结构来。
Eq
“k.Eq(a, b)
构造的目标是什么意思呢?”老明以一个看似平凡的问题开头。
“简单,意思就是a
要等于b
这个条件。”
“孤立地看,是这样。但是考虑到上下文,更精确地说应该是,在上下文的基础上追加a
等于b
这个条件。”
小皮有点不解:“emm……多了‘追加’有什么不同呢?”
“从文字上看,多了‘追加’后,目标的解释从一种名词(一组条件)变成了动词(追加条件)。这样一来,目标不仅表达了一组条件,同时也表达了这些条件如何跟上下文结合。就Eq
的情况来说,这个结合方式是‘追加’。而Any
和All
会有其他结合方式。”
“虽然还不是很明白,我想这个要等Any
和All
的情况一起对比才能清晰起来。我还另外有个问题,上下文指的是什么?”
“狭义地说,上下文是解释器运行到这一条代码时,已执行的代码生成的替换。
上下文 <-> 一个替换 <-> 一组条件
“广义上看,上下文还应该包含回溯分支等控制信息,不过目前我们先忽略这些。
“综合起来,按照对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
。”
“你说的太笼统了。a
和b
可能都有多个分支,这种情况下怎么做?”老明接着问道。
小皮想了想一开始做的例子,答道:“这种情况要取所有组合,也就是a
的分支和b
的分支两两组合!最后分支数量等于a
分支数量乘以b
分支数量。”
“很好。如果Any
类比加法,那么All
类比的是乘法。下面这图描述了开头例子中的All
方法的结合过程。
“这是个有向图,每条边表示一次追加条件的过程。每条从开始节点(上下文)到结尾的路径,上面的节点组合起来就是一个替换。遍历所有路径,我们就遍历了所有替换。而遍历的顺序,就是解释器输出结果的顺序。”
Anyi
接下来我们还可以来看看Anyi
。
普通的Any
使用的普通的树结构遍历顺序:
而Anyi
以交替的顺序遍历分支:
Alli
类似采用交替的顺序遍历,这里就不再画了(主要是不好画,懒)。
再看目标(Goal)
上一篇主要从构造目标的角度出发,介绍了不同方式构造出来的目标。为了实现NMiniKanren的解释器,我们需要更加深入地了解在解释器的实现中,Goal是什么类型。
在前面的讨论中,我们知道,目标的含义是对上下文/一个替换按照某种方式追加一些条件,返回零个、一个或多个替换——Eq
返回一个;Any
和All
可能返回多个;另外前面没讨论到的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只支持相等条件。”。
“那如果要支持这些运算应该怎么做呢?”
“要拓展条件的类型。除了相等条件,还要有不相等条件等。响应的求解算法也要有所变化。”
“没错。改动虽然不大,但是代码看起来会混乱得多。所以以教学为目的的话,就不支持这些了。”
小结
不知不觉时间已到了喜闻乐见的午餐时间,于是老明总结道:“虽然还没有落地成代码,但运行原理算是弄清楚了。关键点就两个:
- 要在什么数据结构上按照什么顺序遍历替换。
- 如何从替换中算出一个解,或者判断其无解。
“第一点,我们从代码构造了一张图。该图的每条路径对应一个替换,遍历路径的顺序就是遍历替换的顺序。同时也明确了目标Goal的类型。
“第二点,我们使用代入消元法,来回两遍代入解出了所有未知量。”
“接下来可以写代码实现NMiniKanren解释器了吧。”理解了原理后,小皮的十条手指已经饥渴难耐,蚯蚓似的扭动着。
“不着急,下午还要先讲一个编程小技巧,然后就可以开搞了。”
逻辑式编程语言极简实现(使用C#) - 3. 运行原理的更多相关文章
- 逻辑式编程语言极简实现(使用C#) - 2. 一道逻辑题:谁是凶手
本系列前面的文章: 逻辑式编程语言极简实现(使用C#) - 1. 逻辑式编程语言介绍 这是一道Prolog经典的练习题,中文翻译版来自阮一峰的文章<Prolog 语言入门教程>. 问题 B ...
- 逻辑式编程语言极简实现(使用C#) - 4. 代码实现(完结)
本文是本系列的完结篇.本系列前面的文章: 逻辑式编程语言极简实现(使用C#) - 1. 逻辑式编程语言介绍 逻辑式编程语言极简实现(使用C#) - 2. 一道逻辑题:谁是凶手 逻辑式编程语言极简实现( ...
- 逻辑式编程语言极简实现(使用C#) - 1. 逻辑式编程语言介绍
相信很多朋友对于逻辑式编程语言,都有一种最熟悉的陌生人的感觉.一方面,平时在书籍.在资讯网站,偶尔能看到一些吹嘘逻辑式编程的话语.但另一方面,也没见过周围有人真正用到它(除了SQL). 遥记当时看&l ...
- Docker极简入门:使用Docker运行Java程序
运行简单的Java程序 先在当前目录创建App.java文件 public class App{ public static void main(String[] args){ String os = ...
- Docker极简入门:使用Docker-Compose 运行网站浏览量统计Demo
Docker-Compose 是一个可以对 Docker 容器集群的快速编排的工具,能够减轻您心智和手指的负担. 简单的来说 Docker-Compose 就是将你运行多个容器的命令编写到了一起,类似 ...
- Atitit.编程语言的主要的种类and趋势 逻辑式语言..函数式语言...命令式语言
Atitit.编程语言的主要的种类and趋势 逻辑式语言..函数式语言...命令式语言 1. 编程语言的主要的种类 逻辑式语言..函数式语言...命令式语言 1 2. 逻辑式语言,,不必考虑实现过程而 ...
- 原生JS轮播-各种效果的极简实现
寒假持续摸鱼中~此为老早以前博客的重写,当时还是分开写的,这里汇总重写,正好复习一遍~ 春招我来了! 所有有意思的,一股脑先扔进收藏,然后再也不看哈哈,真是糟糕. 今日事,今日毕,说起来容易. 当时竟 ...
- HTML5 极简的JS函数
页面初始化 mui框架将很多功能配置都集中在mui.init方法中,要使用某项功能,只需要在mui.init方法中完成对应参数配置即可,目前支持在mui.init方法中配置的功能包括:创建子页面.关闭 ...
- 【股票盯盘软件】01_程序员炒股之开发一款极简风格的股票盯盘软件StockDog_V1.0.0.1
1.前言 话说最近一段时间受疫情的影响,股市各种妖魔横行.本人作为一个入股市不满三年的小韭菜,就有幸见证了好几次历史,也是满心惊喜,就权当是接受资本市场的再教育了吧. 小韭菜的炒股方法其实很简单,这两 ...
随机推荐
- Java实现 LeetCode 57 插入区间
57. 插入区间 给出一个无重叠的 ,按照区间起始端点排序的区间列表. 在列表中插入一个新的区间,你需要确保列表中的区间仍然有序且不重叠(如果有必要的话,可以合并区间). 示例 1: 输入: inte ...
- Java实现第八届蓝桥杯正则问题
正则问题 考虑一种简单的正则表达式: 只由 x ( ) | 组成的正则表达式. 小明想求出这个正则表达式能接受的最长字符串的长度. 例如 ((xx|xxx)x|(x|xx))xx 能接受的最长字符串是 ...
- Linux 系统资源查看
vmstat监控系统资源 vm [刷新延时 刷新次数],vmstat 1 3 dmesg查看开机时内核检测信息 dmesg | grep CPU free命令查看内存使用状态 查看cpu信息:cat ...
- 关于uniapp获取当前距离屏幕顶部的距离
onPageScroll(e){ console.log(e); }
- 通过SecureCRT向远程Linux主机上传和下载文件
有时候直接在Linux服务器上通过 wget 或 curl 工具下截比较大的网络文件时会比较慢,这时我们通常会改用在Windows平台通过迅雷等更加现代化的下载功具下好目标文件(迅雷开会员才能更高速的 ...
- list基本运用
#include<iostream> #include<list> using namespace std; list<int>list1,list2; void ...
- Spring WebFlux 学习笔记 - (一) 前传:学习Java 8 Stream Api (3) - Stream的终端操作
Stream API Java8中有两大最为重要的改变:第一个是 Lambda 表达式:另外一个则是 Stream API(java.util.stream.*). Stream 是 Java8 中处 ...
- Python 导入CSV、JSON、XML数据
常见的机器可读格式包括: - 逗号分隔值(Comma-Separated Values,CSV)- 制表符分隔值(tab-separated values,TSV)- JavaScript 对象符号( ...
- Windows环境下PHP安装pthreads多线程扩展
一.判断PHP是ts还是nts版 通过phpinfo(); 查看其中的 Thread Safety 项,这个项目就是查看是否是线程安全,如果是:enabled,一般来说应该是ts版,否则是nts版. ...
- 关于thinkphp5下URL附加参数,无法获取到(?参数)
nginx 配置问题: 修改配置后: