AIMA in Clojure
The AIMA textbook comes with implementation of the book’s algorithms in both Python and Common Lisp. The algorithms were originally implemented in Common Lisp, but later translated to Python for educational purposes.
Peter Norvig says that Python is easier to use for students who don’t have Lisp experience. It has an interactive interpreter. It has better interoperability with Java (using Jython).
He also mentions disadvantages:
The two main drawbacks of Python from my point of view are (1) there is very little compile-time error analysis and type declaration, even less than Lisp, and (2) execution time is much slower than Lisp, often by a factor of 10 (sometimes by 100 and sometimes by 1). Qualitatively, Python feels about the same speed as interpreted Lisp, but very noticably slower than compiled Lisp. For this reason I wouldn’t recommend Python for applications that are (or are likely to become over time) compute intensive (unless you are willing to move the speed bottlenecks into C). But my purpose is oriented towards pedagogy, not production, so this is less of an issue.
Clojure, which didn’t exist at the time AIMA was published, has most of the above advantages.
- It has an interactive REPL
- It runs on the JVM
- It is not as complicated as Common Lisp
- It does some compile-time error analysis
- It has good performance.
So Clojure is a good language for me to practice some of the exercises from the AIMA book.
Search
Chapter 3 is about problem-solving agents
that use search algorithms to find solutions.
It gives a formal definition of a problem
with five components: initial state, actions, transition model, goal test, and path cost.
This can be translated into Clojure as a protocol:
There book also defines a node in a search tree having four components: state, parent, action, and path cost.
In Clojure, this can be a record:
Another important data structure is the fringe or frontier. It has three operations: empty?, pop, and insert.
Again, we can use a protocol:
Since, we will be using persistent, immutable data structures for the fringe, the insert function returns a new fringe, and the remove-next function returns a vector with the next item, and the remaining items.
Now we can define a general tree search function, if we first define some utility functions:
Graph search is almost the same as tree search, except that it also uses a Clojure
set (#{}
) to maintain the closed set of expanded nodes:
Fringe implementations
These general search functions are nice, but they are useless without an implementation of fringe.
We can start by implementing a LIFO fringe (a stack). In Clojure, the way to do this is to use a persistent list. So using our previously defined Fringe protocol, we have:
We can do a similar thing for a FIFO fringe, using Clojure’s persistent queue:
Now we can use these fringe types to create some different search functions:
and
Solving problems
We can see how well our search algorithms work on some simple problems:
For example, we can define the N Queens problem like this:
The state is represented as a vector of integers, where the integer at each index is the column position of the queen in that row.
The valid-column function used to get the actions looks like this: