Uso de fail para Iteração em Prolog

filósofo
Gerando todas as respostas automaticamente com backtracking forçado

Em Prolog, quando fazemos uma consulta que tem múltiplas respostas, o comportamento padrão é mostrar a primeira solução e aguardar o usuário pressionar ; (ponto-e-vírgula) para ver a próxima. Mas e se quisermos ver todas as respostas de uma vez, sem interação manual? É aí que entra o predicado fail/0.

O Problema: Respostas Sob Demanda

Considere a base de fatos:

d(0).
d(1).

A consulta ?- d(X). retorna:

?- d(X).
X = 0 % aguarda ‘;’ para continuar
% usuário pressiona ‘;’
X = 1
% fim

Para ver as duas respostas, o usuário precisa interagir. Em muitas situações — como ao gerar relatórios, listar todos os registros de um banco de dados ou depurar — queremos que o programa imprima todas as soluções automaticamente.

A Solução: fail + Efeitos Colaterais

O predicado fail/0 é um predicado pré-definido que sempre falha. Quando usado em conjunto com efeitos colaterais (como write/1 e nl/0), ele força o backtracking repetidamente, gerando todas as soluções.

Programa 3.5: Gerando Todas as Combinações Binárias

bin :-
  d(A), d(B), d(C),
  write([A, B, C]), nl,
  fail.

Vamos executar:

?- bin.
[0, 0, 0]
[0, 0, 1]
[0, 1, 0]
[0, 1, 1]
[1, 0, 0]
[1, 0, 1]
[1, 1, 0]
[1, 1, 1]
false

Todas as 8 combinações foram impressas automaticamente, sem necessidade de pressionar
; nenhuma vez!

Passo a Passo da Execução

Entenda como o Prolog processa bin :- d(A), d(B), d(C), write, nl, fail.:

  1. Gera: d(A) unifica com 0 (primeira solução).
  2. Gera: d(B) unifica com 0.
  3. Gera: d(C) unifica com 0.
  4. Processa: write([0,0,0]) imprime a lista, nl pula linha.
  5. Falha: fail força o backtracking para o ponto de escolha mais recente — d(C).
  6. Próxima: d(C) unifica com 1 (segunda solução).
  7. Repete: O ciclo continua: escreve [0,0,1], falha, backtracking…
  8. Esgotamento: Quando todas as combinações de d(A), d(B), d(C) se esgotam,
    o fail não tem mais para onde voltar, e a regra bin falha, retornando
    false.
Padrão clássico: Gerar → Processar → Falhar
Este é o padrão de iteração em Prolog. A geração produz valores, o processamento (write, nl) os exibe, e o fail força a busca pela próxima combinação.

Comparação com Loops de Outras Linguagens

O padrão gerar → processar → fail é análogo a loops em linguagens imperativas:

Python

for a in [0,1]:
  for b in [0,1]:
    for c in [0,1]:
      print([a,b,c])

Prolog

bin :-
  d(A), d(B), d(C),
  write([A,B,C]), nl,
  fail.

Em Python, o loop é explícito com for. Em Prolog, o backtracking substitui o loop
— a iteração é conduzida pelo mecanismo de busca do Prolog, não por uma construção de controle explícita.

⚠️ Cuidados com fail

Atenção: O uso incorreto de fail pode criar loops infinitos se não houver uma base que esgote as possibilidades.

Por exemplo, se você usar fail com um predicado que tem infinitas soluções (como between(0, inf, X)), o programa nunca terminará. Sempre certifique-se de que a parte “geradora” tem um número finito de soluções, ou inclua uma condição de parada.

Dica: Se você precisa processar todas as soluções e parar após um determinado número, combine fail com between/3 ou use o predicado forall/2 (que é uma abstração segura sobre esse padrão).

Exercícios Propostos

  1. Listar todos os números binários de 4 bits: Modifique o programa bin
    para gerar combinações de 4 dígitos ([A,B,C,D]).
  2. Listar todos os filhos de um pai: Crie uma base de fatos pai(pedro, joao).,
    pai(pedro, maria)., etc., e use fail para listar todos os filhos de Pedro.
  3. Listar todas as capitais de estados: Com fatos capital(estado, cidade),
    use fail para imprimir todas as capitais.
  4. Contar soluções: Use fail com um contador (inc/1)
    para contar quantas soluções existem para uma consulta.
  5. Desafio: Crie um predicado listar_solucoes(Goal) que recebe uma meta
    como argumento (usando call/1) e imprime todas as soluções usando fail.

Conclusão

O fail é uma ferramenta elegante que transforma o backtracking em um mecanismo de iteração. Combinado com efeitos colaterais, ele permite gerar, processar e exibir todas as soluções de uma consulta automaticamente.
Essa é uma das características que tornam o Prolog único: o fluxo de controle é guiado pela busca lógica, e não por instruções passo-a-passo. Use fail com sabedoria, e ele se tornará um aliado poderoso para explorar todas as respostas que seus programas podem oferecer.

O Operador de Corte ‘!’ em Prolog

filósofo
Controlando o backtracking com poder e responsabilidade

O operador de corte, representado por !, é uma das
ferramentas mais poderosas — e perigosas — do Prolog. Ele permite controlar o backtracking,
eliminando alternativas desnecessárias e otimizando a execução. Mas como toda ferramenta poderosa,
seu uso requer cuidado: um corte mal colocado pode eliminar soluções válidas e alterar
a semântica declarativa do programa.

O Problema: Backtracking Desnecessário

Considere a função matemática f(X) definida por partes, como no Programa 3.2:

% f(X) = 0 se X < 5
% f(X) = 1 se 5 =< X < 10
% f(X) = 2 se X >= 10

f(X, 0) :- X < 5.
f(X, 1) :- X >= 5, X < 10.
f(X, 2) :- X >= 10.

As regras são mutuamente exclusivas: para qualquer X, apenas uma delas
será verdadeira. No entanto, o Prolog não sabe disso. Ao consultar ?- f(3, Y)., ele:

  1. Tenta a primeira regra: X < 5 é verdadeiro, então Y = 0.
  2. Se pedirmos mais soluções (;), ele retrocede e tenta a segunda regra,
    testando X >= 5 (que falha).
  3. Depois tenta a terceira regra, que também falha.

Esse backtracking inútil desperdiça tempo, especialmente em programas com muitas regras
ou bases de dados grandes. Precisamos de uma forma de dizer ao Prolog: “se esta regra funcionou,
não tente as outras
”.

A Solução: O Operador de Corte (!)

O corte é um predicado especial que sempre é bem-sucedido, mas com
um efeito colateral: ele descarta todos os pontos de escolha criados
antes de sua posição dentro da mesma regra e descarta as cláusulas restantes para aquele
predicado. Veja o Programa 3.3 com cortes:

f(X, 0) :- X < 5, !.
f(X, 1) :- X >= 5, X < 10, !.
f(X, 2) :- X >= 10.

Agora, quando consultamos ?- f(3, Y).:

  • A primeira regra é ativada, X < 5 é verdadeiro.
  • O corte ! é executado: ele descarta as alternativas para f/2
    (as regras 2 e 3) e também descarta qualquer backtracking dentro da regra (embora não haja).
  • O Prolog não tenta mais as outras regras, economizando tempo.

Árvore de Busca: Antes e Depois do Corte

Visualize a diferença na árvore de busca para ?- f(3, Y).:

SEM CORTE
├─ f(3, Y)
│ ├─ Regra 1: X < 5 ✅ → Y = 0 (solução)
│ │ └─ ↩️ backtracking
│ ├─ Regra 2: X >= 5 ❌ (falha)
│ └─ Regra 3: X >= 10 ❌ (falha)

COM CORTE
├─ f(3, Y)
│ ├─ Regra 1: X < 5 ✅ → ! (solução)
│ │ └─ ✂️ pontos de escolha descartados
│ └─ (Regras 2 e 3 não são tentadas)

Estrutura Condicional com Corte (Programa 3.4)

O corte é frequentemente usado para implementar estruturas condicionais do tipo
if-then-else:

if(Condition, Then, Else) :-
  Condition, !,
  Then.
if(_, _, Else) :-
  Else.

A primeira cláusula tenta a condição. Se for bem-sucedida, o corte previne que a segunda
cláusula seja usada, e executa Then. Se a condição falhar, a primeira cláusula falha,
e o Prolog tenta a segunda (o Else).

Exemplo prático:
?- if(X < 5, write('pequeno'), write('grande')).
• Se X for instanciado com um valor, ele escreve a mensagem correta.
• O corte garante que apenas uma das mensagens seja escrita.

Os Perigos do Corte

Atenção: O corte não é negação e pode eliminar soluções válidas
se usado incorretamente.

Um exemplo clássico de erro:

% ERRADO — este corte elimina soluções legítimas
pai(X, Y) :- homem(X), !, filho(Y, X).

Aqui, o corte impede que o Prolog tente outras cláusulas de pai/2 mesmo que homem(X)
seja verdadeiro, eliminando potencialmente outros pais que poderiam ser encontrados em outras regras.

O corte é uma “fuga do paradigma declarativo puro”.
Em Prolog puro, a ordem das cláusulas não afeta o resultado (apenas a eficiência).
Com o corte, a ordem passa a importar semanticamente. Programas com cortes
são mais difíceis de ler, depurar e manter.

Boas Práticas com Corte

  • Use o corte apenas quando as cláusulas são mutuamente exclusivas e você tem certeza
    de que as alternativas não devem ser consideradas.
  • Coloque o corte após a condição que determina a exclusividade, não antes.
  • Prefira estruturas condicionais com -> (o operador condicional do Prolog)
    em vez de cortes manuais, quando possível.
  • Documente o uso do corte com comentários explicando por que ele está ali.

Exercícios Propostos

  1. Função sinal: Escreva a função sinal(X, S) que retorna S = -1
    se X < 0, S = 0 se X = 0, e S = 1 se X > 0.
    Use cortes para otimizar.
  2. Classificação de idade: Crie idade_class(X, C) onde C é
    'crianca' se X < 12, 'adolescente' se 12 =< X < 18,
    e 'adulto' se X >= 18. Use cortes.
  3. Teste sem cortes: Execute as mesmas funções sem cortes e compare
    o comportamento quando você pede soluções alternativas com ;.
  4. Desafio: Implemente a estrutura if/3 apresentada acima e teste com
    diferentes condições, incluindo casos em que a condição tem múltiplas soluções.

Conclusão

O operador de corte é uma ferramenta valiosa para otimização e controle
do backtracking, mas deve ser usado com moderação e consciência. Ele quebra a pureza
declarativa do Prolog, tornando o programa mais procedural. Em muitas situações, a ordenação
adequada das cláusulas
e o uso de cortes verdes (que apenas otimizam, sem
alterar o resultado) são suficientes. Use o corte quando a eficiência realmente importar, mas sempre
com a certeza de que ele não eliminará soluções corretas.