amb:   Ambiguous Operator
1 Evaluation Model
2 Ambiguous Operator
amb
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. 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: 467 real time: 245 gc time: 31

     amb: cpu time: 189 real time: 99 gc time: 12

  50 solutions

pythagorean 120:

  stream: cpu time: 492 real time: 260 gc time: 147

     amb: cpu time: 522 real time: 272 gc time: 26

  65 solutions

pythagorean 140:

  stream: cpu time: 576 real time: 291 gc time: 8

     amb: cpu time: 525 real time: 280 gc time: 40

  78 solutions

pythagorean 160:

  stream: cpu time: 398 real time: 391 gc time: 8

     amb: cpu time: 437 real time: 435 gc time: 17

  94 solutions

pythagorean 180:

  stream: cpu time: 876 real time: 874 gc time: 6

     amb: cpu time: 833 real time: 833 gc time: 14

  108 solutions

pythagorean 200:

  stream: cpu time: 1279 real time: 1278 gc time: 9

     amb: cpu time: 826 real time: 832 gc time: 16

  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: 1 real time: 1 gc time: 0

  50 solutions

pythagorean 120:

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

  65 solutions

pythagorean 140:

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

  78 solutions

pythagorean 160:

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

  94 solutions

pythagorean 180:

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

  108 solutions

pythagorean 200:

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

  125 solutions

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) signals failure, causing the search to backtrack. This is commonly used as a pruning mechanism to discard branches that do not satisfy a constraint.

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

'(1 2 3)

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

'()

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))

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.

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.

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)

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)).

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) β†’ stream?

  thk : (-> any)
The procedural backend of in-amb.

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)

Similar to in-amb, but returns a generic sequence rather than specifically a lazy stream.

procedure

(in-amb*/do thk) β†’ sequence?

  thk : (-> any)
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. 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)