在Prolog中定义“让表达式”

问题描述

在许多函数式编程语言中,可以使用let表达式“重新定义”局部变量:

let example = 
    let a = 1 in
        let a = a+1 in
            a + 1

为此,我找不到内置的Prolog谓词,因此我尝试以这种方式定义let表达式:

:- initialization(main).
:- set_prolog_flag(double_quotes,chars).

replace(Subterm0,Subterm,Term0,Term) :-
        (   Term0 == Subterm0 -> Term = Subterm
        ;   var(Term0) -> Term = Term0
        ;   Term0 =.. [F|Args0],maplist(replace(Subterm0,Subterm),Args0,Args),Term =.. [F|Args]
        ).

let(A,B) :-
    ((D,D1) = (A1 is B1,C is B1);
    (D,D1) = (A1=B1,C=B1)),subsumes_term(D,A),D=A,replace(A1,C,B,B2),call((D1,B2)).

main :- let(A = 1,(
            writeln(A),let(A is A+1,(
                writeln(A),let(A is A * 2,(
                    writeln(A)
                ))
            ))
        )).

此实现似乎不正确,因为某些变量在替换之前已绑定。我想定义一个表达式,该表达式允许同时“重新定义”多个变量:

main :- let((A = 1,B = 2),% this will not work with the let/2 predicate that I defined
            let((A=B,B=A),writeln(B)
            ))  
        ).

是否可以以允许同时重新定义多个变量的方式实现let表达式?

解决方法

let本质上是一种(在源代码中)创建一个新的本地上下文以评估函数的方法(另请参见:In what programming language did “let” first appear?

Prolog没有“本地上下文”-唯一的上下文是子句。变量名称仅对子句有效,并且在子句中完全可见。与功能程序不同,Prolog非常“扁平”。

考虑main

main :- let(A = 1,(
            writeln(A),let(A is A+1,(
                writeln(A),let(A is A * 2,(
                    writeln(A)
                ))
            ))
        )).

上下文是子句,对于以下情况,这本质上是“错误的伪代码”:

main :- f(1).
f(A) :- writeln(A),B is A+1,g(B).
g(A) :- writeln(A),B is A*2,h(B).
h(A) :- writeln(A).
?- main.
1
2
4
true.

let在这里并没有带来太多好处。似乎可以避免在is的右侧“手动”重新标记变量,但这是不值得的。

(现在,如果有一种方法可以创建谓词的嵌套上下文来组织代码,我很乐意接受!)。


让我们进一步探索乐趣(因为我目前正在尝试实现Monad Idiom,以查看是否有意义)。

您可以考虑创建变量绑定上下文的显式表示形式,就像在编写LISP解释器一样。使用SWI-Prolog dicts可以轻松完成此操作,SWI-Prolog foldl/4只是函数编程中使用的不可变映射。现在请注意,变量的值可能会随着计算的进行而变得“更精确”,只要它的任何部分仍然是“空洞”,就可能导致旧的深层上下文被当前操作修改,不知道该怎么想。

首先定义谓词以从现有的谓词生成新的dict,即从旧的谓词定义新的上下文,然后代码变为:

inc_a(Din,Din.put(a,X))   :- X is Din.a + 1.
twice_a(Din,X)) :- X is Din.a * 2.

main :- f(_{a:1}).
f(D) :- writeln(D.a),inc_a(D,D2),g(D2).
g(D) :- writeln(D.a),twice_a(D,h(D2).
h(D) :- writeln(D.a).

A进入了通过调用编织的字典D内。

您现在可以编写一个带字典和谓词名称的谓词 上下文修改谓词ModOp,根据上下文执行某些操作(例如使用值为writeln/1调用a),然后修改 ModOp中的上下文。

然后部署perldoc perlrun: how to execute the Perl interpreter: command line switches处理列表,而不是对象列表,而是操作列表,或更确切地说是操作名称:

inc_a(Din,X)) :- X is Din.a * 2.
nop(Din,Din).

write_then_mod(ModOp,DictFromLeft,DictToRight) :-
   writeln(DictFromLeft.a),call(ModOp,DictToRight).

main :- 
   express(_{a:1},[inc_a,twice_a,nop],_DictOut).

express(DictIn,ModOps,DictOut) :-
   foldl(
      write_then_mod,% will be called with args in correct order
      ModOps,DictIn,DictOut).

行得通吗?

?- main.
1
2
4
true.

有用吗?绝对灵活:

?- express(_{a:1},inc_a,_DictOut).
1
2
4
8
9
_DictOut = _9368{a:9}.
,

let定义为常规谓词的问题在于,您无法重新定义最外面的let之外的变量。这是我尝试使用目标扩展的更正确版本的尝试。 (对我来说,这很有意义,因为据我所知,在类似Lisp的语言中,let不能定义为函数,而可以定义为宏。)

%goal_expansion(let(Decl,OriginalGoal),Goal) :- %% SWI syntax
goal_expansion(let(Decl,_M,_,Goal,[]) :- %%SICStus syntax 
        !,expand_let(Decl,OriginalGoal,Goal).
        
expand_let(X,Goal) :-
        var(X),!,replace(X,_Y,NewGoal),Goal=(true,NewGoal).        
expand_let(X is Decl,Y,Goal=(Y is Decl,NewGoal).
expand_let(X = Decl,Goal=(Y = Decl,NewGoal).
expand_let([],Goal) :-
        !,Goal=OriginalGoal.
expand_let([L|Ls],expand_let_list([L|Ls],InitGoals,Goal=(InitGoals,NewGoal).
expand_let((L,Ls),expand_let(Ls,SecondGoal),expand_let(L,SecondGoal,Goal).

expand_let_list([],true,Goal).
expand_let_list([L|Ls],(Init,InitGoals),NewGoal):-
        (
          var(L)
        ->
          replace(L,Init=true
        ;
          L=(X=Decl)
        ->
          replace(X,Init=(Y=Decl)
        ;
          L=(X is Decl)
        ->
          replace(X,Init=(Y is Decl)
        ),expand_let_list(Ls,NewGoal).

这将重用问题中定义的replace/4谓词。还要注意,钩子谓词在Prolog版本之间是不同的。我正在使用SICStus,它定义了goal_expansion/5。我快速浏览了一下文档,似乎SWI-Prolog中有一个goal_expansion/2

我为单个let中的多个声明引入了不同的语法:let((X1,X2),...)定义了X1,然后定义了X2(因此等效于let(X1,let(X2,...))) ,而let([X1,X2],...)同时定义X1X2(允许使用交换示例)。

以下是一些示例调用:

test1 :- let(A = 1,(
            print(A),nl,(
                print(A),let(A is A + 1,(
                    print(A),nl
                ))
            ))
        )).

test2 :- A=2,let([A=B,B=A],(print(B),nl)).

test3 :- A=1,let((
                    A is A * 2,A is A * 2,A is A * 2
                  ),(
                      print(A),nl
                    )),print(A),nl.

test4 :- let([A=1,B=2],(print(A-B),nl))).

test5 :- let((
               [A=1,[A=B,B=A]
             ),(
                 print(A-B),nl
               )).
,

这是您使用Prolog语法键入的方式:

example(X,Y) :-
    X = 1,succ(X,Y).

如果您要实现其他目标,则需要更好地说明。严格在“我在做什么?”之后出现“我如何在Prolog中键入它”?


还是您真的想要在Prolog中进行这种语法嵌套?您能提供一些您认为有益的示例吗?

,

可以定义一个let谓词,该谓词以递归方式替换嵌套的let表达式,以便可以在不重命名的情况下“重新定义”局部变量。这是实现它的一种方法:

:- initialization(main).
:- set_prolog_flag(double_quotes,chars).

replace(Subterm0,Subterm,Term0,Term) :-
        (   Term0 == Subterm0 -> Term = Subterm
        ;   var(Term0) -> Term = Term0
        ;   Term0 =.. [F|Args0],maplist(replace(Subterm0,Subterm),Args0,Args),Term =.. [F|Args]
        ).

replace_let(Term0,Term) :-
        (   [Term0,Term1] = [A,(A2 is B1,C2)],(Pattern = (A1 is B1);Pattern = (A1 = B1)),P1 = let(Pattern,C1),subsumes_term(P1,A),P1=A,replace(A1,A2,C1,C2),replace_let(Term1,Term)
        ;   var(Term0) -> Term = Term0
        ;   Term0 =.. [F|Args0],maplist(replace_let,Term =.. [F|Args]
        ).

let(A,B) :- replace_let(let(A,B),C),call(C).

main :-
    B = 3,let(A is B+1,(
        writeln(A),C is A + 1,let(A = C,(
                writeln(A)
            ))
        ))
    )).

此实现仍然不能与“同时”变量定义一起使用,但是可以很容易地修改replace/2谓词以同时替换多个变量。