使谓词可逆

Make a predicate reversible

我是序言新手;我来自结构化编程背景,这一点很明显:)

我正在构建一个涉及反转数字的序言查询;例如。 reverse_num(123,X) 结果为 X = 321。我想出了以下定义,但只有当我提供一个数字作为第一个参数时它才有效。

reverse_num(Num, Revnum) :-
  number_chars(Num, Atoms),
  reverse(Revatoms, Atoms),
  number_chars(Reversed, Revatoms),
  Reversed = Revnum.

如果我这样做,number_chars/2 谓词不喜欢未经证实的变量:reverse_num(X,123)(我期望 X321)。

我是不是太努力让 reverse_num 做一些它不应该做的事情(是否应该理解为只使用数字作为第一个参数,变量作为第二个参数)?

或者是否有一种简单/直接的方法将变量作为第一个参数处理?

number_chars/2 谓词具有签名:

number_chars(?Number, ?CharList) 

但是虽然签名没有完全指定,但至少Number或者CharList是要实例化的。这就是错误发生的地方。

如果您致电:

reverse_num(Num,123)

您将调用 number_chars/2 时两者都未实例化,因此谓词会出错。

问题的一个不太好的解决方案是询问 NumRevNum 是否是 number/2。您可以通过编写两个版本来做到这一点。它还会过滤其他调用,如 reverse_num(f(a),b) 等:

reverse_num(Num,Revnum) :-
    \+ number(Num),
    \+ number(Revnum),
    throw(error(instantiation_error, _)).

reverse_num(Num, Revnum) :-
    ground(Num),
    number(Num),
    !,
    number_chars(Num, Atoms),
    reverse(Revatoms, Atoms),
    number_chars(Revnum, Revatoms).

reverse_num(Num, Revnum) :-
    ground(Revnum),
    number(Revnum),
    reverse_num(Revnum,Num).

或者如果你使用两个非基础(例如 reverse_num(X,Y).)一个实例化错误而不是 false 正如@false 所说:

reverse_num(Num,Revnum) :-
    \+ number(Num),
    \+ number(Revnum),
    !,
    throw(error(instantiation_error, _)).

reverse_num(Num, Revnum) :-
    number(Num),
    !,
    number_chars(Num, Atoms),
    reverse(Revatoms, Atoms),
    number_chars(Revnum, Revatoms).

reverse_num(Num, Revnum) :-
    reverse_num(Revnum,Num).

剪切 (!) 在行为上不是必需的,但会稍微提高性能。我不是这个实现的真正粉丝,但是 Prolog 不能总是完全使谓词可逆,因为 (a) 可逆性是 undecidable 属性 因为 Prolog 是图灵完备的; (b) Prolog 的特征之一是体原子的计算从左到右。否则评估某些程序将需要很长时间。有逻辑引擎可以按任意顺序执行此操作,因此将成功完成此任务。

如果 predicate/2 是可交换的,可以推广的解决方案是以下模式:

predicate(X,Y) :-
    predicate1(X,A),
    predicate2(A,B),
    % ...
    predicaten(C,Y).
predicate(X,Y) :-
    predicate(Y,X).

但是你不能简单地将最后一个子句添加到理论中,因为它可以无限循环。

很高兴看到有人也担心在绑定参数集中定义没有限制的灵活规则。

如果使用支持协程 when/2 内置谓词(例如 SICStus Prolog、SWI-Prolog 或 YAP)的 Prolog 系统,请尝试:

reverse_num(Num, Reversed) :-
  when( ( ground(Num); ground(Atoms) ), number_chars(Num, Atoms) ),
  when( ( ground(Reversed); ground(Revatoms) ), number_chars(Reversed, Revatoms) ),
  reverse(Atoms , Revatoms).

给出:

?- reverse_num( 123, X ).
X = 321.

?- reverse_num( X, 123 ).
X = 321 .

(感谢提供论文答案的人:

关系命名

在开始编码之前,让我们先退一步。毕竟,Prolog 中的思想是定义关系。你的名字 reverse_num/2 更像是一些行动的建议,num_reversed/2 可能是一个更好的名字。

确定关系

你的定义还不错,我改写成1:

num_reversed(Num, Reversed) :-
   number_chars(Num, Chars),
   reverse(Chars, Revchars),
   number_chars(Reversed, Revchars).

?- num_reversed(123,X).
X = 321.

?- num_reversed(1230,X).
X = 321.

?- num_reversed(12300,X).
X = 321.

你看到规律了吗?所有数字 N*10^I 结果相同!

现在,让我们再问一些:

?- num_reversed(Num, 321).
ERROR: number_chars/2: Arguments are not sufficiently instantiated

嗯,我们期待什么?实际上,我们希望打印所有 123*10^I。这是无限多的解决方案。所以上面的查询,如果正确回答,将需要打印无限多的解决方案。如果我们直接打印它们,那将花费我们整个宇宙的生命,甚至更多!

正是由于这个原因,Prolog 产生了一个实例化错误。通过这个,Prolog 基本上声明:

This goal is too general that I can make a good answer. Maybe there are infinitely many solutions, maybe not. I know not. But at least I indicate this by issuing an error. To remove this error you need to instantiate the arguments a bit more.

所以 Prolog 产生的答案根本那么糟糕!事实上,产生一个干净的错误比错误地失败要好得多。通常,Prolog 的错误通常是对您可能遇到的语义问题的非常有用的提示。见 all error classes 如何。

协程

正如其他答案所建议的那样,协程,使用 when/2 可能会解决这个问题。然而,协程本身有很多语义问题。并非没有理由,像 XSB 这样的系统不提供它,因为与包含检查相关的许多问题。与其兼容的实现会出乎意料地低效。

但为了这一点,我们可以通过像

这样查询来使我们的定义更加通用
 ?- when(nonvar(Num), num_reversed(Num, Reversed)).
 when(nonvar(Num), num_reversed(Num, Reversed)).

现在我们得到的答案正是我们输入的查询。这也被称为挣扎。所以 一种以紧凑的方式表示无限可能解决方案的方法!然而,这要付出相当高的代价:您不再知道解决方案是否存在。想到:

?- when(nonvar(Num), num_reversed(Num, -1)).
when(nonvar(Num), num_reversed(Num, -1)).

其他人也建议等待 nonvar(Reversed),这只有在我们产生无限多的答案时才是正确的 - 但是,正如我们所见 - 这会花费太多时间。

协程在 80 年代初看起来是一条非常有前途的道路。然而,它从未真正成为一种通用的编程方法。大多数时候你会遇到太多的困难,这只是一种痛苦,甚至比实例化错误更难处理。

然而,这种发展的更有前途的后代是制约因素。在那里,机制定义得更清晰。出于实际目的,程序员将只使用现有的库,如 CLPFD、CLPQ 或 CHR。实现您自己的库本身就是一个非常重要的项目。事实上,甚至可以使用 library(clpfd) 提供 num_reversed/2 的实现,即将关系限制为整数情况。

模式相关条件

传统上,许多此类问题都是通过显式测试实例化来解决的。像 when/2- other type test predicates lead easily to errors as exemplified by .

中的条件一样,只使用 nonvar/1ground/1 执行此操作是一种很好的风格
num_reversed(Num, Reversed) :-
   (  nonvar(Num)
   -> original_num_reversed(Num, Reversed)
   ;  original_num_reversed(Reversed, Base),
      (  Base =:= 0
      -> Num is 0
      ;  length(_, I),
         Num is Base*10^I
      )
   ).

对于使用基数 2 的浮点数,上面的代码很快就会中断,而对于基数 10 则稍晚一些。事实上,对于经典的基数 2 浮点数,关系本身没有多大意义。

至于number_chars/2的定义,ISO/IEC13211-1:1995有如下template and mode subclause

8.16.7.2 Template and modes

number_chars(+number, ?character_list)
number_chars(-number, +character_list)

第一种情况是第一个参数被实例化(因此nonvar)。第二种情况,当第一个参数未被实例化时。在这种情况下,必须实例化第二个参数。

但是请注意,由于非常相似的问题,number_chars/2 不是关系。例如,Chs = ['0','0'], number_chars(0, Chs) 成功,而 number_chars(0, Chs), Chs = ['0','0'] 失败。

印刷精美

1 这种重写是必要的,因为在许多序言中 reverse/2 只有在第一个参数已知时才会终止。在 SWI 中,由于一些特殊的低效率,这种重写是必要的。

这个SWISH session显示我努力回答。

然后我回到了这里,我发现自己处于@PasabaPorAqui 的心情 (+1),但我没弄对。

但是,这样一个有趣的话题:请注意连接模式的规律性。

reverse_num(X, Y) :-
    when((nonvar(Xs);nonvar(Ys)), reverse(Xs, Ys)),
    when((nonvar(X) ;nonvar(Xs)), atomic_chars(X, Xs)),
    when((nonvar(Y) ;nonvar(Ys)), atomic_chars(Y, Ys)).

所以,我们可以用一种简单的方式概括(在考虑 PasabaPorAqui 校正后,ground/1 这是关键):

% generalized... thanks Pasaba Por Aqui
:- meta_predicate when_2(0).
when_2(P) :-
    strip_module(P,_,Q),
    Q =.. [_,A0,A1],
    when((ground(A0);ground(A1)), P).

reverse_num(X, Y) :-
    maplist(when_2, [reverse(Xs, Ys), atomic_chars(X, Xs), atomic_chars(Y, Ys)]).

我想我理解为什么 nonvar/1 有问题:绑定到反向的列表 'fired' 太早了,当头被绑定时.. .太快了!

maplist/2 并不是必须的:我们可以手写

reverse_num(X, Y) :-
    when_2(reverse(Xs, Ys)),
    when_2(atomic_chars(X, Xs)),
    when_2(atomic_chars(Y, Ys)).

这似乎是术语重写的理想应用...您如何看待 -:-?实现我们可以编写像

这样的双向代码
reverse_num(X, Y) -:-
    reverse(Xs, Ys),
    atomic_chars(X, Xs),
    atomic_chars(Y, Ys).

编辑 SWISH 可能不 'term_rewrite' 友好...所以这是一个较低级别的方法:

:- op(900, xfy, ++).
A ++ B ++ C :- when_2(A), B ++ C.
A ++ B :- when_2(A), when_2(B).

reverse_num(X, Y) :- 
    reverse(Xs, Ys) ++ atomic_chars(X, Xs) ++ atomic_chars(Y, Ys).

抛开尾随零变成前导零的问题,它似乎不应该比这样的事情复杂得多(通过处理负数变得更加复杂):

reverse_number(X,Y) :- number(X) , ! , rev(X,Y) .
reverse_number(X,Y) :- number(Y) , ! , rev(Y,X) .

rev(N,R) :-
  N < 0 ,
  ! ,
  A is abs(N) ,
  rev(A,T) ,
  R is - T
  .
rev(N,R) :-
  number_chars(N,Ns) ,
  reverse(Ns,Rs) ,
  number_chars(R,Rs)
  .

请注意,这确实需要至少实例化 reverse_number/2 的参数之一。