Implementing stacks and queues in Prolog - Depth-first and Breadth-first search

Hello everyone,
lately I have started reworking with the de facto Prolog Bible “The Craft of Prolog” by Richard O’Keefe. In particular I’m trying to make my way through chapter 2, which deals with searching and implementing different search strategies in Prolog.
O’Keefe deals with two different search strategies (at least in the first part of chapter 2 which I’m studying): depth-first and breadth-first. These two different strategies are closely related to the two different data structures that we use to store the nodes yet to be visited: a stack in the case of depth-first, a queue in the case of breadth-first search.

Depth-first search is so to say the “natural”, the “default” search strategy used by Prolog to look for an answer to queries, this doesn’t mean that one cannot implement other search strategies.
In keeping with the common vocabulary that parallels the relation between nodes in a graph to that of a family tree we may say that a depth-first search explores the children of a node (or its grand-children, or its grand grand-children and so on) before visiting its siblings. In a breadth-first search the siblings are visited first and only if a solution isn’t found the search explores the children nodes. Here I’ve shared a link a of a gif animation showing the order in which nodes are visited in a depth-first search:

https://commons.wikimedia.org/wiki/File:Depth-First-Search.gif#/media/File:Depth-First-Search.gif

In a breadth-first search the order in which nodes are visited would be: 1 2 5 9 3 6 8 10 4 7

Now the interesting thing is that these two different strategies are closely tied to how we store the nodes to visit. If we use a stack we get depth-first, if we use a queue we get breadth-first.

A stack is a collection of items in which we add and remove items from the same side, so to say (these two actions are usually named “pushing” to and “popping” items from the stack): we may imagine a stack as a bag in which we place several things one above the other. Of course when we remove something from the bag the first item we pop is the last item we have added and certainly not the first one, which probably lies deep in the bag under lots of other stuff…). For this reason a stack is called a LIFO (Last In First Out) data structure. Stacks are well known to Prolog programmers because lists are stacks: we add and remove items of lists always from the left side and though the usual representation [a, s, d] hides the truth, we know that the real representation of a list is rather .(a, .(s, .(d, ))) (I use here the traditional dot notation for the list constructor instead of the more verbose [|]).
In a queue on the contrary we add items at one side and remove them from the other (these two operations are usually named “enqueuing” and “dequeuing” items): queues are therefore FIFO (First In First Out) data structures.
O’Keefe explains very well how the only different thing we have to do to build a stack or a queue is to append the children of a node to the front (in which case children nodes will be visited first) or to the back of the list of the nodes to visit (in which case sibling nodes will be visited first and children after).
But there is a problem: in Prolog doing things at the right side of a list is more complicated than doing the same things at the left side of it. Adding and removing items at the left is a constant time operation, adding and removing items at the right side of a list requires more time and space as the length of the list grows and the number of items to add grows…
So of course O’ Keefe asks himself: is it possible to implement a queue in Prolog (so that we can have breadth-first search, and not only depth-first) in a more efficient way than what appending to the right allows us to do? The answer is yes, there is a way. But it’s a little bit crazy, or at least it looks a little bit crazy at first, before one understand how it works.

To implement a queue O’Keefe uses a list pair, that is two lists united by a functor which can be + or / or whatever one likes more (he uses + by the way). An empty queue looks like this: empty_queue(+). The Front list will be the list from which we dequeue items, the Back list will be the one in which we enqueue them. The interesting fact is that, being both lists, we always work at the left side of the two, we avoid working (appending) to the right side (which is the costy operation that would slow down the whole thing if we work with big data).
A queue with some items looks like this: queue([a, b, c]+[f, e, d]).
If we dequeue an item (in this case ‘a’), the new queue would be: queue([b, c]+[f, e, d]).
Now the question is: what happens when the front list is empty and we want to dequeue one more item?
Like this: queue(+[f, e, d])?
In this case we reverse the back list and move it to the front, dequeue the first item from it (in this case ‘d’, remember that the list has been reversed!) and add an empty list as the new back list. So after dequeuing ‘d’ the new queue will look like queue([e, f]+). So we can start again adding new items to the back list or dequeuing them from the new front list…

It works. Even though it looks a little bit crazy at first (or at least it looked crazy to me)… and more importantly it’s efficient because we emulate a queue behavior by using two lists and working always at the left side of lists both in enqueing and dequeuing instead of working at the right side (which would be highly inefficient). Thanks for now

Here I paste the code to implement a queue in Prolog, in case you wanted to try it yourself:

% Implementing the same data type (a queue) using two lists
empty_queue([]/[]).

% Enqueuing here means prepending to the back list
enqueue(Item, Front/Back, Front/[Item|Back]).

% Dequeuing here means popping the first item from the front list.
% If the front list is empty: we reverse the back list, return the first element from it 
% and move the rest to the front adding a new empty back list
dequeue(Item, [Item|Front]/Back, Front/Back).
dequeue(Item, []/Back, Front/[]) :-
    reverse(Back, [Item|Front]).

There is a cleaner/faster queue implementation if you keep reading. By “cleaner” I mean less hacky.

He uses also difference lists…but that looked crazy as well :sweat_smile:
But that’s another way to work efficiently at the right side of a list

It is basically a singly linked list with insertion at either end and popping at the front. Quite familiar so less esoteric than the earlier solutions :grinning: There is the “length as a term” weird bit that’s true.

What you say makes me think of another thing which I was uncertain about: in a proper queue you can insert at one side only right? So this thing that with the predicates he defines one can enqueue at either end is - let’s say - a non-traditional queue right???

You get it for free with a difference list, exactly as with a singly linked list (where you have the head and the sentinel). And in the interface that you see in the book it will be additional effort to prevent inserting at the front. So I would guess it is purely pragmatic decision. I can’t remember, did the book give some motivation other than “it just works”?

For the moment I stop here and take a breath. I find sometimes difficult to follow his explanations because he offers several solutions, several ways to approach a problem…and sometimes you haven’t yet fully understood the first thing and already he starts offering a different way…but for today I’ll remain here. Head and sentinel I’ve heard about but I don’t exactly what they mean (the sentinel in particular)

You mean that I’m missing the “beauty” of it, I guess? I would say it is a brilliant solution

Sorry “sentinel” is just a silly word for what is at the end of a one-directional traversal. So for a singly linked list it is the thing that the “next” of the last element is pointing to. I admit I don’t know if this terminology is widely used.

This is what makes it a useful book. It doesn’t teach, it shows.

Maybe you’re right… :sweat_smile: as a matter of fact no one nowadays writes books that way. But that doesn’t affect the contents of course. He had in mind a very high-brow and specialized audience of readers possibly. Good evening

Yes, computer programmers :rofl:

Well of course one may say that Prolog is in itself a niche programming language. Books on other languages usually don’t assume too much about their audience’s prerequisites…but i don’t think it’s only that. He’s writing as a specialist to a group of other specialists. He’s a cordon bleu chef or something like that

Anyways it soon gets silly…I’m studying the book I hope there’s something to learn there…

1 Like

No. You’re a careful reader. This a slightly different realization using a list pair. O’Keefe describes both a list pair version and a difference list version. In another version he adds an extra argument to count the items inserted so far in unary notation (0, s(0), s(s(0)), and so on) and thus prevent what he calls hallucinating…that is removing items that were never there in the first place

Don’t delete this. I want to try it when at home

Depth vs Breadth first is a topic I experimented with quite a bit a few years ago and put notes and diagrams on a now defunct (thanks to the transition from Python 2 to 3) wiki system called MoinMoin. I started moving my notes there to to a Hugo site, but now see the page of Depth First is blank so need to get back to it sometime.

The main gist of my notes are available at Graph Traversal | Frontier Software and Breadth First | Frontier Software

Something I find interesting is that the only difference between depth first and breadth first is the order in which children are appended to the frontier. With Depth First they are put at the front, which in 70s programming languages made things much faster because memory was very scarce causing appending stuff to the back of long lists to be a common cause of slowness.

Experimenting with SWI-Prolog, I’ve found the assumption that the default is Depth First is no longer true, and if you include tabling, it seems to do Iterative Deepening.

I haven’t done any benchmarks to see if appending stuff to the back of a long list is much slower than to the front with SWI-Prolog, but I feel all those digressions into doing that more efficiently in ancient languages in ancient textbooks should hopefully have been sorted by now. I know Erlang’s documentation says it’s no longer an issue and coders should ignore warnings about doing that from old texts.

1 Like

Thanks for sharing this

I don’t know what the term “fork” means…like in

but I may learn it another time

No time like the now

If you add an element to the front of a list, you can keep your original list and have as many new lists as you want:

?- L0 = [a,b,c], L1 = [foo|L0], L2 = [bar,baz|L0].
L0 = [a, b, c],
L1 = [foo, a, b, c],
L2 = [bar, baz, a, b, c].

But if you try to extend the list from the back using a difference list:

L = [a,b,c|L0], L0 = [foo|L1]. % and that's that

Of course you can use append:

?- L0 = [a,b,c], append(L0, [foo], L1), append(L0, [bar,baz], L2).
L0 = [a, b, c],
L1 = [a, b, c, foo],
L2 = [a, b, c, bar, baz].

Playing with your database and your code on graph traversal I also found something interesting. That is: you write your search predicate a little bit differently from what O’Keefe does…and this also yields different orderings in the results.
You write:

tc(X, Y) :-
  arc(X, Y).

tc(X, Z) :-
  arc(X, Y),
  tc(Y, Z).

O’Keefe writes:

child_star(X, X).
child_star(X, Z) :-
    child(X, Y),
    child_star(Y, Z).

With O’Keefe’s code one gets a strict depth-first search order, with your code one gets a kind of halfway solution of breadth-first and depth-first search, I find

PS: I hope I’m not saying something really stupid here…maybe I’d better think twice :sweat_smile:
PSS: in the worst case I will delete it…but the orderings are different anyways

1 Like

I’m a bit rusty on this stuff, but recall experimenting and the automagical way SWI-Prolog traverses graphs isn’t the same as in the historical texts, and is completely different with tabling.

The safest way to ensure depth first or breadth first is to manage the listied tree yourself. I recall doing a simple example which I put up here Tree Breadth-First in Prolog - #18 by joeblog