I always prefer declarative predicates (if possible) because:
- they force my mind to think about the problem in a different, better, way (this is the most important reason)
- They are easier to read and understand when I read them afterwards.
- They are usually more efficient.
So I was making a little exercise to describe a binary tree using declarative predicates. The first try was:
btree(bt(A,B)) :-
( btree(A); leaf(A) ),
( btree(B); leaf(B) ).
leaf(l(_)).
It works great for grounded queries (not that useful).
I also want the predicate to be able to answer the most general query, namely btree(T)
.
Obviously the above will not terminate.
One solution
So my first thought went to tabling, but tabling won’t terminate either in this case (for the most general query), because tabling needs a finite set of answers.
Okay, so what is the real problem? The problem is that we have an infinite recursion which doesn’t end. This brought me to think about call_with_depth_limit/3
and this solution:
depth(4). %This is just for testing, can be replaced by a complex algorithm.
btree(T) :-
depth(D),
call_with_depth_limit(btree_(T),D,_).
btree_(bt(A,B)) :-
( btree_(A); leaf(A) ),
( btree_(B); leaf(B) ).
leaf(l(_)).
This works great, and it answers the most general query:
12 ?- btree(T).
T = bt(bt(bt(l(_7898), l(_7902)), bt(l(_7912), l(_7916))), bt(bt(l(_7932), l(_7936)), bt(l(_7946), l(_7950)))) ;
T = bt(bt(bt(l(_7898), l(_7902)), bt(l(_7912), l(_7916))), bt(bt(l(_7932), l(_7936)), l(_7940))) ;
T = bt(bt(bt(l(_7898), l(_7902)), bt(l(_7912), l(_7916))), bt(l(_7926), bt(l(_7936), l(_7940)))) ;
T = bt(bt(bt(l(_7898), l(_7902)), bt(l(_7912), l(_7916))), bt(l(_7926), l(_7930))) ;
T = bt(bt(bt(l(_7898), l(_7902)), bt(l(_7912), l(_7916))), l(_7920)) ;
T = bt(bt(bt(l(_7898), l(_7902)), l(_7906)), bt(bt(l(_7922), l(_7926)), bt(l(_7936), l(_7940)))) ;
T = bt(bt(bt(l(_7898), l(_7902)), l(_7906)), bt(bt(l(_7922), l(_7926)), l(_7930))) ;
T = bt(bt(bt(l(_7898), l(_7902)), l(_7906)), bt(l(_7916), bt(l(_7926), l(_7930)))) ;
T = bt(bt(bt(l(_7898), l(_7902)), l(_7906)), bt(l(_7916), l(_7920))) ;
T = bt(bt(bt(l(_7898), l(_7902)), l(_7906)), l(_7910)) ;
T = bt(bt(l(_7892), bt(l(_7902), l(_7906))), bt(bt(l(_7922), l(_7926)), bt(l(_7936), l(_7940)))) ;
T = bt(bt(l(_7892), bt(l(_7902), l(_7906))), bt(bt(l(_7922), l(_7926)), l(_7930))) ;
T = bt(bt(l(_7892), bt(l(_7902), l(_7906))), bt(l(_7916), bt(l(_7926), l(_7930)))) ;
T = bt(bt(l(_7892), bt(l(_7902), l(_7906))), bt(l(_7916), l(_7920))) ;
T = bt(bt(l(_7892), bt(l(_7902), l(_7906))), l(_7910)) ;
T = bt(bt(l(_7892), l(_7896)), bt(bt(l(_7912), l(_7916)), bt(l(_7926), l(_7930)))) ;
T = bt(bt(l(_7892), l(_7896)), bt(bt(l(_7912), l(_7916)), l(_7920))) ;
T = bt(bt(l(_7892), l(_7896)), bt(l(_7906), bt(l(_7916), l(_7920)))) ;
T = bt(bt(l(_7892), l(_7896)), bt(l(_7906), l(_7910))) ;
T = bt(bt(l(_7892), l(_7896)), l(_7900)) ;
T = bt(l(_7886), bt(bt(l(_7902), l(_7906)), bt(l(_7916), l(_7920)))) ;
T = bt(l(_7886), bt(bt(l(_7902), l(_7906)), l(_7910))) ;
T = bt(l(_7886), bt(l(_7896), bt(l(_7906), l(_7910)))) ;
T = bt(l(_7886), bt(l(_7896), l(_7900))) ;
T = bt(l(_7886), l(_7890)).
In a very efficient way (just ca. 300 inferences):
13 ?- time((btree(T),fail)).
% 343 inferences, 0.000 CPU in 0.000 seconds (94% CPU, 5723153 Lips)
false.
Instead of chosing the most complex tree first, we can rearrange the order to choose the simplest tree first:
btree_(bt(A,B)) :-
( leaf(A); btree_(A) ),
( leaf(B); btree_(B) ).
Good.
The obvious disadvantage of the recursion limiting approach is that we have to figure out a proper depth for the call, but I can live with that, given the advantages of the declarative model.
One can implement iterative deepening with this model also. It is clean and efficient.
- Do you guys have a better way (remember one of the requirements is to be able to answer the most general query with a declarative model)?
- Do you see any major problem with this approach ?
EDIT: Note that my point is not specifically about binary trees, I know there are many more efficient and fast ways to do that.