Nondeterministic programming is a technique wherein the flow of an algorithm is not linear, and there exists multiple possible continuations. The behavior of a computation can also change with the same inputs. There are several methods to achieve nondeterminism. In this article the method that I’ll use is backtracking.

Additionally, I’ll use Scheme to do it. In Scheme, you are permitted to go back in an early computation and back, later, with ease.

I will also discuss the prerequisite topics to make nondeterminism in Scheme easier to understand.

In Scheme, you can capture the current continuation—the next computation—as a variable. You can do that with call/cc. It is an operator which accepts only one argument exclusively—a lambda—an anonymous function, with one argument:

(call/cc (lambda (k) k))

If you don’t have call/cc define it with:

(define call/cc call-with-current-continuation)

The phrase call with current continuation can also be treated as call with next computation. The function that it calls is the lambda. Additionally, call/cc passes the current continuation—the next computation—to that anonymous function.

In this lambda:

(lambda (k) k)

k is a function itself which accepts one argument. So, in

(call/cc (lambda (k) k))

call/cc returns the current continuation—simply the function. However, in

However, you may notice that you did not apply the function k to anything. The expression (* 1 2) is evaluated, then the result goes to (+ _ 3). In other words, that expression is functionally equivalent to:

One of the uses of the famous amb operator in Scheme is to implement non-deterministic programming by means of backtracking. With that mechanism, the computation can go to an earlier state; carry computations; change change; escape computations; and more.

In this article we’re going to use the amb operator to enable the backtracking mechanism.

In this section, we are going to deconstruct the definitions of the amb operator and other functions. The function really? technically is not part of the definition, however, we’ll use it to demonstrate the functionality of amb.

Here are superficial steps of the definitions:

In line 1 call/cc is bound to call-with-current-continuation mainly for implementations which doesn’t have that definition.

In line 3 f is bound to a default value.

In lines 5 to 16 is the body of amb.

In lines 18 to 21 is an example function: if x and y are equal, it returns a list; if not, amb is called.

In lines 23 to 26, call/cc is called, in which the true initial value of f is initialized with a lambda which returns 'no-choices.

Let’s go back to the body of amb in lines 5 to 16. In line 7, if amb is called as such:

(amb)

the function f is called, with whatever that f can do.

In line 8, if amb is called as such:

(amb "dog")

the argument "dog" is simply returned.

However, with more than one arguments, the behavior of amb changes. Firstly, in line 10, the current value of f is bound to s, next in line 11, call/cc is called, capturing the current continuation to k.

Inside the body of the lambda, the global variable f will have a new value—another lambda—which will not be evaluated until it is explicitly requested. In that body, f will have the earlier value of s, then k will be called with the value b ..., which are the remaining arguments for amb.

Lastly, the k function—the remaining computation—will be applied to the value a.

In this article, we noticed that with call/cc we have easily achieved non-determinism through backtracking with amb and basic Scheme functions. However, there are more elaborate and better methods of achieving this.