Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Defining "let expressions" in Prolog

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?

like image 596
Anderson Green Avatar asked Oct 15 '20 16:10

Anderson Green


Video Answer


2 Answers

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
               )).
like image 200
jnmonette Avatar answered Oct 22 '22 06:10

jnmonette


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}.
like image 2
David Tonhofer Avatar answered Oct 22 '22 07:10

David Tonhofer