These exercises require loading SDDR, the Simple Deductive Data Retriever.
(ql:quickload "sddr")
The tests for the member and map coloring exercises are in sddr-exs-tests.lisp.
To do the logic problems,
define your rules in a Lisp file that begins with
(in-package :sddr-tests)
. Run
the tests in that package in the Lisp Listener.
(in-package :sddr-tests)
For each test, put your rules in the global variable
listed in the exercise description,
using defparameter
.
Send those variable definitions to the Critic when they
are passing the test cases.
Feel free to create new test cases!.
Exercise: member
Global variable name: *member-kb*
Test name: member
Use defparameter
to define *member-kb*
to hold a set of rules to define a deductive (not Lisp!) member
predicate. The query (member x l)
should be
true if and only if x is a member of the list l.
Look at the append
example
in sddr-tests.lisp
to see how to represent lists with (cons x y)
functional terms.
Exercise: Map Coloring
Global variable name: *map-color-kb*
Tests: color-map1
, color-map2
, and
color-map3
in
sddr-exs-tests.lisp
The map coloring problem has a long history. The problem is simple: given a map of various countries bordering each other, color the map using the fewest number of colors, such that no countries sharing a border of more than a point have the same color. For many years, it had been proven that 5 colors was sufficient for any flat map, no matter how complex, and that no map was known that needed more than 4. It finally required a computer to generate the complete proof that 4 was enough.
Here are three small maps, one already colored, the others left for you to figure out:
![]() |
![]() |
![]() |
map1 | map2 | map3 |
Here is a more complicated map that Martin Gardner claimed required 5 colors.
You can encode a map coloring problem in logic with:
- some assertions about what colors there are, and
-
rules (at least one per problem) for a predicate
(colors-for map a-region-color b-region-color c-region-color...)
that gives the color constraints that have to be satisfied for a given map
Maps are identified by a map ID, e.g., map1
for the
first map. colors-for
will have a different
number of arguments for each map. This is not a mistake nor
anything you have to treat specially. Different rules can use
different numbers of arguments for predicates.
You can have rules for all three maps at the same time.
Exercise: DDR ask-1
Tests: ask-1
Note: you run the tests in sddr-tests, but you define the modified SDDR code in the sddr package.
This exercise is not about defining logic rules. It is about changing the retriever to return answers one at a time. Do this as a "patch file". Don't change the source code but create a new file, e.g., sddr-patch.lisp. This file should define code in the sddr package and contain only the new and changed definitions necessary to change the retriever. Submit only the patches for review.
The goal of this exercise is to make it possible to get answers one at a time from the Deductive Retriever, somewhat the way the Prolog interpreter works. It should be possible to get one answer and stop, and not pay the cost of looking for other answers. This approach has two advantages. First, if one answer is sufficient, you can get it faster. Second, if a query has an infinite number of answers, a particular application can ask for the first N answers.
This does not prevent infinite loops. If you have rules with that can loop forever, you can still get an infinite loop if you ask a query that has no answer.
More specifically, in this exercise you need to
- define a new function, ask-1, to get answers one at a time, and
- redefine ask to get all the answers that ask-1 can generate.
The syntax of ask-1 is like ask. I.e.,
(ask-1 [query rules])
The difference is that the arguments are optional.
- If a query and rules are given, return the first answer, if any, for the given query and rules.
- if query and rules are not given, return the next answer keeping the same query and rules used before.
Here's an example of how ask-1 should work in SDDR with one of the test cases.
> (ask-1 '(can-satisfy-hunger ?x) *tiger-kb*) (CAN-SATISFY-HUNGER ANTELOPE) > (ask-1) (CAN-SATISFY-HUNGER TIGER) > (ask-1) NIL
If (ask-1) is evaluated when no previous query has been specified, (ask-1) should return false.
Approach
This problem has both a conceptual challenge and a software programming challenge. The conceptual challenge is how to represent the state of the deductive search in a way that supports getting answers one at a time. The programming challenge is how to make as few changes as possible to the existing tested retriever code.
The current code uses a simple uniform recursive logic: explore all branches and append all results. This automatically takes care of branches that lead to no results or many. But it means that the code has many functions calling other functions recursively, expecting to get lists of all answers back. If now only one answer is returned, how can the basic logic structure be kept?
The answer is generated lists, also known as lazy lists and, in Abelson and Sussman's Structure and Interpretation of Computer Programs, streams. A generated list is a data structure with list-like operations, analogous to car and cdr, but the elements of a generated list may not exist until accessed. That is, the elements may be generate on demand.
The glist package defines some basic functions that create and manipulate generated lists. Most of these functions are analogous to the standard list functions: gcar, gcdr, gappend and so on.
Because generated list functions work much like regular list functions, converting code using lists to generated lists is much less work than completely redoing the logic of the retriever to keep a queue of answers. Instead of consing or appending lists of all answers, you gcons or gappend them.
Do not generate all the answers and then store
the results in a generated list. Only the work needed to find one solution
should happen for the first call to ask-1
.
The work to find the next solution should only happen if
you call ask-1
again. You need to look inside the code
for where backchaining occurs, both in
the main code
and some of the logic extensions, like and and not.
Do not convert all lists to lazy lists. Only lists that involve finding alternative solutions should be converted. Other lists, e.g., the list of bindings returned by unification, should remain normal lists.
Hint: you may find it useful to first convert some code using iterative loops to a recursive form first. Change and test first, then introduce generated lists.
You'll need a few top-level global variables to hold the the initial arguments and the generated list of answers. (ask-1 query rules) should set these variables. (ask-1) should then pop off the next answer from the generated list. Note that the original ask can be defined to simply call ask-1 repeatedly until it returns false, and collects all the answers. That way the original tests still work.
Make sure all the still SDDR tests still work as they did before. Note that blocks-world fails one test because of how the rules were written. It should still fail in the revised version.
Use the Lisp Critic on your code before sending.