amb:   Ambiguous Operator
1 Evaluation Model
1.1 Empty Handlers
2 Ambiguous Operator
amb
amb-fail
for/  amb
for*/  amb
make-amb
3 Stream Constructor
in-amb
in-amb*
in-amb/  do
in-amb*/  do
4 Parameters
current-amb-depth-first?
current-amb-fair?
current-amb-shuffler
9.3

amb: Ambiguous Operator🔗ℹ

 (require amb) package: amb

The amb library provides John McCarthy’s ambiguous operator for nondeterministic programming. All uses of amb operators must occur within the dynamic extent of in-amb, which delimits an amb-tree. in-amb traverses the amb-tree and returns its leaves as a lazy stream. The order of the leaves depends on the search strategy (see Parameters).

1 Evaluation Model🔗ℹ

An amb-tree is a leaf-labeled tree: internal nodes are pure branching points with no values of their own; only leaves carry the results of complete computations. Structurally, a tree is either a leaf value or a stream of subtrees. Categorically, this is the free monad over the stream functor.

Each amb expression creates an amb-tree with one branch per alternative. A branch that calls (amb) is a dead end: it is pruned and the search backtracks.

Internally, in-amb maintains an amb-frontier—an immutable deque of pending amb-trees. Each step pops a tree, runs one branch, and, depending on the outcome, either yields a leaf and suspends, enqueues a child tree and continues, discards the tree and continues (dead end), or just continues.

The alternatives in amb are mutually exclusive—each is lazily evaluated only when that branch is selected.

Example:
> (stream->list (in-amb (amb (amb 1 2) (amb) (amb 'a 'b))))

'(1 2 a b)

Corresponding amb-tree:

(amb (amb 1 2) (amb) (amb 'a 'b))

├── (amb 1 2)

│   ├── 1

│   └── 2

├── (amb)

└── (amb 'a 'b)

    ├── 'a

    └── 'b

Nested amb expressions are implicitly flattened: their alternatives are inserted into the parent’s choice list. Under unfair depth-first order, (amb (amb 1 2) (amb) (amb 'a 'b)) therefore produces the same sequence of leaves as (amb 1 2 'a 'b).

Example:
> (stream->list (in-amb (amb 1 2 'a 'b)))

'(1 2 a b)

By contrast, when amb expressions appear in order (e.g., via let*), each subsequent amb is in the continuation of the previous one. The amb-tree becomes a Cartesian product of all choices.

Example:
> (stream->list
   (in-amb
    (let* ([x (amb 1 2)]
           [y (amb 'a 'b)]
           [z (amb x y)])
      z)))

'(1 a 1 b 2 a 2 b)

Corresponding amb-tree:

(amb 1 2)

├── (amb 'a 'b)

│   ├── (amb 1 'a)

│   │   ├── 1

│   │   └── 'a

│   └── (amb 1 'b)

│       ├── 1

│       └── 'b

└── (amb 'a 'b)

    ├── (amb 2 'a)

    │   ├── 2

    │   └── 'a

    └── (amb 2 'b)

        ├── 2

        └── 'b

Since this is the free monad over the stream functor, it can be expressed purely with streams. The correspondence is direct: amb is stream, in-amb is stream-flatten, and sequential amb in let* corresponds to for*/stream composed with stream-flatten. This is the interpretation without an empty handler, in which an empty choice contributes nothing; it does not model the handler, whose leaf ends the whole branch. The examples above, rewritten in this style:

(define (stream-flatten s*)
  (stream*
   (if (stream-empty? s*)
       s*
       (call-with-values
        (λ () (stream-first s*))
        (λ (v . v*)
          (define s (stream-rest s*))
          (if (and (null? v*) (stream? v))
              (if (stream-empty? v)
                  (stream-flatten s)
                  (stream-flatten (stream* (stream-first v) (stream-rest v) s)))
              (stream-cons (apply values v v*) (stream-flatten s))))))))

Examples:
; A single amb node with nested alternatives:
> (stream->list
   (stream-flatten (stream (stream 1 2) (stream) (stream 'a 'b))))

'(1 2 a b)

; Sequential amb in let* form — the monadic bind —
; for*/stream with stream-flatten at each level:
> (stream->list
   (for*/stream ([x (stream-flatten (stream 1 2))]
                 [y (stream-flatten (stream 'a 'b))]
                 [z (stream-flatten (stream x y))])
     z))

'(1 a 1 b 2 a 2 b)

The translation extends to realistic programs. Whereas in-amb and nested amb expressions both flatten implicitly, the stream encoding requires explicit stream-flatten at each level. Intermediate bindings use #:do, and (amb) (dead end) becomes #:when or #:unless.

Examples:
> (define sequence->amb (compose make-amb sequence-generate))
> (define (amb:pythagorean n)
    (define a (sequence->amb (in-range 1 n)))
    (define a² (* a a))
    (define b (sequence->amb (in-range a n)))
    (define b² (* b b))
    (define c (sequence->amb (in-range b n)))
    (define c² (* c c))
    (unless (= (+ a² b²) c²) (amb))
    (vector-immutable a b c))
> (define (stream:pythagorean n)
    (for*/stream ([a (stream-flatten (in-range 1 n))]
                  #:do [(define a² (* a a))]
                  [b (stream-flatten (in-range a n))]
                  #:do [(define b² (* b b))]
                  [c (stream-flatten (in-range b n))]
                  #:do [(define c² (* c c))]
                  #:when (= (+ a² b²) c²))
      (vector-immutable a b c)))
> (for/and ([n (in-inclusive-range 100 200 20)])
    (printf "pythagorean ~a:\n" n)
    (printf "  stream: ")
    (define stream:sol*
      (time (stream->list (stream:pythagorean n))))
    (printf "     amb: ")
    (define amb:sol*
      (time (stream->list (in-amb (amb:pythagorean n)))))
    (printf "  ~a solutions\n" (length stream:sol*))
    (equal? amb:sol* stream:sol*))

pythagorean 100:

  stream: cpu time: 241 real time: 131 gc time: 61

     amb: cpu time: 195 real time: 102 gc time: 16

  50 solutions

pythagorean 120:

  stream: cpu time: 312 real time: 165 gc time: 16

     amb: cpu time: 400 real time: 219 gc time: 100

  65 solutions

pythagorean 140:

  stream: cpu time: 475 real time: 241 gc time: 7

     amb: cpu time: 398 real time: 256 gc time: 25

  78 solutions

pythagorean 160:

  stream: cpu time: 338 real time: 338 gc time: 4

     amb: cpu time: 322 real time: 322 gc time: 5

  94 solutions

pythagorean 180:

  stream: cpu time: 480 real time: 481 gc time: 3

     amb: cpu time: 452 real time: 452 gc time: 7

  108 solutions

pythagorean 200:

  stream: cpu time: 655 real time: 656 gc time: 4

     amb: cpu time: 613 real time: 612 gc time: 10

  125 solutions

#t

The difference is in the traversal mechanism. stream-flatten relies on the implicit program stack as its frontier, yielding unfair depth-first order (LIFO). in-amb replaces the stack with an amb-frontier to make the traversal order configurable (see Parameters).

Despite the continuation capture and deque management, the overhead is negligible in practice—on the Pythagorean triple example above, the amb version runs at roughly the same speed as the stream version.

The stream-flatten wrappers in the stream version serve a specific purpose: they mirror amb’s implicit flattening of nested amb-trees, so that each loop variable can traverse a leaf-labeled tree rather than just a flat stream. When the iteration source is already flat—as it is for in-range or in-stream over a plain stream—wrapping it in stream-flatten adds significant overhead. In those cases, omit stream-flatten and iterate directly:

Examples:
> (define (pythagorean n)
    (for*/stream ([a (in-range 1 n)]
                  #:do [(define a² (* a a))]
                  [b (in-range a n)]
                  #:do [(define b² (* b b))]
                  [c (in-range b n)]
                  #:do [(define c² (* c c))]
                  #:when (= (+ a² b²) c²))
      (vector-immutable a b c)))
> (for ([n (in-inclusive-range 100 200 20)])
    (printf "pythagorean ~a:\n  " n)
    (define sol* (time (stream->list (pythagorean n))))
    (printf "  ~a solutions\n" (length sol*)))

pythagorean 100:

  cpu time: 0 real time: 0 gc time: 0

  50 solutions

pythagorean 120:

  cpu time: 1 real time: 1 gc time: 0

  65 solutions

pythagorean 140:

  cpu time: 1 real time: 1 gc time: 0

  78 solutions

pythagorean 160:

  cpu time: 2 real time: 2 gc time: 0

  94 solutions

pythagorean 180:

  cpu time: 3 real time: 3 gc time: 0

  108 solutions

pythagorean 200:

  cpu time: 4 real time: 4 gc time: 0

  125 solutions

1.1 Empty Handlers🔗ℹ

So far, a branch that reaches (amb) simply disappears. Sometimes it is useful to see where branches gave up: to count them, to report them, or to mark a pause. For that, a search can be given an empty handler, a thunk passed as #:empty-handler (see in-amb). When a branch reaches (amb), the handler is called right there, and whatever it returns becomes one result of the search. The branch then stops, just as it would on failure: (amb) itself still does not return, so nothing after it in that branch runs.

In the tree, an (amb) node answered by the handler becomes a leaf, written here as (amb) ⇒ (handler) ⇒ and the handler’s result. With a handler that returns 'none:

Example:
> (stream->list (in-amb (amb (amb 1 2) (amb) (amb 'a 'b))
                        #:empty-handler (λ () 'none)))

'(1 2 none a b)

(amb (amb 1 2) (amb) (amb 'a 'b))

├── (amb 1 2)

│   ├── 1

│   └── 2

├── (amb) ⇒ (handler) ⇒ 'none

└── (amb 'a 'b)

    ├── 'a

    └── 'b

Because the branch stops at (amb), the handler’s result takes the place of everything that branch would have computed. Below, the branch with x = 1 produces 'none instead of a list:

Example:
> (stream->list
   (in-amb (let* ([x (amb 1 2)]
                  [y (if (= x 1) (amb) (amb 'a 'b))])
             (list x y))
           #:empty-handler (λ () 'none)))

'(none (2 a) (2 b))

(amb 1 2)

├── (amb) ⇒ (handler) ⇒ 'none

└── (amb 'a 'b)

    ├── '(2 a)

    └── '(2 b)

Two practical points follow.

  • Choose a mark that cannot be mistaken for an answer. The results do not say where each one came from, so if the handler returns a value that the computation could also produce, the two cannot be told apart. A value of its own, such as an instance of a private structure type, is safest.

  • Use (amb-fail) for a failure that must stay a failure. The handler answers (amb) but never (amb-fail); without a handler, the two are the same. For the same reason, (amb e (amb)) has the same results as e only without a handler: with one, it has one more.

Here a is the type of the computation’s results; 1 is a type with exactly one element (the terminal object of category theory), such as (Values), whose only element is (values); 0 is a type with no elements (the initial object), that is, Nothing; and b + a is a disjoint union, whose elements come from b or from a, tagged with their side.

In terms of types, a handler that always returns the same mark has the type 1, so the results of the search form a stream of 1 + a: each is an answer, or the mark of a branch that ended at (amb). A handler that returns (values) also has the type 1, and contributes a result of zero values. The default handler amb-fail never returns, so its result type is 0; since 0 + a has just the elements of a, the results are a stream of a, as without a handler. The first practical point above is that the consumer sees the values but not the tags of the sum.

2 Ambiguous Operator🔗ℹ

syntax

(amb expr ...)

John McCarthy’s ambiguous operator. It creates an amb-tree to nondeterministically choose among the given exprs.

Each expr is implicitly wrapped in a thunk and collected into a mutable vector.

When called with no arguments, (amb) is a choice among no alternatives: the computation ends there. What it ends with is decided by the innermost search. By default it ends with nothing, so (amb) signals failure, causing the search to backtrack; this is commonly used as a pruning mechanism to discard branches that do not satisfy a constraint. A search started with a handler (#:empty-handler for in-amb and in-amb/do) instead calls the handler at the point of (amb) and produces its results as one leaf.

Examples:
> (stream->list (in-amb (amb 1 2 3)))

'(1 2 3)

> (stream->list (in-amb (amb)))

'()

> (stream->list (in-amb (amb) #:empty-handler (λ () 'none)))

'(none)

A dead-end (amb) nested among alternatives prunes only that branch—the others proceed normally:

Example:
> (stream->list (in-amb (amb 1 2 (amb) 3 4)))

'(1 2 3 4)

A productive nested amb creates a child amb-tree. When the inner tree is exhausted, the search backtracks and continues with the next sibling in the parent:

Example:
> (stream->list (in-amb (amb (amb 1 2 3) (amb 'a 'b 'c))))

'(1 2 3 a b c)

Alternatives producing multiple values are supported:

Example:
> (for/list ([(i j) (in-amb (amb (values 1 'x) (values 2 'y)))])
    (cons i j))

'((1 . x) (2 . y))

procedure

(amb-fail) → none/c

Signals failure unconditionally: the branch is a dead end even in a search that has a handler for (amb). Without a handler, (amb-fail) and (amb) behave the same.

Example:
> (stream->list (in-amb (amb 1 (amb) (amb-fail) 2) #:empty-handler (λ () 'none)))

'(1 none 2)

syntax

(for/amb maybe-length (for-clause ...) break-clause ... body ...+)

(for/amb type-ann-maybe maybe-length (for-clause ...) type-ann-maybe expr ...+)

syntax

(for*/amb maybe-length (for-clause ...) break-clause ... body ...+)

(for*/amb type-ann-maybe maybe-length (for-clause ...) type-ann-maybe expr ...+)
Variants of for that treat each iteration of the loop body as an alternative.

If the optional #:length clause is specified, the syntax resembles for/vector and for*/vector. Each loop body is implicitly wrapped in a thunk and collected into a mutable vector, mirroring how amb itself works.

Examples:
> (for/list ([(a b) (in-amb (for/amb #:length 4 #:fill (values 0 'w)
                                     ([i #(1 2)] [j #(x y)])
                              (values i j)))])
    (cons a b))

'((1 . x) (2 . y) (0 . w) (0 . w))

> (for/list ([(a b) (in-amb (for/amb #:length 4
                                     ([i #(1 2)] [j #(x y)])
                              (values i j)))])
    (cons a b))

'((1 . x) (2 . y))

If the optional #:length clause is omitted, the number of iterations is unknown, so each loop body is deferred to a thunk and produced one at a time by driving the for loop as a coroutine: a captured continuation resumes the loop to yield the next alternative on demand. Because the body is evaluated lazily, this works with sequences of any size, including infinite ones.

A for/amb that provides no alternative at all is (amb): without #:length, a loop with no iterations; with #:length, a length of 0, or no iterations and no #:fill. Slots filled by default are not alternatives, but with #:fill every slot is one. The clauses are evaluated once either way.

Examples:
> (stream->list (in-amb (for/amb ([i '()]) i) #:empty-handler (λ () 'none)))

'(none)

> (stream->list (in-amb (for/amb #:length 2 ([i '()]) i) #:empty-handler (λ () 'none)))

'(none)

> (stream->list (in-amb (for/amb #:length 2 #:fill 0 ([i '()]) i) #:empty-handler (λ () 'none)))

'(0 0)

Examples:
> (for/list ([x (in-amb (for/amb ([i (in-naturals)]) i))] [_ 5]) x)

'(0 1 2 3 4)

> (for/list ([x (in-amb (for/amb ([c "hello"]) c))]) x)

'(#\h #\e #\l #\l #\o)

> (for/list ([x (in-amb (for/amb ([x '(1 2)]) (amb x (- x))))]) x)

'(1 -1 2 -2)

procedure

(make-amb more? get) → any

  more? : (-> boolean?)
  get : (-> any)
The foundational primitive for building ambiguous operators. It captures the current continuation as an amb-tree, then on each resumption calls more? to check for remaining alternatives and get to produce the next values. When (more?) returns #f, make-amb signals a dead end; if it does so before any alternative has been produced, make-amb is (amb).

Examples:
> (define sequence->amb (compose make-amb sequence-generate))
> (define (roll-doubles)
    (define a (sequence->amb (in-range 1 7)))
    (define b (sequence->amb (in-range 1 7)))
    (unless (= a b) (amb))
    (list a b))
> (stream->list (in-amb (roll-doubles)))

'((1 1) (2 2) (3 3) (4 4) (5 5) (6 6))

3 Stream Constructor🔗ℹ

syntax

(in-amb expr maybe-empty)

 
maybe-empty = 
  | #:empty-handler handler-expr
 
  handler-expr : (-> any)
Constructs a lazy stream of amb-tree leaves from the ambiguous expression expr. All amb operations within expr are delimited by a prompt installed by in-amb.

The form (in-amb expr) expands to (in-amb* (λ () expr)), and (in-amb expr #:empty-handler handler-expr) to (in-amb* (λ () expr) handler-expr).

The empty handler answers (amb). When the computation reaches (amb), the handler is called there, in the dynamic context of (amb), and the values it returns are produced as one leaf; the rest of that branch is abandoned, as with failure. The default handler is amb-fail, which makes (amb) a failure. The handler may also call amb-fail itself, make choices of its own, or raise an exception; if it reaches (amb) of the same search, exn:fail:contract is raised. It should return as many values as expr does: Typed Racket checks this, and otherwise the mismatch is reported only where the leaf is received.

Since the search is lazy, the dynamic context of (amb) is the one in which the search is resumed, that is, the consumer’s, as for any other code in expr:

Examples:
> (define p (make-parameter 'first))
> (define-values (more? get)
    (sequence-generate
     (in-amb/do (begin (when (amb #t #f) (amb)) (when (amb #t #f) (amb)) 'x)
                #:empty-handler p)))
> (list (get) (parameterize ([p 'second]) (get)) (get))

'(first second x)

Example:
> (stream->list (in-amb (let ([x (amb 1 2 3)]) (if (even? x) (amb) x))
                        #:empty-handler (λ () 'even)))

'(1 even 3)

The handler belongs to the search that was given it, not to the dynamic extent of in-amb: a search started inside the computation, without a handler of its own, still fails at its own (amb). Together with a choice point, a handler makes a pause observable: (when (amb #t #f) (amb)) produces the handler’s leaf and then continues the computation after it.

Examples:
> (define (pause) (when (amb #t #f) (amb)))
> (stream->list (in-amb (begin (pause) 'done) #:empty-handler (λ () 'paused)))

'(paused done)

> (stream->list (in-amb (begin (pause) 'done)))

'(done)

Example:
> (stream->list (in-amb (amb 1 (amb 2 3) (amb 4 5 6) 7 8)))

'(1 2 3 4 5 6 7 8)

in-amb acts as a one-step lookahead stream constructor. To determine whether the stream is non-empty, it eagerly evaluates the next leaf.

Examples:
> (for/list ([i (in-amb (begin0 (amb 1 (amb 2 3) 4) (displayln 'hi)))]
             [_ 2])
    i)

hi

hi

hi

'(1 2)

> (for/list ([_ 2]
             [i (in-amb (begin0 (amb 1 (amb 2 3) 4) (displayln 'hi)))])
    i)

hi

hi

'(1 2)

procedure

(in-amb* thk [handler]) → stream?

  thk : (-> any)
  handler : (-> any) = amb-fail
The procedural backend of in-amb; handler answers (amb), as with #:empty-handler.

Examples:
> (define (choices)
    (define x (amb 1 2 3))
    (define y (amb 'a 'b))
    (list x y))
> (stream->list (in-amb* choices))

'((1 a) (1 b) (2 a) (2 b) (3 a) (3 b))

syntax

(in-amb/do expr maybe-empty)

 
maybe-empty = 
  | #:empty-handler handler-expr
 
  handler-expr : (-> any)
Similar to in-amb, but returns a generic sequence rather than specifically a lazy stream. The sequence is single-pass: results are not retained, so iterating it consumes them, and once every result has been consumed the sequence is empty. This makes it cheaper than in-amb when the results are needed only once.

procedure

(in-amb*/do thk [handler]) → sequence?

  thk : (-> any)
  handler : (-> any) = amb-fail
The procedural backend of in-amb/do.

4 Parameters🔗ℹ

The behavior of the amb search engine is governed by two orthogonal parameters: current-amb-depth-first? controls traversal order and current-amb-fair? controls fairness. Together, they make the amb-frontier pluggable in ways impossible to achieve with the implicit stack alone.

As described in Evaluation Model, the search driver maintains an amb-frontier. Each step pops an amb-tree from the front and runs it. The result determines what happens next:

  • Dead end: the tree is exhausted and is discarded (not returned to the amb-frontier). The search continues.

  • Nothing: the tree produced nothing on this resumption. The parent tree is put back for its remaining alternatives. current-amb-fair? controls whether the parent tree goes to the front or the back. The search continues.

  • Leaf: a result is produced, by the computation or by the empty handler at (amb). The parent tree is put back (as in the nothing case). The search suspends.

  • Branch: a new amb-tree was created. The parent tree is put back (as in the nothing case), and the new child tree is also inserted. current-amb-depth-first? controls whether the child tree goes to the front or the back. The search continues.

Both parameters are captured per-tree at the point where amb or for/amb is evaluated. This means different parts of a computation can use different strategies via parameterize. As a result, different subtrees may coexist in the same search with different traversal strategies.

parameter

(current-amb-depth-first?) → boolean?

(current-amb-depth-first? depth-first?) → void?
  depth-first? : boolean?
Controls where a newly created child tree is inserted in the amb-frontier.

When #t (the default), child trees are pushed to the front of the amb-frontier, yielding depth-first search (DFS). When #f, child trees are pushed to the back, yielding breadth-first search (BFS).

Examples:
> (stream->list (in-amb (amb 1 (amb 2 3) (amb 4 5 6) 7 8)))

'(1 2 3 4 5 6 7 8)

> (parameterize ([current-amb-depth-first? #f])
    (stream->list (in-amb (amb 1 (amb 2 3) (amb 4 5 6) 7 8))))

'(1 7 8 2 3 4 5 6)

parameter

(current-amb-fair?) → boolean?

(current-amb-fair? fair?) → void?
  fair? : boolean?
Controls where the parent tree is placed back in the amb-frontier after being popped and producing a result.

When #f (the default), the parent tree is placed back at the front. When #t, the parent tree is placed at the back, giving other pending trees a chance to run first. This prevents any single branch from starving its siblings.

Examples:
> (stream->list
   (in-amb
    (let* ([a (amb 1 2 3)]
           [b (amb 4 5 6)])
      (list a b))))

'((1 4) (1 5) (1 6) (2 4) (2 5) (2 6) (3 4) (3 5) (3 6))

> (stream->list
   (in-amb
    (let ([a (amb 1 2 3)])
      (parameterize ([current-amb-fair? #t])
        (define b (amb 4 5 6))
        (list a b)))))

'((1 4) (2 4) (3 4) (1 5) (2 5) (3 5) (1 6) (2 6) (3 6))

parameter

(current-amb-shuffler) → (-> mutable-vector? void?)

(current-amb-shuffler shuffle!) → void?
  shuffle! : (-> mutable-vector? void?)
Controls how to shuffle thunks before creating an amb-tree via amb. The procedure receives the mutable vector of thunks and may reorder them in place.

The default value is void (no shuffling).

Examples:
> (require srfi/43)
> (parameterize ([current-amb-shuffler vector-reverse!])
    (stream->list (in-amb (amb 1 2 3 4 5))))

'(5 4 3 2 1)