Amb and Search

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

    To extend Scheme to support nondeterminism, we introduce a new special form called amb. [*] The expression (amb e1e2 ... en) returns the value of one of the n expressions ei ``ambiguously.'' For example, the expression

    (list (amb 1 2 3) (amb 'a 'b))
    
    can have six possible values:
    (1 a) (1 b) (2 a) (2 b) (3 a) (3 b)
    Amb with a single choice produces an ordinary (single) value.

    Amb with no choices--the expression (amb)--is an expression with no acceptable values. Operationally, we can think of (amb) as an expression that when evaluated causes the computation to ``fail'': The computation aborts and no value is produced. Using this idea, we can express the requirement that a particular predicate expression p must be true as follows:

    (define (require p)
      (if (not p) (amb)))
    

    With amb and require, we can implement the an-element-of procedure used above:

    (define (an-element-of items)
      (require (not (null? items)))
      (amb (car items) (an-element-of (cdr items))))
    
    An-element-of fails if the list is empty. Otherwise it ambiguously returns either the first element of the list or an element chosen from the rest of the list.

    We can also express infinite ranges of choices. The following procedure potentially returns any integer greater than or equal to some given n:

    (define (an-integer-starting-from n)
      (amb n (an-integer-starting-from (+ n 1))))
    
    This is like the stream procedure integers-starting-from described in section [*], but with an important difference: The stream procedure returns an object that represents the sequence of all integers beginning with n, whereas the amb procedure returns a single integer. [*]

    Abstractly, we can imagine that evaluating an amb expression causes time to split into branches, where the computation continues on each branch with one of the possible values of the expression. We say that amb represents a nondeterministic choice point. If we had a machine with a sufficient number of processors that could be dynamically allocated, we could implement the search in a straightforward way. Execution would proceed as in a sequential machine, until an amb expression is encountered. At this point, more processors would be allocated and initialized to continue all of the parallel executions implied by the choice. Each processor would proceed sequentially as if it were the only choice, until it either terminates by encountering a failure, or it further subdivides, or it finishes. [*]

    On the other hand, if we have a machine that can execute only one process (or a few concurrent processes), we must consider the alternatives sequentially. One could imagine modifying an evaluator to pick at random a branch to follow whenever it encounters a choice point. Random choice, however, can easily lead to failing values. We might try running the evaluator over and over, making random choices and hoping to find a non-failing value, but it is better to systematically search all possible execution paths. The amb evaluator that we will develop and work with in this section implements a systematic search as follows: When the evaluator encounters an application of amb, it initially selects the first alternative. This selection may itself lead to a further choice. The evaluator will always initially choose the first alternative at each choice point. If a choice results in a failure, then the evaluator automagically[*] backtracks to the most recent choice point and tries the next alternative. If it runs out of alternatives at any choice point, the evaluator will back up to the previous choice point and resume from there. This process leads to a search strategy known as depth-first search or chronological backtracking. [*]

    Driver loop

    The driver loop for the amb evaluator has some unusual properties. It reads an expression and prints the value of the first non-failing execution, as in the prime-sum-pair example shown above. If we want to see the value of the next successful execution, we can ask the interpreter to backtrack and attempt to generate a second non-failing execution. This is signaled by typing the symbol try-again. If any expression except try-again is given, the interpreter will start a new problem, discarding the unexplored alternatives in the previous problem. Here is a sample interaction:

    ;;; Amb-Eval input:
    (prime-sum-pair '(1 3 5 8) '(20 35 110))
    ;;; Starting a new problem
    ;;; Amb-Eval value:
    (3 20)
    
    ;;; Amb-Eval input:
    try-again
    ;;; Amb-Eval value:
    (3 110)
    
    ;;; Amb-Eval input:
    try-again
    ;;; Amb-Eval value:
    (8 35)
    
    ;;; Amb-Eval input:
    try-again
    ;;; There are no more values of
    (prime-sum-pair (quote (1 3 5 8)) (quote (20 35 110)))
    
    ;;; Amb-Eval input:
    (prime-sum-pair '(19 27 30) '(11 36 58))
    ;;; Starting a new problem
    ;;; Amb-Eval value:
    (30 11)
    

    Exercise. Write a procedure an-integer-between that returns an integer between two given bounds. This can be used to implement a procedure that finds Pythagorean triples, i.e., triples of integers (i,j,k) between the given bounds such that $i \leq j$ and i2 + j2 =k2, as follows:

    (define (a-pythagorean-triple-between low high)
      (let ((i (an-integer-between low high)))
        (let ((j (an-integer-between i high)))
          (let ((k (an-integer-between j high)))
            (require (= (+ (* i i) (* j j)) (* k k)))
            (list i j k)))))
    

    Exercise. Exercise [*] discussed how to generate the stream of all Pythagorean triples, with no upper bound on the size of the integers to be searched. Explain why simply replacing an-integer-between by an-integer-starting-from in the procedure in exercise [*] is not an adequate way to generate arbitrary Pythagorean triples. Write a procedure that actually will accomplish this. (That is, write a procedure for which repeatedly typing try-again would in principle eventually generate all Pythagorean triples.)

    Exercise. Ben Bitdiddle claims that the following method for generating Pythagorean triples is more efficient than the one in exercise [*]. Is he correct? (Hint: Consider the number of possibilities that must be explored.)

    (define (a-pythagorean-triple-between low high)
      (let ((i (an-integer-between low high))
            (hsq (* high high)))
        (let ((j (an-integer-between i high)))
          (let ((ksq (+ (* i i) (* j j))))
            (require (>= hsq ksq))
            (let ((k (sqrt ksq)))
              (require (integer? k))
              (list i j k))))))
    

    Previous: Variations on a Scheme--Nondeterministic Computing Next: Examples of Nondeterministic Programs

      webmaster@arsdigita.org