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.

Backtracking em Prolog

filósofo
Explorando a árvore de busca com números binários

O backtracking é o mecanismo fundamental que torna o Prolog uma linguagem
de busca e resolução de problemas. Quando uma consulta é feita, o Prolog tenta satisfazer os objetivos
na ordem em que aparecem. Se um objetivo falha, ele volta atrás (backtrack) e tenta
outra alternativa, até encontrar todas as soluções possíveis. Vamos entender esse processo com um exemplo
clássico: a geração de números binários de 3 bits.

O Programa 3.1 — Números Binários

% Fatos: dígitos binários válidos
d(0).
d(1).

% Regra: uma lista de 3 bits é formada por 3 dígitos binários
b([A, B, C]) :- d(A), d(B), d(C).

O predicado d/1 define os dois dígitos binários possíveis. A regra b/1
afirma que uma lista de três elementos [A, B, C] é um número binário se A,
B e C forem todos dígitos binários.

Objetivo da Consulta: ?- b(N).

Ao executar ?- b(N)., o Prolog deve gerar todas as combinações possíveis
de 3 bits: [0,0,0], [0,0,1], [0,1,0], e assim por diante, até
[1,1,1] — totalizando 8 soluções.

A Árvore de Busca Passo a Passo

O Prolog constrói uma árvore de busca onde cada nó representa um objetivo a ser provado.
Acompanhe a execução abaixo:

?- b(N).

├─ N = [A, B, C] (unificação com a cabeça da regra)

├─ d(A)
│ ├─ A = 0 ✅ sucesso
│ └─ A = 1 (backtracking após esgotar B e C)

├─ d(B)
│ ├─ B = 0
│ └─ B = 1

├─ d(C)
│ ├─ C = 0 [0,0,0] (1ª solução)
│ └─ C = 1 [0,0,1] (2ª solução)

├─ ↩️ Backtracking em B (após C=1, falha ao tentar C=2)
│ └─ B = 1
│ ├─ C = 0 [0,1,0] (3ª)
│ └─ C = 1 [0,1,1] (4ª)

├─ ↩️ Backtracking em A (após esgotar B e C com A=0)
│ └─ A = 1
│ ├─ d(B) → B = 0
│ │ ├─ C = 0 [1,0,0] (5ª)
│ │ └─ C = 1 [1,0,1] (6ª)
│ └─ B = 1
│ ├─ C = 0 [1,1,0] (7ª)
│ └─ C = 1 [1,1,1] (8ª)

└─ ❌ Falha total — todas as combinações foram exploradas

🔎 Interpretação:
• O Prolog tenta primeiro a solução mais à esquerda (A=0, B=0, C=0).
• Ao pressionar ; (ponto-e-vírgula) no terminal, ele retrocede no C e tenta C=1.
• Quando C se esgota, retrocede no B e tenta B=1, depois C=0, C=1, e assim por diante.
• O processo continua até esgotar todas as combinações de A, B e C.

Resolução e Redução de Objetivos

O Prolog utiliza um mecanismo chamado resolução SLD (Selected Linear Definite).
A resolução é o processo de unificar a consulta com a cabeça de uma regra e substituir
a consulta pelo corpo da regra. A redução ocorre quando um objetivo é substituído por
subobjetivos (ex: b(N) é reduzido a d(A), d(B), d(C)). A árvore acima mostra
exatamente essa cadeia de reduções e tentativas de unificação.

Ordem das Cláusulas e Objetivos

A ordem dos fatos e das metas no corpo das regras afeta diretamente
a forma da árvore de busca. Por exemplo, se definíssemos d(1). d(0). (invertendo a ordem),
a primeira solução seria [1,1,1] em vez de [0,0,0]. A ordem dos objetivos
também importa: d(C), d(B), d(A) geraria a mesma lista de soluções, mas em ordem diferente.

💡 Dica de eficiência: Coloque os objetivos mais restritivos primeiro
para podar a árvore de busca cedo. Por exemplo, se você está buscando d(A), A > 0,
é mais eficiente testar A > 0 antes de chamar d(A)? Na verdade, A
precisa estar instanciado para a comparação, então a ordem ideal depende do caso — mas em geral,
metas que geram poucas soluções devem vir antes.

Exercício Proposto

Modifique a ordem dos objetivos na regra b/1 para:

b([A, B, C]) :- d(C), d(B), d(A).

Perguntas:
1. Qual será a primeira solução gerada?
2. Como a árvore de busca se altera?
3. O número total de soluções (8) continua o mesmo?
4. O que acontece se mudarmos a ordem dos fatos d(0). d(1). para d(1). d(0).?

Teste no seu interpretador Prolog e observe o comportamento. Essa prática ajuda a internalizar
como o backtracking e a ordem influenciam a execução.

✅ Conclusão

O backtracking é o motor que impulsiona a busca em Prolog.
Ele é automático e controlado pelo motor de inferência — o programador não precisa
gerenciar pilhas ou loops manualmente. Entender a árvore de busca é essencial para:

  • Prever a ordem das soluções;
  • Escrever regras eficientes (colocando metas restritivas primeiro);
  • Controlar o fluxo com corte (!) quando necessário;
  • Depurar programas que não terminam ou que geram soluções inesperadas.

Dominar o backtracking é dar um grande passo para se tornar um programador Prolog confiante e produtivo.