Prolog/Associative map

Prolog's built-in lists are handy, but sometimes a simple linear list is not enough. When you want to maintain an association between one set of elements ('keys') and another ('values'), you need an associative map data structure. Here's how such a structure can be implemented in Prolog using binary search trees (BSTs).

First we need a representation of search trees in Prolog terms. Remember that a BST is a binary tree with key-value pairs stored in the nodes. We can thus represent nodes as functors t(Key,Value,LeftChild,RightChild). The empty tree will be represented by the atom nil.

But we don't want to program to this representation directly: by providing the appropriate predicates, we can specify an abstract data structure (ADT). The reason for doing so is that we can later change our implementation to use balanced binary trees or some other more sophisticated data structure. We shall call this ADT the ordered map, or ordmap. Here are the basic operations on an ordmap:


 * empty_map(-Map): unifies Map with the empty map.
 * lookup_ordmap(+Key, +Map, -Value): lookup the Value associated with Key in <tt>Map</tt>.
 * <tt>insert_ordmap(+Key, +Value, +Map0, -Map)</tt>: insert the pair <tt>Key</tt>, <tt>Value</tt> in the map <tt>Map0</tt>, yielding <tt>Map</tt>. Note that afterwards, both <tt>Map0</tt> and <tt>Map</tt> are valid ordered maps, which differ by at most one element. This predicate fails if <tt>Key</tt> is already present in <tt>Map0</tt>.
 * <tt>update_ordmap(+Key, +Value, +Map0, -Map)</tt>: like <tt>insert_ordmap</tt>, but removes any previous association of <tt>Key</tt> with a different value (and always succeeds).
 * <tt>remove_ordmap(+Key, +Map0, -Map, -Value)</tt>: remove <tt>Key</tt> from <tt>Map0</tt>, yielding <tt>Map</tt>. The value associated with <tt>Key</tt> is returned in <tt>Value</tt>. This predicate fails if <tt>Key</tt> was not in <tt>Map0</tt>.
 * <tt>member_ordmap(+Map, -Key, -Value)</tt>: backtracks over all key/value pairs in <tt>Map</tt> by order of keys.
 * <tt>rmember_ordmap(+Map, -Key, -Value)</tt>: backtracks over all key/value pairs in <tt>Map</tt> by reverse order of keys.
 * <tt>size_ordmap(+Map, -Size)</tt>: determines the number of elements in <tt>Map</tt>.

For reasons that will become clear later on, keys in our ordered maps should always be ground terms.

The implementation of <tt>empty_map</tt> is trivial:

Our next predicate, <tt>lookup_map</tt>, follows the usual recursion patterns for BST operations:

Note the use of <tt>==/2</tt> instead of unification. The reason for doing so lies in the use <tt>@</2</tt> which compares terms according to the standard order of terms. In this ordering, for any two distinct terms $$T_1$$ and $$T_2$$, either $$T_1$$ <tt>==</tt> $$T_2$$, $$T_1$$ <tt>@&lt;</tt> $$T_2$$, or $$T_2$$ <tt>@&lt;</tt> $$T_1$$. For example, <tt>a @< b</tt>, <tt>X @< Y</tt> and <tt>X @< foo</tt>. In fact, a free variable is always <tt>@<</tt> a ground term. But when two variables, or a variable and a ground term are unified, the ordering changes: after <tt>X=Y</tt>, <tt>X==Y</tt> is also true. This is why keys should, in principle, always be ground terms: that way, the ordering is always preserved (but we leave the appropriate check up to the user of our ordered map data structure). Note that there is no such restriction on values, since they don't need to be ordered.

Also note that we have no case for <tt>nil</tt>, since looking up anything in an empty tree will always fail.

Deletion of an element from a binary search tree can be a bit tricky to implement; the following code replaces a node with two children by its in-order predecessor, which is the maximum element of the left subtree. It uses the <tt>rm_max</tt> helper predicate to remove the maximum element from a subtree.

The rest of the predicates are now easy to write:

Exercise: implement <tt>rmember_ordmap</tt> and <tt>update_ordmap</tt>.

Libraries
Associative map data structures are built into the libraries of various Prolog implementations (though often not in compatible ways):
 * SICStus provides library(avl)
 * SWI-Prolog provides library(assoc)

Both libraries are based on AVL trees; note that SICStus also provides a library(assoc), but that is based on simple lists.