• Learn Prolog Now 翻译


    内容提要

     本章主要介绍使用递归操纵列表的一个实际例子:判断一个元素是否在包含在一个列表中。

     是时候介绍第一个Prolog中通过递归操纵列表的程序例子了。我们最感兴趣的事情之一是,某个对象是否是列表中的元素。所以,我们想写一个程序,当假设输入是一个对象X和一个列表L,

    得出结果是X是否属于L。这个程序的名字通常是:member,是Prolog程序中使用递归操纵列表最简单的例子,如下:

     member(X, [X|T]).

     member(X, [H|T]) :- member(X, T).

     这就是全部的代码:一个事实(即member(X, [X|T]))和一个规则(即,member(X, [H|T]) :- member(X, T))。但是请注意这个规则是递归的(因为函子:member同时出现在规则的头部和

    主干),它能够解释为什么这么短的程序就能够达到要求,让我们进一步分析。

     首先我们从声明性方面解读这段程序。通过这种解读,会发现它是完全有意义的。第一个子句(即事实)简单地说:如果对象X是一个列表的头部,那么X就是列表的元素。请注意我们使用了

    内置的“|”操作符去操作列表。

     那么第二个递归的子句呢?它的含义是:如果对象X是一个列表尾部的元素,那么它同时也是这个列表的元素。同样注意使用“|”操作符操作列表。

     现在,很明显这个定义具备很好的声明性含义。但是这个程序是否会像它声明性含义一样的去运行?即,程序是否真的能够求出一个对象X是否是一个列表L的元素?如果是,是如何做到的?

    为了回答这些问题,我们需要思考程序性的含义。让我们通过以下简单的列子得出结果。

      假设我们进行如下的查询:

       ?- member(yolanda, [yolanda, trudy, vincent, jules]).

     Prolog会立即回答true。为什么?因为程序通过第一个子句(即事实),将yolanda和X合一,所以答案就立即给出。

     接下来,思考下面的查询:

       ?- member(vincent, [yolanda, trudy, vincent, jules]).

     现在第一个子句无法提供答案(vincent和yolanda是不同的原子),所以Prolog继续使用第二个递归规则的子句,它会给出新的目标:

       member(vincent, [trudy, vincent, jules]).

     现在第一个子句再一次无法提供答案,所以Prolog继续使用第二个递归规则的子句,它又给出新的目标:

       member(vincent, [vincent, jules]).

     这一次,第一个子句会提供答案,查询也就成功了。

     到此为止一切都还好,但是我们需要问一个重要的问题,如果我们进行的查询失败了会发生什么?比如,如果进行查询:

       member(zed, [yolanda, trudy, vincent, jules]).

     现在,这个查询明显会失败(毕竟,zed不在列表中)。所以Prolog会如何处理?特别地,我们如何确认Prolog会终止,并且返回false,而不是陷入递归无限循环中?

     让我们通过系统性思考来解答这个问题。又一次地,第一个子句无法提供答案,所以Prolog使用递归规则,会给出新的目标:

      member(zed, [trudy, vincent, jules])

     同样地,第一个子句无法提供答案,所以Prolog使用递归规则并且给出新的目标:

       member(zed, [vincent, jules])

     同样地,第一个子句无法提供答案,所以Prolog使用递归规则并且给出新的目标:

       member(zed, [vincent])

     现在第一个子句还是无法提供答案,所以Prolog使用递归规则并且给出新的目标:

       member(zed, [])

     现在到了有趣的时候,明显地第一个子句还是无法提供答案。但是请注意:递归规则也无法起作用了,为什么?很简单:递归规则依赖于将列表分解为头部和尾部,正如我们之前看到的,

    空列表是无法再根据这种方式拆分的。所以递归规则不能再起作用了,所以Prolog停止了进一步搜索解决方案,并且报告false。即,Prolog告诉我们zed不属于这个列表,也正是我们期望

    的答案。

     我们可以对member/2这个谓词逻辑进行总结:它是一个递归的谓词逻辑,会系统地遍历整个列表去搜索答案。它通过将列表分解为更小的列表,并且搜索每一个更小列表的头部去进行匹

    配。这种方法会导致搜索是递归的,而且因为这种递归是安全的(因为,不会陷入无限循环),最终Prolog必须涉及到空列表。空列表无法再分解为更小的列表,所以就可以结束掉递归。

     好了,我们现在已经完全明白member/2的工作原理了,但是事实上,这个谓词逻辑远比之前的例子有用。之前我们只是通过它回答一些true/false的例子,但是我们能够在查询中使用变量,

    比如,如果我们进行查询:

       ?- memeber(X, [yolanda, trudy, vincent, jules]).

       X = yolanda;

       X = trudy;

       X = vincent;

       X = jules;

       false

     即,Prolog已经告诉我们一个列表的每一个元素,这是member/2一个特别普通的使用方式。本质上讲,通过变量,我们可以问Prolog:“快!给我这个列表的一些元素!”。在许多的

    应用中我们需要从列表中获取元素,这种就是典型的方法。

     最后提及一下,上述我们定义member/2的方式是正确的,但是有点冗余。

     试想一下,第一个子句是处理列表的头部,虽然列表尾部在第一个子句中是无关的,但是我们依然使用了变量T对其进行合一。类似地,递归规则中处理的是列表的尾部,虽然列表的头部

    无关的,但是我们依然使用了变量H对其进行合一。这些不必要的变量可以被剔除:如果谓词逻辑的定义集中我们关注的概念,而无关的信息使用匿名变量处理,是一种更好的定义方式。

    比如,我们对member/2进行如下的重构:

       member(X, [X | _]).

       member(X, [_ | T]) :- member(X, T).

     这个版本从声明性和程序性上都是和第一个版本相同的。但是这个版本定义更加清晰:当你阅读的时候,你会集中在问题的本质上。

  • 相关阅读:
    汇编基础概念
    linux实验小结
    乐视三合一体感摄像头开发记录
    IplImage* 格式与Mat 格式的转换
    STM32关于优先级设定的理解 NVIC_SetPriority()
    "IRQn_Type" is undefined
    串口通信实验编译没有错误,但是串口调试助手收不到数据
    SecureCRT 8版本 自用备份
    Matlab使用robot Toolbox
    开篇
  • 原文地址:https://www.cnblogs.com/seaman-h-zhang/p/4638258.html
Copyright © 2020-2023  润新知