Edit page

Operador de Corte, Negação, Paragem/Execução Forçada

Operador de Corte

O operador de corte, !, é utilizado para indicar que, num programa onde um dado ramo produz soluções, devemos seguir esse ramo (algo do género "se já sabes que o que fizeste está bem, segue em frente."). Tal como o break de outras linguagens, só deve ser utilizado quando estritamente necessário (e, quando utilizado, devidamente comentado), visto que pode alterar inadvertidamente a semântica declarativa do programa. Cria, portanto, uma "barreira" no ramo da árvore SLD durante o retrocesso. Tem sempre sucesso, quando chamado.

Exemplo de um programa a utilizar o operador de corte:

% remove_repetidos(L1, L2)

remove_repetidos([], []).

remove_repetidos([P|R], L) :-
  membro(P, R), !, % caso seja membro, nem precisa de ir à cláusula seguinte
  remove_repetidos(R, L).

remove_repetidos([P|R], [P|L]) :- remove_repetidos(R, L).

% a interação resultante é correta:

?- remove_repetidos([a,c,c,a,b,c], L).
L = [a,b,c].

De notar que não é por não avançar para a próxima cláusula que a atual é "ignorada" - a atual é levada até ao fim, com ou sem sucesso.

Abaixo podemos observar duas versões diferentes de um programa que faz exatamente o mesmo, com diferentes colocações do operador de corte:

% parte(L, N, L1, L2)
% L1 contém os elementos de L menores que N, L2 os maiores ou iguais

% versão 1

parte([], _, [], []) :- !. % quando L é vazia, podemos começar o "retrocesso"

parte([P|R], N, [P|R1], L2) :-
  P < N, !, % caso P < N, não será necessário verificar a próx. cláusula
  parte(R, N, R1, L2).

parte([P|R], N, L1, [P|R2]) :-
  P >= N, % utilizar o corte aqui seria desnecessário, não há mais cláusulas
  parte(R, N, L1, R2).


% versão 2

parte([], _, [], []). % note-se a ausência do !

parte([P|R], N, [P|R1], L2) :-
  P < N, !, % aqui mantém-se igual
  parte(R, N, R1, L2).

parte([P|R], N, L1, [P|R2]) :-
  P >= N, !, % usado visto que a cláusula "caso-base" não tem !
  parte(R, N, L1, R2).

Note-se o posicionamento do operador de corte na versão 2 deste programa, que talvez possa fazer menos sentido à primeira vista. Visto que no retrocesso o terceiro argumento começa por ser a lista vazia, e a lista vazia não é do tipo [P|R1] (não tem primeiro elemento), não iria haver unificação com a cabeça da primeira regra, mesmo que fosse o que fizesse sentido. Assim sendo, temos de adicionar um corte à 2ª regra.

Podem testar esta diferença (presença/ausência do corte na 2ª regra) com o input parte([4,8,1,10], 7, [], [4,8,1,10]). Na versão correta, a resposta é false, visto que L1 devia ser [4,1] e L2 [8,10]; na versão errada, esta interação devolveria true.

Quick Sort

Fazendo a ponte com IAED, podemos ainda utilizar o programa definido acima, em conjunto com o append, para implementar um Quick Sort. A implementação passará por considerar um pivô, por ordenar, e dividir os restantes elementos em 2 grupos, menores e maiores ou iguais que ele (usando o parte); chamar o programa de novo até ordenar os dois grupos e colocar, por fim, o pivô entre os grupos, utilizando o append.

% quicksort(L1, L2) - L1 desordenada, L2 ordenada

quicksort([], []).

quicksort([P|R], L) :-
  parte(R, P, Menores, Maiores),
  quicksort(Menores, MenoresOrd),
  quicksort(Maiores, MaioresOrd),
  append(MenoresOrd, [P|MaioresOrd], L).

% interação possível:
?- quicksort([6,2,3,1,8,4,7,5],L).
L = [1, 2, 3, 4, 5, 6, 7, 8].
Junção de Listas Ordenadas

É possível realizar a junção de duas listas ordenadas sem o operador de corte. Contudo, por uma questão de eficiência, é usual aparecer o operador de corte nalguma posição da cláusula. Por exemplo:

% junta_ord(L1, L2, Res)

% quando uma das listas chega ao fim, realizamos a recursão
% da lista que ainda não acabou para trás
junta_ord(L, [], L) :- !. % não é preciso verificar mais
junta_ord([], L, L) :- !. % não é preciso verificar mais

junta_ord([P1|R1], [P2|R2], [P1|R]) :-
  P1 < P2, !, % se P1 < P2, não precisa de verificar mais
  junta_ord(R1, [P2, R2], R).

junta_ord([P1|R1], [P2|R2], [P1|R]) :-
  P1 = P2, !, % se P1 = P2, não precisa de verificar mais
  junta_ord(R1, R2, R).

junta_ord([P1|R1], [P2|R2], [P2|R]) :-
  P1 > P2, !, % não seria necessário, é colocado por consistência
  junta_ord([P1|R1], R2, R).

Ora, dadas as suas semelhanças com o break de outras linguagens, devemos também ser relembrados dos perigos inerentes ao operador de corte, pelo uso indevido que lhe podemos dar.

Peguemos num programa, menor, que nos dá o menor de dois números. Podemos defini-lo, sem recorrer ao corte, através de:

% menor(X, Y, Z) - Z é o menor entre X e Y

menor(X, Y, X) :- X =< Y.
menor(X, Y, Y) :- X > Y.

Podemos (e bem) tentar aplicar o operador de corte, tal que:

menor(X, Y, X) :- X =< Y, !.
menor(X, Y, Y) :- X > Y.

Podíamos, contudo, tentar (mal) sintetizar ainda mais este programa, tal que:

menor(X, Y, X) :- X =< Y, !.
menor(_, Y, Y). % ups

Este programa apresenta, no entanto, comportamentos errados. Na interação menor(5, 10, 10), por exemplo, devolveria true, quando claramente deveria devolver false. Isto acontece porque a unificação de menor(5, 10, 10) com menor(X, Y, X) falha, vendo-nos, portanto, forçados a alterar o nosso programa se quisermos manter esta estrutura que utiliza a variável anónima:

menor(X, Y, Z) :-
  X =< Y, !,
  Z = X.

menor(_, Y, Y).

Falhanço Forçado

O predicado fail/0 tem duas utilidades principais, sendo que apenas uma delas é vulgarmente utilizada. O seu propósito é criar um nó falhado propositadamente.

O primeiro propósito, menos usual, é para obter todas as respostas a um objetivo de uma vez, em vez de ter de utilizar o ; para verificar todas as respostas. Podemos observar uma interação deste género abaixo:

?- membro(X,[1,2,3]), writeln(membro(X,[1,2,3])), fail.
membro(1,[1,2,3])
membro(2,[1,2,3])
membro(3,[1,2,3])
false.
% esta interação é realizada sem a necessidade de ;
% o próprio ; também não aparece

O segundo propósito, mais utilizado e bastante poderoso, é utilizar o fail em conjunto com o operador de corte. Um exemplo bastante simples para ilustrar a sua utilidade é o de verificar se duas listas são disjuntas - duas listas são disjuntas quando não têm nenhum membro em comum, pelo que basta haver um para o objetivo retornar false. Assim sendo, é interessante combinar um operador de corte com um fail, tal que:

% disjuntas(L1, L2)

disjuntas([], _) :- !.
disjuntas(_, []) :- !.

% caso seja membro, nem precisa de verificar mais, é falso de certeza
disjuntas([P1|_], L2) :-
  member(P1, L2),
  !, fail.

disjuntas([_|R1], L2) :- disjuntas(R1, L2).

Negação

A combinação mencionada acima, combinar o fail com o corte, permite definir a negação por falhanço, diferente da negação lógica. Esta negação é baseada na hipótese do mundo fechado, mencionada na introdução ao Prolog. Se o Prolog não consegue derivar algo, assume que é falso.

Em Prolog, este tipo de negação é utilizado através de um meta-predicado, \+, aplicado a literais.

Observemos o exemplo:

\+(P) :-
  P,
  !, fail.

\+(P).

Este programa pode ser lido tal que "para responder ao objetivo \+(P), tente-se provar P. Caso não seja possível, o objetivo é satisfeito; caso contrário, retorne-se false".

Em relação a um exemplo concreto:

voa(P) :- ave(P), \+ pinguim(P).
% P voa caso seja uma ave que não um pinguim

ave(gelido).
ave(piupiu).
pinguim(gelido).

% interações possíveis

?- voa(gelido).
false.
?- voa(piupiu).
true.
?- voa(X).
X = piupiu ;
false.

A interação acima pode ser ilustrada por:

Piupiu Gelido

A negação por falhanço não funciona, contudo, corretamente para objetivos não chãos (ou seja, para objetivos com variáveis).

Execução Forçada

Apesar de teoricamente uma regra ter o formato <literal> :- <literais>, podemos supor a hipótese de literal ser "nada". A regra ficaria, então, com um aspeto do género :- <literais>, algo do género "para provar "nada", prove os literais a seguir a :-". Pode ser bastante útil em casos de tentar fazer debug (i.e :- writeln('Este é o passo <n> do programa')), ou até mesmo para definir certos acontecimentos que acontecem sempre ao carregar um certo ficheiro no Prolog.

Por exemplo, se utilizarem a GUI do SWI-Prolog, podem ir às definições e ao user init file e escrever alguns comandos que serão forçosamente executados, como por exemplo:

% dark mode
:- use_module(library(theme/dark)).

% mensagem ao entrar - deverão ver isto ao abrir o SWI-Prolog
:- writeln('This is a test!').

% se estiverem na vossa diretoria default e quiserem abrir x ficheiros
% pode ser útil no projeto para não andarem sempre a abrir o ficheiro
:- [<nome do ficheiro>].
...