4.3.1 Busca e amb
Para estender o JavaScript para suportar não determinismo, introduzimos uma nova forma sintática chamada amb.1 A expressão amb() retorna o valor de uma das expressões "ambiguamente." Por exemplo, a expressão
list(amb(1, 2, 3), amb("a", "b"));
// Pressione "Run" para a primeira solução. Digite
// retry
// no REPL à direita, para mais soluções
pode ter seis valores possíveis:
list(1, "a") list(1, "b") list(2, "a")
list(2, "b") list(3, "a") list(3, "b")
Uma expressão amb com uma única escolha produz um valor comum (único).
Uma expressão amb sem escolhas—a expressão amb()—é uma expressão sem valores aceitáveis. Operacionalmente, podemos pensar em amb() como uma expressão que quando avaliada faz a computação "falhar": A computação é abortada e nenhum valor é produzido. Usando esta ideia, podemos expressar o requisito de que uma expressão predicado particular p deve ser verdadeira da seguinte forma:
function require(p) {
if (! p) {
amb();
} else {}
}
Com amb e require, podemos implementar a função an_element_of usada acima:
function an_element_of(items) {
require(! is_null(items));
return amb(head(items), an_element_of(tail(items)));
}
Uma aplicação de an_element_of falha se a lista está vazia. Caso contrário, ela retorna ambiguamente ou o primeiro elemento da lista ou um elemento escolhido do resto da lista.
Também podemos expressar intervalos infinitos de escolhas. A seguinte função potencialmente retorna qualquer inteiro maior ou igual a algum dado:
function an_integer_starting_from(n) {
return amb(n, an_integer_starting_from(n + 1));
}
Isto é como a função integers_starting_from de streams descrita na seção 3.5.2, mas com uma diferença importante: A função de stream retorna um objeto que representa a sequência de todos os inteiros começando com , enquanto a função amb retorna um único inteiro.2
Abstratamente, podemos imaginar que avaliar uma expressão amb faz com que o tempo se divida em ramificações, onde a computação continua em cada ramificação com um dos valores possíveis da expressão. Dizemos que amb representa um ponto de escolha não determinístico. Se tivéssemos uma máquina com um número suficiente de processadores que pudessem ser alocados dinamicamente, poderíamos implementar a busca de uma maneira direta. A execução prosseguiria como em uma máquina sequencial, até que uma expressão amb fosse encontrada. Neste ponto, mais processadores seriam alocados e inicializados para continuar todas as execuções paralelas implicadas pela escolha. Cada processador prosseguiria sequencialmente como se fosse a única escolha, até que ou terminasse encontrando uma falha, ou subdividisse ainda mais, ou terminasse.3
Por outro lado, se tivermos uma máquina que pode executar apenas um processo (ou poucos processos concorrentes), devemos considerar as alternativas sequencialmente. Pode-se imaginar modificar um avaliador para escolher aleatoriamente uma ramificação para seguir sempre que encontrar um ponto de escolha. A escolha aleatória, no entanto, pode facilmente levar a valores que falham. Poderíamos tentar executar o avaliador repetidamente, fazendo escolhas aleatórias e esperando encontrar um valor sem falha, mas é melhor buscar sistematicamente todos os caminhos de execução possíveis. O avaliador amb que desenvolveremos e trabalharemos com nesta seção implementa uma busca sistemática da seguinte forma: Quando o avaliador encontra uma aplicação de amb, ele inicialmente seleciona a primeira alternativa. Esta seleção pode em si levar a uma escolha adicional. O avaliador sempre escolherá inicialmente a primeira alternativa em cada ponto de escolha. Se uma escolha resultar em uma falha, então o avaliador automagicamente4 retrocede (backtrack) ao ponto de escolha mais recente e tenta a próxima alternativa. Se ele esgotar as alternativas em qualquer ponto de escolha, o avaliador voltará ao ponto de escolha anterior e retomará de lá. Este processo leva a uma estratégia de busca conhecida como busca em profundidade ou backtracking cronológico.5
Driver loop
O loop de driver para o avaliador amb tem algumas propriedades incomuns. Ele lê um programa e imprime o valor da primeira execução sem falha, como no exemplo prime_sum_pair mostrado acima. Se quisermos ver o valor da próxima execução bem-sucedida, podemos pedir ao interpretador para retroceder e tentar gerar uma segunda execução sem falha. Isso é sinalizado digitando retry. Se qualquer outra entrada exceto retry for fornecida, o interpretador iniciará um novo problema, descartando as alternativas não exploradas no problema anterior. Aqui está um exemplo de interação:
prime_sum_pair(list(1, 3, 5, 8), list(20, 35, 110));
// Pressione "Run" para a primeira solução. Digite
// retry
// no REPL à direita, para mais soluções
Iniciando um novo problema
amb-evaluate value:
[3, [20, null]]
retry
amb-evaluate value:
[3, [110, null]]
retry
amb-evaluate value:
[8, [35, null]]
retry
Não há mais valores de
prime_sum_pair([1, [3, [5, [8, null]]]], [20, [35, [110, null]]])
prime_sum_pair(list(19, 27, 30), list(11, 36, 58));
// Pressione "Run" para a primeira solução. Digite
// retry
// no REPL à direita, para mais soluções
Iniciando um novo problema
amb-evaluate value:
[30, [11, null]]
Escreva uma função an_integer_between que retorna um inteiro entre dois limites dados. Isso pode ser usado para implementar uma função que encontra triplas pitagóricas, isto é, triplas de inteiros entre os limites dados tal que e , da seguinte forma:
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);
}
Solução:
// solução do usuário do GitHub jonathantorres
function an_integer_between(low, high) {
require(low <= high);
return amb(low, an_integer_between(low+1, high));
}
O exercício 3.69 discutiu como gerar o stream de todas as triplas pitagóricas, sem limite superior no tamanho dos inteiros a serem pesquisados. Explique por que simplesmente substituir an_integer_between por an_integer_starting_from na função do exercício 4.35 não é uma maneira adequada de gerar triplas pitagóricas arbitrárias. Escreva uma função que realmente conseguirá isso. (Isto é, escreva uma função para a qual digitar repetidamente retry em princípio eventualmente geraria todas as triplas pitagóricas.)
Ben Bitdiddle afirma que o seguinte método para gerar triplas pitagóricas é mais eficiente que o do exercício 4.35. Ele está correto? (Dica: Considere o número de possibilidades que devem ser exploradas.)
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);
}