Examples of Nondeterministic Programs

SICP > Metalinguistic Abstraction > Variations on a Scheme--Nondeterministic Computing > Examples of Nondeterministic Programs
Previous: Amb and Search Next: Implementing the Amb Evaluator

    Section [*] describes the implementation of the amb evaluator. First, however, we give some examples of how it can be used. The advantage of nondeterministic programming is that we can suppress the details of how search is carried out, thereby expressing our programs at a higher level of abstraction.

    Logic Puzzles

    The following puzzle (taken from Dinesman 1968) is typical of a large class of simple logic puzzles:

    Baker, Cooper, Fletcher, Miller, and Smith live on different floors of an apartment house that contains only five floors. Baker does not live on the top floor. Cooper does not live on the bottom floor. Fletcher does not live on either the top or the bottom floor. Miller lives on a higher floor than does Cooper. Smith does not live on a floor adjacent to Fletcher's. Fletcher does not live on a floor adjacent to Cooper's. Where does everyone live?

    We can determine who lives on each floor in a straightforward way by enumerating all the possibilities and imposing the given restrictions: [*]

    (define (multiple-dwelling)
      (let ((baker (amb 1 2 3 4 5))
            (cooper (amb 1 2 3 4 5))
            (fletcher (amb 1 2 3 4 5))
            (miller (amb 1 2 3 4 5))
            (smith (amb 1 2 3 4 5)))
        (require
         (distinct? (list baker cooper fletcher miller smith)))
        (require (not (= baker 5)))
        (require (not (= cooper 1)))
        (require (not (= fletcher 5)))
        (require (not (= fletcher 1)))
        (require (> miller cooper))
        (require (not (= (abs (- smith fletcher)) 1)))
        (require (not (= (abs (- fletcher cooper)) 1)))
        (list (list 'baker baker)
              (list 'cooper cooper)
              (list 'fletcher fletcher)
              (list 'miller miller)
              (list 'smith smith))))
    

    Evaluating the expression (multiple-dwelling) produces the result

    ((baker 3) (cooper 2) (fletcher 4) (miller 5) (smith 1))
    
    Although this simple procedure works, it is very slow. Exercises [*] and [*] discuss some possible improvements.

    Exercise. Modify the multiple-dwelling procedure to omit the requirement that Smith and Fletcher do not live on adjacent floors. How many solutions are there to this modified puzzle?

    Exercise. Does the order of the restrictions in the multiple-dwelling procedure affect the answer? Does it affect the time to find an answer? If you think it matters, demonstrate a faster program obtained from the given one by reordering the restrictions. If you think it does not matter, argue your case.  

    Exercise. In the multiple dwelling problem, how many sets of assignments are there of people to floors, both before and after the requirement that floor assignments be distinct? It is very inefficient to generate all possible assignments of people to floors and then leave it to backtracking to eliminate them. For example, most of the restrictions depend on only one or two of the person-floor variables, and can thus be imposed before floors have been selected for all the people. Write and demonstrate a much more efficient nondeterministic procedure that solves this problem based upon generating only those possibilities that are not already ruled out by previous restrictions. (Hint: This will require a nest of let expressions.)  

    Exercise. Write an ordinary Scheme program to solve the multiple dwelling puzzle.

    Exercise. Solve the following ``Liars'' puzzle (from Phillips 1934):

    Five schoolgirls sat for an examination. Their parents--so they thought--showed an undue degree of interest in the result. They therefore agreed that, in writing home about the examination, each girl should make one true statement and one untrue one. The following are the relevant passages from their letters: What in fact was the order in which the five girls were placed?

    Exercise. Use the amb evaluator to solve the following puzzle: [*]

    Mary Ann Moore's father has a yacht and so has each of his four friends: Colonel Downing, Mr. Hall, Sir Barnacle Hood, and Dr. Parker. Each of the five also has one daughter and each has named his yacht after a daughter of one of the others. Sir Barnacle's yacht is the Gabrielle, Mr. Moore owns the Lorna; Mr. Hall the Rosalind. The Melissa, owned by Colonel Downing, is named after Sir Barnacle's daughter. Gabrielle's father owns the yacht that is named after Dr. Parker's daughter. Who is Lorna's father?
    Try to write the program so that it runs efficiently (see exercise [*]). Also determine how many solutions there are if we are not told that Mary Ann's last name is Moore.

    Exercise. Exercise [*] described the ``eight-queens puzzle'' of placing queens on a chessboard so that no two attack each other. Write a nondeterministic program to solve this puzzle.

    Parsing natural language

    Programs designed to accept natural language as input usually start by attempting to parse the input, that is, to match the input against some grammatical structure. For example, we might try to recognize simple sentences consisting of an article followed by a noun followed by a verb, such as ``The cat eats.'' To accomplish such an analysis, we must be able to identify the parts of speech of individual words. We could start with some lists that classify various words: [*]

    (define nouns '(noun student professor cat class))
    
    (define verbs '(verb studies lectures eats sleeps))
    
    (define articles '(article the a))
    
    We also need a grammar, that is, a set of rules describing how grammatical elements are composed from simpler elements. A very simple grammar might stipulate that a sentence always consists of two pieces--a noun phrase followed by a verb--and that a noun phrase consists of an article followed by a noun. With this grammar, the sentence ``The cat eats'' is parsed as follows:

    (sentence (noun-phrase (article the) (noun cat))
              (verb eats))
    

    We can generate such a parse with a simple program that has separate procedures for each of the grammatical rules. To parse a sentence, we identify its two constituent pieces and return a list of these two elements, tagged with the symbol sentence:

    (define (parse-sentence)
      (list 'sentence
             (parse-noun-phrase)
             (parse-word verbs)))
    
    A noun phrase, similarly, is parsed by finding an article followed by a noun:
    (define (parse-noun-phrase)
      (list 'noun-phrase
            (parse-word articles)
            (parse-word nouns)))
    

    At the lowest level, parsing boils down to repeatedly checking that the next unparsed word is a member of the list of words for the required part of speech. To implement this, we maintain a global variable *unparsed*, which is the input that has not yet been parsed. Each time we check a word, we require that *unparsed* must be non-empty and that it should begin with a word from the designated list. If so, we remove that word from *unparsed* and return the word together with its part of speech (which is found at the head of the list): [*]

    (define (parse-word word-list)
      (require (not (null? *unparsed*)))
      (require (memq (car *unparsed*) (cdr word-list)))
      (let ((found-word (car *unparsed*)))
        (set! *unparsed* (cdr *unparsed*))
        (list (car word-list) found-word)))
    

    To start the parsing, all we need to do is set *unparsed* to be the entire input, try to parse a sentence, and check that nothing is left over:

    (define *unparsed* '())
    
    (define (parse input)
      (set! *unparsed* input)
      (let ((sent (parse-sentence)))
        (require (null? *unparsed*))
        sent))
    

    We can now try the parser and verify that it works for our simple test sentence:

    ;;; Amb-Eval input:
    (parse '(the cat eats))
    ;;; Starting a new problem
    ;;; Amb-Eval value:
    (sentence (noun-phrase (article the) (noun cat)) (verb eats))
    

    The amb evaluator is useful here because it is convenient to express the parsing constraints with the aid of require. Automatic search and backtracking really pay off, however, when we consider more complex grammars where there are choices for how the units can be decomposed.

    Let's add to our grammar a list of prepositions:

    (define prepositions '(prep for to in by with))
    

    and define a prepositional phrase (e.g., ``for the cat'') to be a preposition followed by a noun phrase:

    (define (parse-prepositional-phrase)
      (list 'prep-phrase
            (parse-word prepositions)
            (parse-noun-phrase)))
    
    Now we can define a sentence to be a noun phrase followed by a verb phrase, where a verb phrase can be either a verb or a verb phrase extended by a prepositional phrase:[*]

    (define (parse-sentence)
      (list 'sentence
             (parse-noun-phrase)
             (parse-verb-phrase)))
    
    (define (parse-verb-phrase)
      (define (maybe-extend verb-phrase)
        (amb verb-phrase
             (maybe-extend (list 'verb-phrase
                                 verb-phrase
                                 (parse-prepositional-phrase)))))
      (maybe-extend (parse-word verbs)))
    

    While we're at it, we can also elaborate the definition of noun phrases to permit such things as ``a cat in the class.'' What we used to call a noun phrase, we'll now call a simple noun phrase, and a noun phrase will now be either a simple noun phrase or a noun phrase extended by a prepositional phrase:

    (define (parse-simple-noun-phrase)
      (list 'simple-noun-phrase
            (parse-word articles)
            (parse-word nouns)))
    
    (define (parse-noun-phrase)
      (define (maybe-extend noun-phrase)
        (amb noun-phrase
             (maybe-extend (list 'noun-phrase
                                 noun-phrase
                                 (parse-prepositional-phrase)))))
      (maybe-extend (parse-simple-noun-phrase)))
    

    Our new grammar lets us parse more complex sentences. For example

    (parse '(the student with the cat sleeps in the class))
    
    produces

    (sentence
     (noun-phrase
      (simple-noun-phrase (article the) (noun student))
      (prep-phrase (prep with)
                   (simple-noun-phrase
                    (article the) (noun cat))))
     (verb-phrase
      (verb sleeps)
      (prep-phrase (prep in)
                   (simple-noun-phrase
                    (article the) (noun class)))))
    

    Observe that a given input may have more than one legal parse. In the sentence ``The professor lectures to the student with the cat,'' it may be that the professor is lecturing with the cat, or that the student has the cat. Our nondeterministic program finds both possibilities:

    (parse '(the professor lectures to the student with the cat))
    
    produces

    (sentence
     (simple-noun-phrase (article the) (noun professor))
     (verb-phrase
      (verb-phrase
       (verb lectures)
       (prep-phrase (prep to)
                    (simple-noun-phrase
                     (article the) (noun student))))
      (prep-phrase (prep with)
                   (simple-noun-phrase
                    (article the) (noun cat)))))
    
    Asking the evaluator to try again yields
    (sentence
     (simple-noun-phrase (article the) (noun professor))
     (verb-phrase
      (verb lectures)
      (prep-phrase (prep to)
                   (noun-phrase
                    (simple-noun-phrase
                     (article the) (noun student))
                    (prep-phrase (prep with)
                                 (simple-noun-phrase
                                  (article the) (noun cat)))))))
    

    Exercise. With the grammar given above, the following sentence can be parsed in five different ways: ``The professor lectures to the student in the class with the cat.'' Give the five parses and explain the differences in shades of meaning among them.

    Exercise. The evaluators in sections [*] and [*] do not determine what order operands are evaluated in. We will see that the amb evaluator evaluates them from left to right. Explain why our parsing program wouldn't work if the operands were evaluated in some other order.

    Exercise. Louis Reasoner suggests that, since a verb phrase is either a verb or a verb phrase followed by a prepositional phrase, it would be much more straightforward to define the procedure parse-verb-phrase as follows (and similarly for noun phrases):

    (define (parse-verb-phrase)
      (amb (parse-word verbs)
           (list 'verb-phrase
                 (parse-verb-phrase)
                 (parse-prepositional-phrase))))
    
    Does this work? Does the program's behavior change if we interchange the order of expressions in the amb?

    Exercise. Extend the grammar given above to handle more complex sentences. For example, you could extend noun phrases and verb phrases to include adjectives and adverbs, or you could handle compound sentences. [*]

    Exercise. Alyssa P. Hacker is more interested in generating interesting sentences than in parsing them. She reasons that by simply changing the procedure parseword so that it ignores the ``input sentence'' and instead always succeeds and generates an appropriate word, we can use the programs we had built for parsing to do generation instead. Implement Alyssa's idea, and show the first half-dozen or so sentences generated. [*]  

    Previous: Amb and Search Next: Implementing the Amb Evaluator

      webmaster@arsdigita.org