Recursividade em Prolog

filósofo
O poder da auto-referência para resolver problemas complexos

A recursividade é um dos conceitos mais fundamentais e poderosos da programação — e em Prolog, ela é ainda mais natural, pois a linguagem foi projetada para suportar definições recursivas de forma elegante. O princípio é simples: resolver um problema usando uma instância menor (mais simples) dele mesmo. Para que isso funcione, precisamos de dois elementos essenciais: um caso base (que para a recursão) e um passo recursivo (que reduz o problema e chama a si mesmo).

Exemplo Matemático: Potência (Programa 4.1)

Vamos calcular potência: \( B^N \) (base elevada ao expoente). Sabemos que:

  • Caso base: \( B^0 = 1 \) (qualquer número elevado a 0 é 1).
  • Passo recursivo: \( B^N = B \times B^{N-1} \) (para N > 0).
% pot(Base, Expoente, Resultado)
% Caso base: qualquer base elevada a 0 é 1
pot(_, 0, 1).

% Passo recursivo: B^N = B * B^(N-1)
pot(B, N, P) :-
  N > 0,
  M is N - 1,
  pot(B, M, R),
  P is B * R.

Estrutura de um Predicado Recursivo

Observe a estrutura:

  1. Caso base: pot(_, 0, 1). — a condição mais simples, que não chama a si mesmo.
  2. Passo recursivo: pot(B, N, P) :- N > 0, M is N-1, pot(B, M, R), P is B * R. — reduz o problema (N diminui), chama recursivamente e depois combina o resultado.

A guarda N > 0 garante que a recursão não seja chamada com expoente negativo, evitando loops infinitos.

Fluxo de Execução: pot(2, 3, P)

Veja como o Prolog processa a consulta passo a passo:

[EXPANSÃO] pot(2, 3, P) │ N = 3 > 0, M = 2 │ chama pot(2, 2, R1) │ │ │ ├─ [EXPANSÃO] pot(2, 2, R1) │ │ N = 2 > 0, M = 1 │ │ chama pot(2, 1, R2) │ │ │ │ │ ├─ [EXPANSÃO] pot(2, 1, R2) │ │ │ N = 1 > 0, M = 0 │ │ │ chama pot(2, 0, R3) │ │ │ │ │ │ │ ├─ [BASE] pot(2, 0, 1) ✅ │ │ │ │ │ │ │ └─ [RETORNO] R3 = 1 │ │ │ P é 2 * 1 = 2 │ │ │ R2 = 2 │ │ │ │ │ └─ [RETORNO] R2 = 2 │ │ P é 2 * 2 = 4 │ │ R1 = 4 │ │ │ └─ [RETORNO] R1 = 4 │ P é 2 * 4 = 8 └─ ✅ P = 8
Interpretação:
• O Prolog expande as chamadas recursivas até atingir o caso base.
• Depois, retorna os resultados de baixo para cima, combinando os valores.
• No final, P é unificado com 8 (que é \(2^3\)).

Exemplo do Fatorial (Programa 4.2)

O fatorial é outro exemplo clássico:

% fat(N, F) → F = N!
% Caso base: 0! = 1
fat(0, 1).

% Passo recursivo: N! = N * (N-1)!
fat(N, F) :-
  N > 0,
  M is N - 1,
  fat(M, R),
  F is N * R.

A estrutura é idêntica à da potência: um caso base (0! = 1) e um passo recursivo que reduz N até chegar a 0.

A Hipótese da Recursividade

Ao escrever um predicado recursivo, você faz uma suposição fundamental: assumir que a chamada recursiva funciona corretamente para uma instância menor do problema. Essa é a hipótese da recursividade. No caso da potência, assumimos que pot(B, M, R) realmente calcula \( B^M \) corretamente, e então usamos esse resultado para calcular \( B^N \). Essa confiança na chamada recursiva é o que torna a recursão poderosa e elegante.

Importante: A condição de parada (caso base) é obrigatória para evitar recursão infinita. Sem ela, o Prolog entraria em um loop até estourar a pilha de chamadas.

Exercícios Propostos

  1. Fibonacci: Implemente fib(N, F) que calcula o N-ésimo termo da sequência de Fibonacci (F(0) = 0, F(1) = 1, F(N) = F(N-1) + F(N-2)). Use dois casos base.
  2. Soma de lista: Implemente soma_lista([], 0). e soma_lista([H|T], S) :- soma_lista(T, R), S is H + R.
  3. Inverter lista: Crie inverter([], []). e inverter([H|T], Inv) :- inverter(T, R), append(R, [H], Inv).
  4. Pertencimento: Implemente membro(X, [X|_]). e membro(X, [_|T]) :- membro(X, T).
  5. Desafio: Implemente max_lista([X], X). e max_lista([H|T], M) :- max_lista(T, R), M is max(H, R).

Conclusão

A recursividade é o coração da programação lógica. Em Prolog, ela é expressa de forma natural através de regras que se referem a si mesmas, com um caso base que encerra a cadeia e um passo recursivo que reduz o problema. Esse padrão aparece em cálculos matemáticos (potência, fatorial), manipulação de listas, definição de estruturas de dados e muito mais. Dominar a recursividade é dominar o pensamento declarativo e estrutural que o Prolog exige. Pratique com os exercícios acima e explore a beleza da auto-referência lógica!

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.