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

栏目: IT技术 · 发布时间: 4年前

内容简介:相信很多朋友对于逻辑式编程语言,都有一种最熟悉的陌生人的感觉。一方面,平时在书籍、在资讯网站,偶尔能看到一些吹嘘逻辑式编程的话语。但另一方面,也没见过周围有人真正用到它(除了SQL)。遥记当时看《The Reasoned Schemer》(一本讲逻辑式编程语言的小人书),被最后两页的解释器实现惊艳到了。看似如此复杂的计算逻辑,其实现竟然这么简洁。不过碍于当时水平有限,也就囫囵吞枣般看了过去。后来有一天,不知何故脑子灵光一闪,把图遍历和流计算模式联系在一起,瞬间明白了《The Reasoned Schemer

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

遥记当时看《The Reasoned Schemer》(一本讲逻辑式编程语言的小人书),被最后两页的解释器实现惊艳到了。看似如此复杂的计算逻辑,其实现竟然这么简洁。不过碍于当时水平有限,也就囫囵吞枣般看了过去。后来有一天,不知何故脑子灵光一闪,把图遍历和流计算模式联系在一起,瞬间明白了《The Reasoned Schemer》中的做法。动手写了写代码,果然如此,短短 两百来行代码 ,就完成了解释器的实现,才发现原来如此简单。很多时候,并非问题本身有多难,只是没有想到正确的方法。

本系列将尽可能简洁地说明逻辑式编程语音的原理,并实现一门简单的逻辑式编程语言。考虑到C#的用户较多,因此选择用C#来实现。实现的这门语言就叫NMiniKanren。文章总体内容如下:

  • NMiniKanren语言介绍
    • 语言基础
    • 一道有趣的逻辑题:谁是凶手
  • NMiniKanren运行原理
    • 构造条件关系图,遍历分支
    • 代入消元法解未知量
  • 实现NMiniKanren
    • 流计算模式简介
    • 代入消元法的实现
    • 遍历分支的实现

故事从两个正在吃午餐的 程序员 说起。

老明和小皮是就职于同一家传统企业的程序员。这天,两人吃着午餐。老明边吃边刷着抖音,鼻孔时不时喷出几条米粉。

小皮是一脸麻木地刷着求职网和资讯网,忽然几个大字映入眼底:《新型逻辑式编程语言重磅出世,即将颠覆IT界!》小皮一阵好奇,往下一翻,结果接着的是一些难懂的话,什么“一阶逻辑”,什么“合一算法”,以及鬼画符似的公式之类。

小皮看得索然无味,但被勾引起来的对逻辑式编程的兴趣仿佛 澳洲 森林大火一样难以平息。于是伸手拍下老明高举手机的左手,问道:“嘿!逻辑式编程有了解过么?是个啥玩意儿?”

“逻辑式编程啊……嘿嘿,前段时间刚好稍微了解了一下。”老明鼻孔朝天吸了两口气,“我说的稍微了解,是指实现了一门逻辑式编程语言。”

“不愧是资深老IT,了解也比别人深入一坨坨……”

“也就比你早来一年好不好……我是一边看一本奇书一边做的。Dan老师(Dan Friedman)写的《The Reasoned Schemer》。这本书挺值得一看的,书中使用一门教学用的逻辑式编程语言,讲解这门语言的特性、用法、以及原理。最后还给出了这门语言的实现。核心代码只用了两页纸。

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

“所谓逻辑式编程,从使用上看是把声明式编程发挥到极致的一种编程范式。普通的编程语言,大部分还是基于命令式编程,需要你告诉机器每一步执行什么指令。而逻辑式编程的理念是,我们只需要告诉机器我们需要的目标,机器会根据这个目标自动探索执行过程。

“逻辑式编程的特点是可以反向运行。你可以像做数学题一样,声明未知量,列出方程,然后程序会为你求解未知量。”

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

“挺神奇的。听起来有点像AI编程。不过这么高级的东西怎么没有流行起来?感觉可以节省不少人力。”小皮忽然有种饭碗即将不保的感觉。

“嘿嘿……想得美。其实逻辑式编程,既不智能,也不好用。你回忆一下你中学的时候是怎么解方程组的?”

“嗯……先盯一会方程组,看看它长得像不像有快捷解法的样子。看不出来的话就用代入法慢慢算。这和逻辑式编程有什么关系?”

逻辑式编程并不智能,它只是把某种类似代入法的通用算法内置到解释器里 。逻辑式编程语言写的程序运行时,不过是根据通用算法进行求解而已。它不像人一样会去寻找更快捷的方法,同时也不能解决超纲问题。

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

而且逻辑式编程语言的学习成本也不低 。如果你要用好这门语言,你得把它使用的通用算法搞清楚。虽然你写的声明式的代码,但内心要时刻清楚程序的执行过程。 如果你拿它当个黑盒来用,那很可能你写出来的程序的执行效率会非常低,甚至跑出一些莫名其妙的结果 。”

“哦哦,要学会用它,还得先懂得怎么实现它。这学习成本还挺高的。”小皮跟着吐槽,不过他知道老明表明上看似嫌弃逻辑式编程的实用性,私底下肯定玩得不亦乐乎,并且也喜欢跟别人分享。于是小皮接着道:“虽然应该是用不着,但感觉挺有意思的,再仔细讲讲呗。天天写CRUD,脑子都淡出个鸟了。”

果然老明坐直起来:“《The Reasoned Schemer》用的这门逻辑式编程语言叫miniKanren,用Scheme/Lisp实现的。去年给你安利过Scheme了,现在掌握得怎么样?”

“一窍不通……”小皮大窘。去年到现在,小皮一直很忙,并没有自学什么东西。如果没有外力驱动的话,他还将一直忙下去。

“果然如此。所以我顺手也实现了个C#魔改版本的miniKanren。就叫NMiniKanren。我把NMiniKanren实现为C#的一个DSL。这样的好处是方便熟悉C#或者 Java 的人快速上手;坏处是DSL会受限于C#语言的能力,代码看起来没有Scheme版那么优雅。”老明用左手做了个打引号的动作,“先从简单的例子开始吧。比如说,有个未知量 q ,我们的目标是让 q 等于5或者等于6。那么满足条件的 q 值有哪些?”

“不就是5和6么……这也太简单了吧。”

“Bingo!”老明打了个响指,“我们先用简单的例子看看代码结构。”只见老明两指轻轻夹住一只筷子,勾出几条米粉,快速在桌上摆出如下代码:

// k提供NMiniKanren的方法,q是待求解的未知变量。
var res = KRunner.Run(null /* null表示输出所有可能的结果 */, (k, q) =>
{
    // q == 5 或者 q == 6
    return k.Any(
        k.Eq(q, 5),
        k.Eq(q, 6));
});
KRunner.PrintResult(res);  // 输出结果:[5, 6]

“代码中, KRunner.Run 用于运行一段NMiniKanren代码,它的声明如下。”老明继续拨动米粉:

public class KRunner
{
    public static IList<object> Run(int? n, Func<KRunner, FreshVariable, Goal> body)
    {
        ...
    }
}

“其中,参数 n 是返回结果的数量限制, n = null 表示无限制;参数 body 是一个函数:

KRunner

“接着我们看函数体的代码。 k.Eq(q, 5) 表示 q 需要等于 5k.Eq(q, 6) 表示 q 需要等于 6k.Any 表示满足至少一个条件。整段代码的意思为:求所有满足 q 等于 5 或者 q 等于 6q 值。显然答案为 56 ,程序的运行结果也是如此。很神奇吧?”

“你这米粉打码的功夫更让我惊奇……”小皮仔细看了一会,“原来如此。不过这DSL的语法确实看着比较累。”

“主要是我想做得简单一些。其实使用C#的Lambda表达式也可以实现像……”老明勾出几条米粉摆出 q == 5 || q == 6 表达式,“……这样的语法,不过这样会增加NMiniKanren实现的复杂度。况且这无非是前缀表达式或中缀表达式这种语法层面的差别而已,语义上并没有变化。 学习应先抓住重点,花里胡哨的东西可以放到最后再来琢磨。

“嗯嗯。 KRunner.Run 里这个 null 的参数是做什么用的呢?”

KRunner.Run 的第一个参数用来限制输出结果的数量。 null 表示输出所有可能的结果。还是上面例子的条件,我们改成限制只输出 1 个结果。”小皮用筷子改了下代码:

// k提供NMiniKanren的方法,q是待求解的未知变量。
var res = KRunner.Run(1 /* 输出1个结果 */, (k, q) =>
{
    // q == 5 或者 q == 6
    return k.Any(
        k.Eq(q, 5),
        k.Eq(q, 6));
});
KRunner.PrintResult(res);  // 输出结果:[5]

“这样程序只会输出5一个结果。在一些包含递归的代码中,可能会有无穷多个结果,这种情况下需要限制输出结果的数量来避免程序不会终止。”

“原来如此。不过这个例子太简单了,有没有其他更好玩的例子。”

老明喝下一口汤,说:“好。时间不早了,我们回公司找个会议室慢慢说。”

NMiniKanren支持的数据类型

到公司后,老明的讲课开始了……

首先,要先明确NMiniKanren支持的数据类型。后续代码都要基于数据类型来编写,所以规定好数据类型是基础中的基础。

简单起见,NMiniKanren只支持四种数据类型:

  • string :就是一个普普通通的值类型,仅有值相等判断。
  • int :同 string 。使用 int 是因为有时候想少写两个双引号……
  • KPair :二元组。可用来构造链表及其他复杂的数据结构。如果你学过Lisp会对这个数据结构很熟悉。下面详细说明。
  • null :这个类型只有 null 一个值。表示空引用或者空数组。

KPair类型

KPair 的定义为:

public class KPair
{
    public object Lhs { get; set; }
    public object Rhs { get; set; }
    
    // methods
    ...
}

KPair 除了用作二元组(其实是最少用的)外,更多的是用来构造链表。构造链表时,约定一个 KPair 作为一个链表的节点, Lhs 为元素值, Rhs 为一下个节点。当 Rhsnull 时链表结束。空链表用 null 表示。

public static KPair List(IEnumerable<object> lst)
{
    var fst = lst.FirstOrDefault();
    if (fst == null)
    {
        return null;
    }
    return new KPair(fst, List(lst.Skip(1)));
}

使用 null 表示空链表其实并不合适,这里纯粹是为了简单而偷了个懒。

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

我们知道,很多复杂的数据结构都是可以通过链表来构造的。所以虽然NMiniKanren只有三种数据类型,但可以表达很多数据结构了。

这时候小皮有疑问了:“C#本身已经自带了 List 等容器了,为什么还要用 KPair 来构造链表?”

“为了让底层尽可能简洁。”老明说道,“我们都知道,程序本质上分为数据结构和算法。 算法是顺着数据结构来实现的 。简洁的数据结构会让算法的实现显得更清晰。相比C#自带的 List ,使用 KPair 构造的链表更加清晰简洁。按照构造的方式,我们的链表定义为:

  1. 空链表 null
  2. 或者是非空链表。它的第一个元素为 Lhs ,并且 Rhs 是后续的链表。

“链表相关的算法都会顺着定义的这两个分支实现:一个处理空链表的分支,一个处理非空链表的递归代码。比如说判断一个变量是不是链表的方法:

public static bool IsList(object o)
{
    // 空链表
    if (o == null)
    {
        return true;
    }
    // 非空链表
    if (o is KPair p)
    {
        // 递归
        return IsList(p.Rhs);
    }
    // 非链表
    return false;
}

“以及判断一个元素是不是在链表中的方法:

public static bool Memeber(object lst, object e)
{
    // 空链表
    if (lst == null)
    {
        return false;
    }
    // 非空链表
    if (lst is KPair p)
    {
        if (p.Lhs == null && e == null || p.Lhs.Equals(e))
        {
            return true;
        }
        else
        {
            // 递归
            return Memeber(p.Rhs, e);
        }
    }
    // 非链表
    return false;
}

“数据类型明确后,接下来我们来看看NMiniKanren能做什么。”

目标(Goal)

编写NMiniKanren代码是一个构造目标( Goal 类型)的过程。NMiniKanren解释器运行时将 求解使得目标成立的所有未知量的值

显然,有两个平凡的目标:

k.Succeed
k.Fail

其中 kKRunner 的一个实例。C#跟Java一样不能定义独立的函数和常量,所以我们DSL需要的函数和常量就都定义为 KRunner 的方法或属性。后面不再对 k 进行复述。

一个基本的目标是 k.Eq(v1, v2) 。这也是NMiniKanren唯一一个使用值来构造的目标,它表示值 v1v2 应该相等。也就是说,当 v1v2 相等时,目标 k.Eq(v1, v2) 成立;否则不成立。

这里的相等,指的是值相等:

  • 不同类型不相等。
  • string 类型相等当且仅当值相等。
  • KPair 类型相等当且仅当它们的 Lhs 相等且 Rhs 相等。

KPair 相等的定义,可以推出由 KPair 构造的数据结构(比如链表),相等条件为当且仅当它们结构一样且对应的值相等。

接下来我们看几个例子。

等于一个值

KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
    return k.Eq(q, 5);
}));  // 输出[5]

直接 q 等于 5

等于一个链表

KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
    return k.Eq(q, k.List(1, 2));
}));  // 输出[(1 2)]

k.List(1, 2) 相当于 new KPair(1, new KPair(2, null)) ,用来快速构造链表。

链表间的相等

KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
    return k.Eq(k.List(1, q), k.List(1, 2));
}));  // 输出[2]

这个例子比较像一个方程了。 q 匹配 k.List(1, 2) 的第二项,也就是 2

无法相等的例子

KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
    return k.Eq(k.List(2, q), k.List(1, 2));
}));  // 输出[]

由于 k.List(2, q) 的第一项和 k.List(1, 2) 的第一项不相等,所以这个目标无法成立, q 没有值。

不成立的例子

KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
    return k.Fail;
}));  // 输出[]

目标无法成立, q 没有值。

永远成立的例子

KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
    return k.Succeed;
}));  // 输出[_0]

目标恒成立, q 可取任意值。输出 _0 表示一个可取任意值的自由变量。

更多构造目标的方式

目标可以看作布尔表达式,因此可以通过“与或非”运算,用简单的目标构造成复杂的“组合”目标。我们把被用来构造“组合”目标的目标叫做该“组合”目标的子目标。

定义未知量

在前面的例子中,我们只有一个未知量 qq 既是未知量,也是程序输出。

在处理更复杂的问题时,通常需要定义更多的未知量。定义未知量的方法是 k.Fresh

// 定义x, y两个未知量
var x = k.Fresh()
var y = k.Fresh()

新定义的未知量和 q 一样,可以用来构造目标:

// x == 2
k.Eq(x, 2)
// x == y
k.Eq(x, y)

使用“与”运算组合的目标,仅当所有子目标成立时,目标才成立。

使用方法 k.All 来构造“与”运算组合的目标。

var g = k.All(g1, g2, g3, ...)

当且仅当 g1 , g2 , g3 , ......,都成立时, g 才成立。

特别的,空子目标的情况,即 k.All() ,恒成立。

KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
    return k.All(
        k.Eq(q, 1),
        k.Eq(q, 2));
}));  // 输出[]

KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
    var x = k.Fresh();
    var y = k.Fresh();
    return k.All(
        k.Eq(x, 1),
        k.Eq(y, x),
        k.Eq(q, k.List(x, y)));
}));  // 输出[(1 1)]

使用“或”运算组合的目标,只要一个子目标成立时,目标就成立。

使用方法 k.Any 来构造“或”运算组合的目标。

var g = k.Any(g1, g2, g3, ...)

g1 , g2 , g3 , ......中至少一个成立, g 成立。

特别的,空子目标的情况,即 k.Any() ,恒不成立。

KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
    return k.Any(
        k.Eq(q, 5),
        k.Eq(q, 6));
}));  // 输出[5, 6]

KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
    var x = k.Fresh();
    var y = k.Fresh();
    return k.All(
        k.Any(k.Eq(x, 5), k.Eq(y, 6)),
        k.Eq(q, k.List(x, y)));
}));  // 输出[(5 _0), (_0 6)]

非?

MiniKanren(以及NMiniKanren)不支持“非”运算。支持“非”会让miniKanren的实现复杂很多。

这或许令人惊讶。“与或非”在逻辑代数中一直像是连体婴儿似的扎堆出现。并且“非”运算是单目运算符,看起来应该更简单。

然而,“与”和“或”运算是在已知的两(多)个集合中取交集或者并集,结果也是已知的。而“非”运算则是把一个已知的集合映射到可能未知的集合,遍历“非”运算的结果可能会很久或者就是不可能的。

对于基于图搜索和代入法求解的miniKanren来说,支持“非”运算需要对核心的数据结构和算法做较大改变。因此以教学为目的的miniKanren没有支持“非”运算。

不过,在一定程度上,也是有不完整替代方法的。

If(这个比较奇葩,可以先跳过)

If是一个特殊的构造目标的方式。对应《The Reasoned Schemer》中的 conda

var g = k.If(g1, g2, g3)

如果 g1g2 成立,那么 g 成立;否则当且仅当 g3 成立时, g 成立。

这个和 k.Any(k.All(g1, g2), g3) 很像,但他们是有区别的:

  • k.Any(k.All(g1, g2), g3) 会解出所有让 k.All(g1, g2) 或者 g3 成立的解
  • k.If(g1, g2, g3) 如果 k.All(g1, g2) 有解,那么只给出使 k.All(g1, g2) 成立的解;否则再求使得 g3 成立的解。

也可以说,If是短路的。

这么诡异的特性有什么用呢?

它可以部分地实现“非”运算的功能:

k.If(g, k.Fail, k.Succeed)

这个这里先不详细展开了,后面用到再说。

控制输出顺序

这是一个容易被忽略的问题。如果程序需要求出所有的解,那么输出顺序影响不大。但是一些情况下,求解速度很慢,或者解的数量太多甚至无穷,这时只求前几个解,那么输出的内容就和输出顺序有关了。

因为miniKanren以图遍历的方式来查找问题的解,所以解的顺序其实也是解释器运行时遍历的顺序。先看如下例子:

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, "a"), k.Eq(y, "b")),
        k.Eq(q, k.List(x, y)));
}));  // 输出[(1 a), (1 b), (2 a), (2 b)]

有两个未知变量 xyx 可能的取值为1或2, y 可能的取值为a或b。可以看到,程序查找解的顺序为:

  • x 值为1
    • y 值为a, q=(1 a)
    • y 值为b, q=(1 b)
  • x 值为2
    • y 值为a, q=(2 a)
    • y 值为b, q=(2 b)

如果要改变这个顺序,我们有一个交替版的“与”运算 k.Alli

KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
    var x = k.Fresh();
    var y = k.Fresh();
    return k.Alli(
        k.Any(k.Eq(x, 1), k.Eq(x, 2)),
        k.Any(k.Eq(y, "a"), k.Eq(y, "b")),
        k.Eq(q, k.List(x, y)));
}));  // 输出[(1 a), (2 a), (1 b), (2 b)]

不过这个交替版也不是交替得很漂亮。下面增加 x 可能的取值到3个:

KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
    var x = k.Fresh();
    var y = k.Fresh();
    return k.Alli(
        k.Any(k.Eq(x, 1), k.Eq(x, 2), k.Eq(x, 3)),
        k.Any(k.Eq(y, "a"), k.Eq(y, "b")),
        k.Eq(q, k.List(x, y)));
}));  // 输出[(1 a), (2 a), (1 b), (3 a), (2 b), (3 b)]

同样,“或”运算也有交替版。

正常版:

KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
    return k.Any(
        k.Any(k.Eq(q, 1), k.Eq(q, 2)),
        k.Any(k.Eq(q, 3), k.Eq(q, 4)));
}));  // 输出[1, 2, 3, 4]

交替版:

KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
    return k.Anyi(
        k.Any(k.Eq(q, 1), k.Eq(q, 2)),
        k.Any(k.Eq(q, 3), k.Eq(q, 4)));
}));  // 输出[1, 3, 2, 4]

后面讲到miniKanren实现原理时会解释正常版、交替版为什么会是这种表现。

递归

无递归,不编程!

递归给予了程序语言无限的可能。NMiniKanren也是支持递归的。下面我们实现一个方法,这个方法构造的目标要求指定的值或者未知量是一个所有元素都为1的链表。

错误的示范

一个值或者未知量的元素都为1,用递归的方式表达是:

  1. 它是一个空链表
  2. 或者它的第一个元素是1,且剩余部分的元素都为1

直译为代码就是:

public static Goal AllOne_Wrong(this KRunner k, object lst)
{
    var d = k.Fresh();
    return k.Any(
        // 空链表
        k.Eq(lst, null),
        // 非空
        k.All(
            k.Eq(lst, k.Pair(1, d)),  // 第一个元素是1
            k.AllOne_Wrong(d)));  // 剩余部分的元素都是1
}

直接运行这段代码,死循环。

为什么呢?因为我们直接使用C#的方法来定义函数,C#在构造目标的时候,会运行最后一行的 k.AllOne_Wrong(d) ,于是就陷入死循环了。

正确的做法

为了避免死循环,在递归调用的地方,需要用 k.Recurse 方法特殊处理一下,让递归的部分变为惰性求值,防止直接调用:

public static Goal AllOne(this KRunner k, object lst)
{
    var d = k.Fresh();
    return k.Any(
        k.Eq(lst, null),
        k.All(
            k.Eq(lst, k.Pair(1, d)),
            k.Recurse(() => k.AllOne(d))));
}

随便构造两个问题运行一下:

KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
    var x = k.Fresh();
    var y = k.Fresh();
    return k.All(
        k.AllOne(k.List(1, x, y, 1)),
        k.Eq(q, k.List(x, y)));
}));  // 输出[(1 1)]

KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
    var x = k.Fresh();
    var y = k.Fresh();
    return k.All(
        k.AllOne(k.List(1, x, y, 0)),
        k.Eq(q, k.List(x, y)));
}));  // 输出[]

k.Recurse 这种处理方法其实是比较丑陋而且不好用的。特别是多个函数相互调用引起递归的情况,很可能会漏写 k.Recurse 导致死循环。

听到这里,小皮疑惑道:“这个有点丑诶。刚刚网上瞄了下《The Reasoned Schemer》,发现人家的递归并不需要这种特殊处理。看起来直接调用就OK了,跟普通程序没啥两样,很美很和谐。”

“因为《The Reasoned Schemer》使用Lisp的宏实现的miniKanren,宏的机制会有类似惰性计算的效果。”老明用擦白板的抹布拍了下小皮的脑袋,“可惜你不会Lisp。如果你不努力提升自己,那丑一点也只能将就着看了。”

关于数值计算

MiniKanren没有直接支持数值计算。也就是说,miniKanren不能直接帮你解像 2 + x = 5 的这种方程。如果要直接支持数值计算,需要实现很多数学相关的运算和变换,会让miniKanren的实现变得非常复杂。MiniKanren是教学性质的语言,只支持了最基本的逻辑判断功能。

“没有‘直接’支持。”小皮敏锐地发现了关键,“也就是可以间接支持咯?”

“没错!你想想,0和1是我们支持的符号,与和或也是我们支持的运算符!”老明兴奋起来了。

“二进制?”

“是的!任何一本计算机组成原理教程都会教你怎么做!这里就不多说了,你可以自己回去试一下。”

“嗯嗯。我以前这门课学得还不错,现在还记得大概是先实现半加器和全加器,然后构造加法器和乘法器等。”小皮干劲十足,从底层开始让他想起了小时候玩泥巴的乐趣。

“而且用miniKanren实现的不是一般的加法器和乘法器,是可以反向运行的加法器和乘法器。”

“有意思,晚上下班回去就去试试。”小皮真心地说。正如他下班回家躺床上后,就再也不想动弹一样真心实意。

(注:《The Reasoned Schemer》第7章、第8章会讲到相关内容。)

小结

“好了,NMiniKanren语言的介绍就先说到这里了。”老明拍了拍手,看了看前面的例子,撇了撇嘴,“以C#的DSL方式实现出来果然丑很多,语法上都不一致了。不过核心功能都还在。”

“接下来就是最有意思的部分,NMiniKanren的原理了吧?”

“是的。不过在继续之前,还有个问题。”

“啥问题?”

“中午米线都用来打码了。现在肚子饿了,你要请我吃下午茶。”

NMiniKanren的源码在: https://github.com/sKabYY/NMiniKanren

示例代码在: https://github.com/sKabYY/NMiniKanren/tree/master/NMiniKaren.Tests


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

世界是平的(3.0版)

世界是平的(3.0版)

[美] 托马斯·弗里德曼 / 何帆、肖莹莹、郝正非 / 湖南科学技术出版社 / 2008-9 / 58.00元

世界变得平坦,是不是迫使我们跑得更快才能拥有一席之地? 在《世界是平的》中,托马斯·弗里德曼描述了当代世界发生的重大变化。科技和通信领域如闪电般迅速的进步,使全世界的人们可以空前地彼此接近——在印度和中国创造爆炸式增长的财富;挑战我们中的一些人,比他们更快占领地盘。3.0版新增两章,更新了报告和注释方面的内容,这些内容均采自作者考察世界各地特别是整个美国中心地带的见闻,在美国本土,世界的平坦......一起来看看 《世界是平的(3.0版)》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换