To extend
JavaScript
to support nondeterminism, we introduce a new
syntactic form
called amb.[1]
The expression
amb($e_1,\ e_2,\ldots, e_n$)
returns the value of one of the $n$ expressions
$e_i$ ambiguously.
For example,
the expression
list(amb(1, 2, 3), amb("a", "b"));
list(1, "a") list(1, "b") list(2, "a") list(2, "b") list(3, "a") list(3, "b")
An amb expression
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:
function require(p) {
if (! p) {
amb();
} else {}
}
With amb and require, we can implement the an_element_of function used above:
function an_element_of(items) {
require(! is_null(items));
return amb(head(items), an_element_of(tail(items)));
}
We can also express infinite ranges of choices. The following function potentially returns any integer greater than or equal to some given $n$:
function an_integer_starting_from(n) {
return amb(n, an_integer_starting_from(n + 1));
}
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.[3]
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[4] 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.[5]
The driver loop for the amb evaluator has some unusual properties. It reads a program 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 retry. If any other input except retry is given, the interpreter will start a new problem, discarding the unexplored alternatives in the previous problem. Here is a sample interaction:
amb-evaluate input:
prime_sum_pair(list(1, 3, 5, 8), list(20, 35, 110));
Starting a new problem amb-evaluate value: [3, [20, null]]
amb-evaluate input:
retry
amb-evaluate value: [3, [110, null]]
amb-evaluate input:
retry
amb-evaluate value: [8, [35, null]]
amb-evaluate input:
retry
There are no more values of prime_sum_pair([1, [3, [5, [8, null]]]], [20, [35, [110, null]]])
amb-evaluate input:
prime_sum_pair(list(19, 27, 30), list(11, 36, 58));
Starting a new problem amb-evaluate value: [30, [11, null]]
function a_pythogorean_triple_between(low, high) {
const i = an_integer_between(low, high);
const j = an_integer_between(i, high);
const k = an_integer_between(j, high);
require(i * i + j * j === k * k);
return list(i, j, k);
}
// solution by GitHub user jonathantorres
function an_integer_between(low, high) {
require(low <= high);
return amb(low, an_integer_between(low+1, high));
}
function a_pythagorean_triple_between(low, high) {
const i = an_integer_between(low, high);
const hsq = high * high;
const j = an_integer_between(i, high);
const ksq = i * i + j * j;
require(hsq >= ksq);
const k = math_sqrt(ksq);
require(is_integer(k));
return list(i, j, k);
}
Automatically, but in a way which, for some reason (typically because it is too complicated, or too ugly, or perhaps even too trivial), the speaker doesn't feel like explaining.(