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?
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(_G267, leaf, leaf)
But never ask for another answer, for it will loop:
?- tree_len(T,1).
T = n(_G267, 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, ... ;
Size = 4, ... ;
Size = 3, ... ;
Size = 4, ... ;
Size = 4, ... ;
Size = 4, ... ;
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!
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).