Notes on the "Sleepy" Program
David Matuszek, Villanova University

This page is a discussion of the Prolog source code for the "Sleepy" program.


The following predicates are declared dynamic because they may be changed during execution of the program:

:- dynamic at/2, i_am_at/1, i_am_holding/1, alive/1, lit/1, visible_objects/1.

The form functor/arity refers to the set of predicates with the given name, for example, at, and the given number of arguments, 2.

The turnstile ( :- ) at the very beginning of the clause indicates that this clause should be executed as soon as it is consulted. This means that the compiler has already seen the lines above it (and can use them as needed in this clause), but has not yet seen the lines below it. You should declare clause to be dynamic before the compiler gets to those actual clauses, otherwise (depending on the compiler) it may be too late.

In fact, i_am_holding/1 does not actually need to be declared dynamic, because the game starts out with no clauses of this form. The first such clause is created as you play the game, so it will automatically be dynamic.

Standard Prolog does not recognize or require dynamic, but some implementations (including SWI Prolog) require dynamic for reasons of efficiency.


The line

:- retractall(i_am_holding(_)), start.

occurs at the end of the game. If it were just :- start. then all it would do is call the start/0 predicate as soon as the game is loaded (this is a predicate defined in the game that prints out instructions). Why does it retract all clauses of  i_am_holding/1 , but not any of the other dynamic clauses?

The answer lies in the way consult/1 works.

So here's what happened. While I was writing and debugging the game, I edited and consulted the file sleepy.pl (and played the game) many times. But when I was partway through the game, and found and fixed a bug, I consulted the file again, and played again from the beginning--I thought. But since at that point I might have been holding the flyswatter, and since the file I consulted had no clauses of i_am_holding/1 in it, my consult left me holding the flyswatter--not exactly the way the game should start out. So this is a case where Prolog's "intelligent" consult caused a subtle problem.


I included a dump/0 predicate for debugging. This predicate lists all my dynamic clauses, that is, the things that I want to look at and see if they are getting updated correctly. The form listing(functor) will list all clauses with that functor; the form listing(functor/arity) will list clauses with the given functor and the given arity.


Note that the usual rule is to put specific clauses before more general ones. For example, take(fly) and take('light switch') precede take(X). This is because clauses are tried in the order in which they appear, and if you put take(X) first, it would be used even when you tried to take the fly or the light switch.

I like to put the "failure" cases first--all the things that can go wrong. That way you don't have to check for them again in the "success" case. (This isn't primarily a matter of efficiency, but of "safe" programming--if you have to check for the same thing in two different places, there's always the danger that you will change or fix one place, and forget to change or fix the other.)

For example, take/1 checks first for trying to take the fly or the light switch, then for whether you are already holding the object. Only after that does take/1 actually try to pick up the object. The other failure case, that the object is not in the same room as you are, is very difficult to put first, because negation in Prolog is tricky. Always try to avoid negation.

In this game negation gave me quite a lot of trouble, because some things depended on the room being lit, and others on it being dark. I didn't want to have both lit/1 and dark/1 predicates, because (again) having the same thing in two places is unsafe programming. So I had to use negation, and juggling the clauses around to minimize the number of negations took quite a bit of time.


Why avoid negation? Because negation in Prolog doesn't behave logically! Negation is an extra-logical predicate.

The basic problem with negation is that it does not "instantiate" (find a value for) variables. There are only a finite number of values that can satisfy a test (because of Prolog's "closed world assumption"), but there are infinitely many that fail to satisfy the test.

A negated test can never instantiate variables. Consider \+foo(X).   If foo(X) fails, that means it didn't find a suitable value for X; so \+foo(X) succeeds, but without a value for X. If foo(X) succeeds, then Prolog found a value of X that satisfies foo(X), but it doesn't satisfy \+foo(X), which fails and forces Prolog to backtrack--even if we could keep the value for X, we couldn't proceed and do anything with it.

For example, if we say
     \+ i_am_at(X)
this does not set X to some value that I'm not at. Since there are an infinite number of such values (not all of them "rooms"), this makes sense. It also means that a double negation, such as
     \+ \+ i_am_at(X)
does not instantiate X, although logically it would make sense to do so.

Another consequence is more subtle, but even more of a trap. Conjunction (and) should be commutative: A and B logically means the same thing as B and A. But negation in Prolog destroys commutativity.

Suppose you have the facts:

     female(jane).
     female(mary).
     rich(mary).

Prolog behaves as follows:

     ?- female(X), \+rich(X).

     X = jane

     Yes
     ?- \+rich(X), female(X).

     No

This seems like a serious bug in Prolog, which is supposed to be a logical language. In fact, it's a deliberate design decision, because otherwise answering queries requires exponential time (it's an NP-complete problem).

You can use negation in Prolog if you understand it well enough (or if you're lucky!), but experienced Prolog programmers try to avoid negations.


Prolog's "cut," ! (that's an exclamation mark), is what I like to call a commit point; it prevents trying any more alternatives. When you reach a commit point, anything that previously occurred in this clause, such as instantiating variables, cannot be undone--you are committed and cannot backtrack. In addition, you are committed to this clause; if something later in the clause causes it to fail, you cannot go on to try other clauses in this predicate.