In the following, we will present PROLOG implementations of well–known
algorithms for searching in graphs and binary search trees. The benefits of PROLOG are
• the elegant handling of data structures (lists, trees, XML), • (implicit) backtracking, and
• the compact representation of case distinctions in different rules.
The algorithms are typically recursive. Recursion can be formulated nicely due to the compact list access.
Graph Search
The graph is given by PROLOG facts for the predicates graph_arc/2 and graph_sink/1. Labyrinth: 6 - a b c d e f g h i graph_arc(i, f). graph_arc(i, h). graph_arc(h, g). graph_arc(g, d). graph_arc(d, e). graph_arc(d, a). graph_arc(a, b). graph_arc(b, c). graph_sink(c).
Search for Simple Paths
The predicate graph_search/2 searches for a simple path from a given node to a sink of a graph:
% graph_search(+Node, ?Path) <- graph_search(X, Path) :-
graph_search(X, [X], Path).
Another predicate graph_search/3 with the same predicate symbol but a
different arity is called.
Notation for arguments in the comment line:
Visited
Path =
[Y1=Y, . . . ,Yn=Z]
X Y Z
- - -
• A call graph_search(X, Visited, Nodes) with a bound
argument X, which is not a sink, and a list Visited of already visitied nodes
– uses an edge from X to not yet vistited successor node Y, and then – calculates a path Path from Y to a sink Z, which does not visit Y and
the nodes in Visited.
If no path from Y to a sink can be found, then another successor node of
X must be used (Backtracking).
• The result Nodes = [X|Path] is a simple path from X to a sink of the graph.
The predicate graph_search/3 is recursive, because of its second rule:
% graph_search(+Node, +Visited, ?Path) <- graph_search(X, _, [X]) :-
graph_sink(X).
graph_search(X, Visited, [X|Path]) :- graph_edge(X, Y),
not(member(Y, Visited)),
write(user, ’->’), write(user, Y), graph_search(Y, [Y|Visited], Path).
Visited
Path =
[Y1=Y, . . . ,Yn=Z]
X Y Z
- - -
Termination is ensured by the fact that already visited nodes cannot be visited
The following rule symmetrisises the predicate graph_arc/2:
graph_edge(X, Y) :- ( graph_arc(X, Y)
; graph_arc(Y, X) ).
Thus, it is not necessary to explicitely list the inverse edges. E.g., from
graph_arc(i, f).
we obtain
graph_edge(i, f). graph_edge(f, i).
• The initial call graph_search(X, [X], Path) calculates a simple path from X to a sink of the graph.
– If X is a sink, then the first rule for graph_search/3 computes
Path as an empty list.
– Otherwise, the recursive, second rule choses a successor node Y using
graph_edge(X, Y), and then it continues searching from there.
• Further paths can be searched for by backtracking.
– Alternative successor nodes Y can be used in the second rule.
– In the implementation above, we can continue searching beyond a sink by using the second rule instead of the fist one.
Implicit and Explicit Backtracking
In PROLOG, backtracking is used automatically (implicitly).
In a procedural language, backtracking has to be implemented explicitly. In a direct translation of the code above to a procedural environment, a call
graph_edge(X, Y) can only produce a single successor node Y of X –
if there is no path from Y to a sink, then the computation fails. Moreover, at most one solution could be computed.
If we implement the graph search procedurally using explicit backtracking, then we get more lines of more complex code than in PROLOG.
Computation
• The predicate graph_search/2 use depth first search, and it calculates simple paths (without duplicate nodes).
• With the call graph_search(+Node, -Path), we can calculate all simple paths from Node to a sink (graph_sink) by backtracking:
?- graph_search(i, Path). ->f->h->g->d->e->a->b->c Path = [i, h, g, d, a, b, c] ?- graph_search(e, Path). ->d->a->b->c Path = [e, d, a, b, c] ; ->g->h->i->f No
• If we add another edge graph_arc(e, b) to the graph (i.e., we tear down the wall between e and b), then there appears another simple path
[e, b, c] from e to the sink c.
• All results can be calculated by backtracking and findall/3:
graph_arc(e, b). ?- findall( Path,
graph_search(e, Path), Paths ).
Paths = [[e, d, a, b, c], [e, b, c]] Yes
The Meta–Predicate findall/3
Finding of all solutions for a goal:
findall( X, goal(X), Xs )
The DDK allows for the following equivalent set notation:
Xs <= { X | goal(X) }
Further important meta–predicates are checklist/2 and maplist/3 for
lists, as well as the predicates for loops (control structures) from the library
Binary Search Trees
% search_in_tree(+Key, +Tree) <- search_in_tree(Key, Tree) :-
Tree = tree(Root, Lson, Rson), ( Key = Root
; Key < Root ->
search_in_tree(Key, Lson) ; Key > Root ->
search_in_tree(Key, Rson) ).
Term Representation of a Search Tree
tree(5,
tree(4, nil, nil), tree(9,
tree(6, nil, nil),
tree(10, nil, nil) ) )
5
10 6
9 4
% insert_into_tree(+Key, +Tree, ?New_Tree) <- insert_into_tree(Key, Tree, New_Tree) :-
Tree = tree(Root, Lson, Rson), ( Key = Root ->
New_Tree = Tree ; Key < Root ->
insert_into_tree(Key, Lson, L), New_Tree = tree(Root, L, Rson) ) ; K > Root ->
insert_into_tree(Key, Rson, R), New_Tree = tree(Root, Lson, R) ). insert_into_tree(Key, nil New_Tree) :-
Important Concepts
• Terms (for Data and Control Structures) and Unification • Backtracking
• SLDNF–Resolution
PROLOG allows for
• declarative programming, • compact programs, and
Data Structures, Operations, and Control Structures
• The restriction to a few basic data types and a single complex data type,
namely the terms, which is generic and subsumes all the other types, standardizes the data structures.
• There are no explicit type declarations.
• There exists a large collection of generic operations that are applicable to
terms – and thus to all data types.
• Frequently, meta–predicates are used.
• In addition to standard control structures, such as branching
(if–then–else), loops (for, while), and recursion, user–defined control structures can be built as meta–predicates.
Software Engineering Aspects
PROLOG supports abstraction and compact code, and thus stimulates
refactoring:
• The generic type of terms with generic operations supports abstraction
and code reuse.
• User–defined control structures allow for further abstraction.
• Unification, implicit backtracking, and abstaining from explicit type
declarations, result in very compact code and support rapid prototyping.
• Declarativity makes the code much more readable and thus extensible.
Switching from conventional programming languages to the logic
programming paradigm is difficult and usually requires a lot of training and effort.
Course on Deductive Databases
Topics:
• foundations and applications of PROLOG and DATALOG,
data modelling and programming;
• the deductive database system DDBASE; • efficient evaluation of DATALOG programs;
• further language constructs in the DDK (DISLOG Developers’ Kit):
– complex data structures,
– default negation and disjunction;