Reversible tree length relation
Asked Answered
J

2

5

I'm trying to write reversible relations in "pure" Prolog (no is, cut, or similar stuff. Yes it's homework), and I must admit I don't have a clue how. I don't see any process to create such a thing.

We are given "unpure" but reversible arithmetic relations (add,mult,equal,less,...) which we must use to create those relations.

Right now I'm trying to understand how to create reversible functions by creating the relation tree(List,Tree) which is true if List is the list of the leaves of the binary tree Tree.

To achieve such a thing I'm trying to create the tree_size(Tree,N) relation which is true when Tree has N leaves. Here is my naïve, un-reversible relation:

tree_len(n(_,leaf,leaf),1).
tree_len(n(op,G,D),N) :-
    tree_len(G,TG),
    tree_len(D,TD),
    add(TG,TD,N).

I can do the query tree_len(some tree, N), but not, say, tree_len(X,3), so it's not reversible. So far I've tried a few things, but I must admit that I feel discouraged as I do not know where and what to look for. Is there actually a way to do this?

Jollification answered 30/12, 2012 at 23:30 Comment(0)
W
9

Reason for non-termination

First, let us try to understand why your definition is not reversible. I will use a failure-slice to better explain it. So consider the query tree_len(X,1). At first sight, everything is perfect, you get even a nice answer!

?- tree_len(T,1).
   T = n(_A,leaf,leaf)
;  ... .

But never ask for another answer, for it will loop:

?- tree_len(T,1).
   T = n(_A,leaf,leaf)
;  loops.

So the answer we got was a bit of a distraction. It at first glance looked like everything was OK, and only on backtracking the real problem was encountered. This is something to get accustomed to in Prolog. Evidently, your query using 3 was better chosen. But that was luck.

There is an easy way to ensure this in general. Simply add the extra goal false to the query. Adding false means, that we are no longer interested in any answers, since it can no longer succeed. In this manner all distraction is removed, and we face the problem directly:

?- tree_len(T,1), false.
   loops.

So, where did this loop come from?

In pure, monotonic Prolog programs (such as this one), we can localize a reason for non-termination by adding some goals false into our program. If the resulting program (called failure-slice) does not terminate, then the original program does not terminate either. This is the minimal failure-slice for our query:

?- tree_len(T,1), false.

tree_len(n(_,leaf,leaf),1) :- false.
tree_len(n(op,G,D),N) :-
    tree_len(G,TG), false,
    tree_len(D,TD),
    N is TG+TD.

There is not much left of our program! It is this tiny fragment that is responsible for the non-termination. If we want to fix the problem, we have to do something in that tiny part. Everything else is futile.

So what we need to do is to somehow change the program such that this fragment no longer loops.

Actually, we have two choices, we could use successor arithmetics or constraints like clpfd. The former is available in any Prolog system, the latter is provided only in some like SWI, YAP, SICStus, GNU, B.

Using successor-arithmetics

Now, 3 is represented by s(s(s(0))).

tree_lensx(T, s(N)) :-
   tree_lendiff(T, N,0).

tree_lendiff(n(_,leaf,leaf), N,N).
tree_lendiff(n(op,G,D), s(N0),N) :-
   tree_lendiff(G, N0,N1),
   tree_lendiff(D, N1,N).

I used here several common coding techniques.

Differences

The actual relation is tree_lendiff/3 which represents a natural number not by one argument, but rather using two. The actual number is the difference between both. In this manner it is possible to retain the definition reversible.

Avoiding left-recursion

The other technique was to avoid left-recursion. The length described by tree_lendiff/3 actually is the length minus one. Remember the failure-slice we got first? The very same failure-slice would be present here too! However by "shifting" the length by one, the head of the recursive rule now ensures termination.

Using library(clpfd)

Originally, constrains over finite domains were developed to solve combinatorial problems. But you can use them also to get reversible arithmetics. The implementation in SWI and YAP even goes that far that it compiles into code which often equals in efficiency to the traditional non-reversible (is)/2 while still being reversible.

:- use_module(library(clpfd)).

tree_fdlen(n(_,leaf,leaf),1).
tree_fdlen(n(op,G,D),N) :-
   N #= TG+TD,
   TG #>= 1,
   TD #>= 1,
   tree_fdlen(G,TG),
   tree_fdlen(D,TD).

This program more closely corresponds to your original definition. Nevertheless, remark the two goals TG #>= 1 and TD #>= 1 which added redundant information to ensure the termination of this program.

We can now enumerate all trees of a certain range like so:

?- Size in 0..4, tree_fdlen(T, Size).
   Size = 1, T = n(_A,leaf,leaf)
;  Size = 2, T = n(op,n(_A,leaf,leaf),n(_B,leaf,leaf))
;  Size = 3, T = n(op,n(_A,leaf,leaf),n(op,n(_B,leaf,leaf),n(_C,leaf,leaf)))
;  Size = 4, T = _ % omitted
;  Size = 4, T = _ % omitted
;  Size = 3, T = _ % omitted
;  Size = 4, T = _ % omitted
;  Size = 4, T = _ % omitted
;  Size = 4, T = _ % omitted
;  false.

Note the exact order of the answer substitutions! It's not just going 1,2,3,4! Rather, answers are found in some order. It does not matter which one, as long as we are only interested in finding all solutions!

Washing answered 31/12, 2012 at 2:18 Comment(2)
Thanks! This really helped me understand my error. I still have one little question, is the general rule for achieving reversibility to avoid potentially infinitely generative clauses, or to let them in and then "build out" of them?Jollification
@Manux: You cannot avoid infinitely generative clauses, if the set of solutions is infinite and can only be described by infinitely many answers. So there is no way around such clauses. A general rule how to approach them is difficult to formulate. I would rather go with failure-slices as a way to root out hopeless cases rapidly and let your intuition do the rest. You may also look at other cases for a failure-slice. Like this oneWashing
H
1

Interesting problem.

Here's what I would do. Basically, your relation isn't reversible since add/3 isn't. What I essentially did was, replace counting with numbers with counting with lists of a size that corresponds to the number of leaves - which is reversible (well, append/3 and length/2 are reversible).

Is this something like what you need? The posted code is runnable under YAP.

PS: this might not be the most concise solution, but it's from the top of my head. I'll try to help if you have any further questions.

:-  use_module(library(lists)).

do_tree_len(n(_,leaf,leaf), [X]).
do_tree_len(n(op,G,D), [X1,X2|T]) :-
    append(TG, TD, [X1,X2|T]),
    TG \= [X1,X2|T], % To prevent infinite loops, when TG or TD is []
    TD \= [X1,X2|T],
    do_tree_len(G, TG),
    do_tree_len(D, TD).

tree_len(Tree, N):-
    length(L, N),
    do_tree_len(Tree, L).
Hinduism answered 31/12, 2012 at 0:44 Comment(3)
The problem I see with this solution is that querying tree_len of an existing tree will give the good answer, but will then loop if asked again.Jollification
You say that the problem is add/3. But this is not true: Even if add/3 is reversible (as when you use TG+TD#=N) you still have termination problems.Washing
@Washing thanks for pointing that out. I enjoyed also reading your answer :).Hinduism

© 2022 - 2024 — McMap. All rights reserved.