6

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?

Erik Kaplun
  • 37,128
  • 15
  • 99
  • 111
Anderson Green
  • 30,230
  • 67
  • 195
  • 328
  • `let a = a+1` will in most functional programming languages (like Haskell) cause an infinite loop, since the `a` in the "body" of the expression, is the same `a` in the *head* of the expression. – Willem Van Onsem Oct 15 '20 at 16:28
  • @WillemVanOnsem But it works differently in some other functional programming languages (such as Futhark), where variables can be "redefined" in this way without causing an infinite loop. – Anderson Green Oct 15 '20 at 17:17
  • 2
    I don't think this code is correct at all, even with a single redefinition. Try replacing "A is A * 2" by "A is A + 1" in the third let: You will still get "4". In the way your rewriting works, *all* occurrences of A in the first let are replaced by "1". This means that in the second let, all occurrences of *1* (because A was replaced by 1) will be replaced by 2, including the original constant 1... – jnmonette Oct 15 '20 at 17:21
  • @AndersonGreen: well it would be really *odd* especially since `futhark` is written in Haskell, and is syntactically very similar. – Willem Van Onsem Oct 15 '20 at 17:24
  • there's nothing wrong with the redefining `let` per se in a language like e.g. Scheme. it's Haskell that's an outlier, as its `let` is actually a `letrec` and the non-recursive `let` is missing. But in Prolog, there's *neither*. the Prolog way is just to use a *new* logvar, `N=1, N1=1+N, N2=2*N1` etc. better write *Prolog* in Prolog, than Scheme or Haskell. if you have a predicate that works with a logvar, just call that predicate: `N=1, pred(1+N,O1), pred(2*O1, O2)` (intending the `O` variables for the "output" i.e. the new "updated" "state"). – Will Ness Oct 15 '20 at 17:38
  • in fact, Prolog way can be said to be "state passing", precisely because it lacks notions of nested scope so the explicit state passing style as in SSA or A-normal form is practically forced on a programmer. unless of course she is willing to resort to various non-pure primitives in the language. – Will Ness Oct 15 '20 at 17:44
  • if I were to do this thing, I'd rather have it written as lists, e.g. `let( [ A // =(1) => writeln(A), A // is(A+1) => writeln(A), A // is(A*2) => writeln(A) ] ), let( [ [A // =(1), B // =(2)] => writeln([A,B]), [A // =(B), B // =(A)] => writeln([A,B]) ]).` or something (with the appropriate precedence for `=>`). – Will Ness Oct 15 '20 at 18:19
  • But why would you do this? Why nest like this when you don't have to? Why redefine names instead of using new names? – TA_intern Oct 16 '20 at 07:02
  • @TA_intern where you addressing me, or the OP? the only advantage with the format I proposed is the possibility to swap the order of code lines inside it easily. With the explicit naming we'd also have to rename all the numbered vars consistently, and that could be error prone. – Will Ness Nov 03 '20 at 11:49
  • @WillNess I was addressing OP. I still think the question is misguided and don't like any of the solutions. I might ask a question and answer it myself; the answer will probably suggest a mechanical re-writing (either manually or using compile-time expansion) to just start new syntactic scope (ie a new predicate) for achieving roughly the same. To me it seems OP is trying to shoehorn a _syntactic_ mechanism into Prolog syntax with no apparent benefits. – TA_intern Nov 03 '20 at 12:45

4 Answers4

3

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
               )).
jnmonette
  • 1,794
  • 4
  • 7
2

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}.
David Tonhofer
  • 14,559
  • 5
  • 55
  • 51
  • My `let/2` predicate would probably work as intended if it recursively replaced all of the nested `let/2` clauses before calling them. Since my original implementation of `let/2` binds the variables before renaming them, it gives incorrect results. – Anderson Green Oct 15 '20 at 19:12
  • 1
    when we manually label the variables we then can't move the lines around freely without renaming the vars. in that sense having this `let` construct could be worth it after all. – Will Ness Oct 15 '20 at 19:51
0

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?

TA_intern
  • 2,222
  • 4
  • 12
  • [Let expressions](https://en.wikipedia.org/wiki/Let_expression) can be useful when translating imperative programs to declarative languages, since they allow local variables to be "redefined" without being renamed. – Anderson Green Oct 16 '20 at 20:20
  • @AndersonGreen I still think it is much easier to just start a new syntactic scope. You don't mean that you translate the program by hand, right? Somehow mechanically instead? I still do not understand why you need to translate the _technique_ to Prolog, instead of the program. – TA_intern Oct 17 '20 at 16:27
  • I don't know if it's possible to define a "new syntactic scope" in a predicate without implementing let-expressions. Since I am writing an interpreter for imperative programs in Prolog, it is necessary to "redefine" the local variables without manually renaming them. – Anderson Green Oct 17 '20 at 18:08
  • @AndersonGreen I know that _that_ kind of feedback is not really welcome, but I will try it anyway. I do not see at all why it is *necessary* to redefine local variables without renaming them (manually!). If there is a particular procedural construct that you need to represent in Prolog, maybe you are solving the wrong problem. You can (surprisingly easily) implement a "C machine" or a "Lisp machine" in Prolog and just feed it source in that language. On the other end, you can properly re-write the code using conventional Prolog constructs. – TA_intern Oct 19 '20 at 13:19
0

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.

Anderson Green
  • 30,230
  • 67
  • 195
  • 328
  • 1
    you have `let` in your interpreted language. why complicate things and introduce it into Prolog? re-write the interpreted code as SSA / A-normal form, and it fits naturally, because it's the nature of Prolog. – Will Ness Nov 03 '20 at 14:45