Defining “let expressions” in Prolog

醉酒当歌 提交于 2020-12-30 17:27:51

问题


In many functional programming languages, it is possible to "redefine" local variables using a let expression:

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

I couldn't find a built-in Prolog predicate for this purpose, so I tried to define a let expression in this way:

:- 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)
                ))
            ))
        )).

This implementation appears to incorrect, since some of the variables are bound before being replaced. I want to define an expression that would allow more than one variable to be "redefined" simultaneously:

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

Is it possible to implement a let expression in a way that allows several variables to be redefined at the same time?


回答1:


let is essentially a way of creating (inline to the source) a new, local context in which to evaluate functions (see also: In what programming language did “let” first appear?)

Prolog does not have "local contexts" - the only context is the clause. Variables names are only valid for a clause, and are fully visible inside the clause. Prolog is, unlike functional programs, very "flat".

Consider the main:

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

Context being clauses, this is essentially "wrong pseudo code" for the following:

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.

The let doesn't really bring much to the table here. It seems to allow one to avoid having to manually relabel variables "on the right" of the is, but that's not worth it.

(Now, if there was a way of creating nested contexts of predicates to organize code I would gladly embrace that!).


Let's probe further for fun (and because I'm currently trying to implement the Monad Idiom to see whether that makes sense).

You could consider creating an explicit representation of the context of variable bindings, as if you were writing a LISP interpreter. This can be done easily with SWI-Prolog dicts, which are just immutable maps as used in functional programming. Now note that the value of a variable may become "more precise" as computation goes on, as long as it has any part that is still a "hole", which leads to the possibility of old, deep contexts getting modified by a current operation, not sure how to think about that.

First define the predicate to generate a new dict from an existing one, i.e. define the new context from the old one, then the code becomes:

inc_a(Din,Din.put(a,X))   :- X is Din.a + 1.
twice_a(Din,Din.put(a,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,D2), h(D2).
h(D) :- writeln(D.a).

The A has gone inside the dict D which is weaved through the calls.

You can now write a predicate that takes a dict and the name of a context-modifying predicate ModOp, does something that depends on the context (like calling writeln/1 with the value of a), then modifies the context according to ModOp.

And then deploy foldl/4 working over a list, not of objects, but of operations, or rather, names of operations:

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

write_then_mod(ModOp,DictFromLeft,DictToRight) :-
   writeln(DictFromLeft.a),
   call(ModOp,DictFromLeft,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).

Does it work?

?- main.
1
2
4
true.

Is it useful? It's definitely flexible:

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



回答2:


The issue with defining let as a normal predicate is that you can't redefine variables that appear outside the outermost let. Here is my attempt at a more correct version, which uses goal expansion. (To me it makes sense, because as far as I know, in lisp-like languages, let cannot be defined as a function but it could be defined as a macro.)

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

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

This is reusing the replace/4 predicate defined in the question. Note also that the hook predicate differs between Prolog versions. I am using SICStus, which defines goal_expansion/5. I had a quick look at the documentation and it seems that SWI-Prolog has a goal_expansion/2.

I introduced a different syntax for multiple declarations in a single let: let((X1,X2),...) defines X1, then defines X2 (so is equivalent to let(X1,let(X2,...))), while let([X1,X2],...) defines X1 and X2 at the same time (allowing the swap example).

Here are a few example calls:

test1 :- let(A = 1,(
            print(A),nl,
            let(A is A+1,(
                print(A),nl,
                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],let([A=B,B=A],(print(A-B),nl))).

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



回答3:


This is how you would type this in using Prolog syntax:

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

If it is something else you are trying to achieve, you need to explain better. "How do I type it in Prolog" comes strictly after "What am I doing?"


Or is it that you really want this kind of syntactic nesting in Prolog? Could you provide a couple of examples where you think it is beneficial?




回答4:


It's possible to define a let predicate that recursively replaces nested let expressions, so that local variables can be "redefined" without being renamed. This is one way to implement it:

:- 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, Args0, Args),
            Term =.. [F|Args]
        ).

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

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

This implementation still doesn't work with "simultaneous" variable definitions, but the replace/2 predicate could easily be modified to replace several variables simultaneously.



来源:https://stackoverflow.com/questions/64375536/defining-let-expressions-in-prolog

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!