Você está na página 1de 42

I – Programas em Lógica

1 – Construções Básicas
As construções básicas da programação em lógica, termos e frases (statements), são
inerentes à lógica. Existem 3 tipos de frases básicas: factos, regras e consultas.
Há só uma estrutura de dados: o termo lógico.

1.1. FACTOS
O tipo de frase mais simples é o facto. Os factos são meios de dizer que existe uma
relação entre objectos.
ex: father(abraham,isaac)
Um outro nome para uma relação é predicado. Nomes de indivíduos são conhecidos
como átomos.
A relação plus (mais) pode ser definida à custa de factos, através duma tabela longa,
como por exemplo plus(0,0,0). plus (0,1,1). etc.
 Os predicados e átomos nos factos começam por letra minúscula.
Um conjunto finito de factos constitui um programa lógico. É também a descrição de
uma situação. Este ponto de vista é a base da programação de bases de dados.
ex: de base de dados de uma relação familiar:
father(terach,abraham). male(terach).
father(terach,nachor). male(abraham).
father(terach,haran). male(nachor).
father(abraham,isaac). male(haran).
father(haran,lot). male(isaac).
father(haran,milcah). male(lot).
father(haran),yiscah).
female(sarah).
mother(sarah,isaac). female(milcah).
female(yiscah).
(Program 1.1.)

1.2. CONSULTAS (Queries)


A segunda forma de frase é a consulta. Consultas são meiOS de encontrar
informação a partir de um programa lógico. Uma consulta pergunta se uma dada
relação existe entre objectos. ex: father(abraham,isaac)?. a resposta é yes.
Sintacticamente as consultas e os factos parecem similares. NO entanto eles
distinguem-se pelo contexto. Mas mesmo que haja dúvidas, o facto termina por ponto
final e a consulta por ?.
Chamamos à entidade sem o . ou o ? de goal (meta). P. diz que P é true P? pergunta
onde P é true.
Uma consulta simples consiste num único goal.
Responder a uma consulta com respeito a um programa é determinar quando a query
é uma consequência lógica do programa.
As consequências lógicas são obtidas por aplicação das regras de dedução.
 A mais simples regra de dedução é a identidade: de P deduz-se P. Uma
consulta é uma consequência lógica de um facto idêntico.
Se o programa for só de factos, como o 1.1. é só percorrê-los para responder yes ou
no a uma query. Isso não quer dizer que a query não seja verdadeira.

1.3. A VARIÁVEL LÓGICA, SUBSTITUIÇÕES E INSTÂNCIAS


 Uma variável lógica diz respeito a um indivíduo não especificado.
ex: father(abraham, X)? X é a variável lógica, e ajuda a expressar melhor a
consulta, que também podíamos fazer por father(abraham,lot)?
father(abraham,milcah)? etc...
Desta maneira, sumarizam várias queries.
Uma consulta contendo uma variável pergunta onde estão os valores que tornam a
query uma consequência lógica do programa.
São diferentes das variáveis nas linguagens convencionais pois representam uma não
especificada mas única entidade e não uma localização de memória.
Termo é a única estrutura de dados em programas lógicos. A definição é indutiva.
Constantes e variáveis são termos. Também termos compostos e estruturas são
termos.
Um termo composto é formado por um functor (chamado o rpincipal functor do termo)
e uma sequência de um ou mais argumentos, que são termos. Um functor é
caracterizado pelo seu nome, que é um átomo, e a sua aridade ou número de
argumentos. Sintacticamente f(t1,t2,...,tn).
Queries, goals, ou mais genericamente, termos onde não ocorrem variáveis, são
chamados ground. Se ocorrerem variáveis são unground.

Substituição é um conjunto finito (eventualmente vazio) de pares da forma Xi=t1 ,


onde Xi é uma variável e ti é um termo, e Xi≠Xj para todo o i≠j, e Xi não ocorre em tj
para nenhum i e j.
um exemplo consistindo num único par é {X=isaac}. As substituições podem ser
aplicadas aos termos. O resultado de aplicar uma substituição Θ a um termo A,
denotado por AΘ, é que o termo obtido por substituição de em qualquer ocorrência de
X por t em A, para todo o par X=t em Θ.
O resultado de aplicar {X=isaac} ao termo father(abraham,X) é o termo
father(abraham,isaac).

A é uma instância de B se há uma substituição Θ tal que A=BΘ

1.4. QUERIES EXISTENCIAIS


Logicamente falando, as variáveis nas queries são existencialmente quantificadas,
embora, por conveniência, a quantificação existencial seja geralmente omitida.
2ª regra de dedução – generalização: Uma query existencial P é uma consequência
lógica de uma instância dela, PΘ, para qualquer substituição Θ.
Operacionalmente, para responder a uma query não ground usando um programa de
factos, procura-se um facto que é uma instância da query. Se encontrado, a resposta,
ou solução, é a instância.
A solução é representada, neste capítulo, pela substituição que, se aplicada à query,
resulta na solução.

1.5. FACTOS UNIVERSAIS


As variáveis também são úteis em factos.
ex: fact likes(X,pomegranates) diz que todos gostam em vez de especificar um a um.
Desta maneira, as variáveis são meios de sumarizar vários factos. ex: times(0,X,0).
As variáveis em factos estão implicitamente quantificadas universalmente.
Logicamente, de um facto universalmente quantificado podemos deduzir qualquer
instância dele.
esta é a 3ª regra de dedução, chamada instanciação: De uma frase universalmente
quantificada P, deduz-se uma isntância dela, PΘ, para qualquer substituição Θ.
ex: plus(0,X,X). likes(X,X).
Responder a uma query ground com um facto universalmente quantificado é directo.
ex: plus(0,2,2) é yes, baseado no facto plus(0,X,X).
Responder a uma query não ground usando um facto não ground envolve uma nove
definição:
C é uma instância comum de A e B se for uma instância de A e uma instância de B,
isto é, se houver substituições Θ1 e Θ2 tais que C=AΘ1 é sintacticamente idêntica a
BΘ2..
ex: plus(0,3,Y) e plus(0,X,X) têm uma isntância comum plus(0,3,3).
Em geral, para se responder a uma query usando um facto, procura-se por uma
instância comum ao facto e à query. A resposta é a instância comum se existir, se não
é não.
Responder usando a instância comum envolve instanciação e generalização.

1.6. QUERIES CONJUNTIVAS E VARIÀVEIS PARTOLHADAS


Consultas conjuntivas são uma conjunção de goals postas numa query única. ex:
father(terach,X), Father(X,Y)? a vírgula significa and.
As consultas conjuntivas são interessantes quando há uma ou mais variáveis
partilhadas, variáveis que ocorrem em duas diferentes metas da query. ex:
father(haran,X), male(X)?
O scope/alcance de uma variável numa query conjuntiva é toda a conjunção.
As variáveis partilhadas são usadas como meio de constrangimento de uma simples
query restringindo o alcance de uma variável.
Um uso diferente de uma variável partilhada pode ser visto em father(terach,X),
father(X,Y)?
Uma query conjuntiva é uma consequência lógica de um programa P se todas as
metas na conjunção são consequências de P, onde as variáveis partilhadas são
instanciadas para os mesmos valores em diferentes metas.
Uma condição suficiente é que há uma instância ground da query que é consequência
de P. Esta instância deduz então os conjunctos na query via generalização. A restrição
a ground não é necessária, como se vrá no cap.4

1.7. REGRAS
 Queries conjuntivas interessantes definem, elas próprias, relações.
ex: father(terach,X), father(X,Y)? questiona sobre os netos de terach.
Isto leva-nos à terceira, e mais importante, frase na programação em lógica – regra –
que nos permite definir novas relações em termos de relações existentes:
São da forma A <-- B1,B2,...,Bn.
A é a cabeça da regra e a conjunção de metas Bs são o corpo da regra.
Regras, factos e queries são também chamadas de cláusulas Horn ou,
simplesmente, cláusulas. De notar que um facto é um caso especial de uma regra
quando n=0. Os factos são também chamados cláusulas unitárias.
Se n=1 é chamada cláusula iterativa
tal como para os factos, as variáveis que aparecem nas regras são universalmente
quantificados e o seu alcance é toda a regra.
ex: son(X,Y) <--father(Y,X), male(X). ou, no meu Prolog :- em vez de <--
daughter(X,Y) <-- father(Y,X), female(X).
grandfather(X,Y) <-- father(X,Z), father(Z,Y).
Cap. 2 – PROGRAMAÇÃO DE BASES DE DADOS
Há 2 estilos básicos de usar programas lógicos: definindo uma base de dados lógica e
manipular estruturas de dados.
Uma base de dados lógica contém um conjunto de factos e regras.
Veremos como um conjunto de factos pode definir relações, como nas bases de dados
relacionais, e como as regras podem definir complexas consultas relacionais, como na
álgebra relacional.

2.1. BASES DE DADOS SIMPLES


Usaremos a base de dados bíblica do exemplo 1.1. e vamos aumentá-la com regras
expressando as relações familiares.
A base de dados tem, ela própria, 4 predicados: father/2, mother/2 male/1 e female/1.
vamos adoptar a convenção da teoria das bases de dados, que dá a cada relação um
esquema de relação que especifica o papel que cada posição na relação (ou
argumento na meta) representa. Esquemas de relação são por exemplo:
father(Father,Child), male(Person).
As variáveis têm nomes mnemónicos nas regras, mas habitualmente X ou Y quando
em consultas.
Variáveis de várias palavras: NieceOrNephew
Predicados de várias palavras: schedule_conflict
Do ponto de vista lógico não é importante que relações são definidas por factos ou por
regras.
Regras interessantes podem ser obtidas criando relações explicitamente, a partir das
que estão implícitas na base de dados.
Quantas mais relações há mais fácil se torna definir relações complexas.

uncle(Uncle,Person) <--
brother(Uncle, Parent), parent(Parent, Person).
sibling(Sib1, Sib2) <--
parent(Parent, Sib1), parent(Parent, Sib2), Sib1≠Sib2.
cousin(Cousin1, Cousin2) <--
parent(Parent1, Cousin1), parent(Parent2,Cousin2), sibling(Parent1, Parent2).
(Programa 2.1.)

com:

brother(Brother, Sib) <-- parent(parent,Brother), parent(Parent,Sib), male(Brother),


Brother ≠ Sib.

Posso definir ≠ por uma colecção de factos do tipo: abraham≠isaac.

Passando a outro exemplo: Um circuito lógico.

resistor(power,n1).
resistor(power,n2).
transistor(n2,ground,n1).
transistor(n3,n4,n2).
transistor(n5,ground,n4).

inverter(Input,Output) <--
transistor(Input, ground, Output),
resistor(power,Output).
nand_gate(Input1,Input2,Output) <--
Output is the logical nand of Input1 e Input2.
nand_gate(Input1,Input2,Output) <--
transistor(input1,X,Output), transistor(Input2, ground, X),
resistor(power,Output).

and_gate(Input1,Input2,Output) <--
Output is the logical and of Input1 and Input2
and_gate(Input1,Input2,Output) <--
nand_gate(Input1,Input2,X),
inverter(X,Output).
Programa 2.2.
A primeira linha da definição (ex: nand_gate) é apenas o esquema de relação para o
procedimento. Isto enfatiza a leitura descritiva dos programas.
ex: de consulta and_gate(In1,In2,Out)?

2.2. DADOS ESTRUTURADOS E DADOS ABSTRACTOS


Uma limitação do programa anterior é o tratamento do circuito como caixa negra. Não
há qualquer indicação da estrutura do circuito na resposta à consulta and_gate, apesar
da estrutura ter sido usada implicitamente para se encontrar uma resposta. As regras
dizem-nos que o circuito repesenta uma and_gate mas a estrutura da and_gate está
presente apenas implicitamente. Podemos remediar isso acrescentando um
argumento extra para cada uma das metas na base de dados. Para uniformidade,
esse argumento extra é o primeiro sempre.
Os nomes dos componentes funcionais devem reflectir a sua estrutura. Um inversor é
composto por um transistor e um resistor. Para representar isto predcisamos de dados
estruturados. A técnica é usar um termo composto, inv(T,R), onde T e R são os nomes
respectivos dos componentes do inversor. Analogamente para outros componentes.
Ficamos com:
resistor(R,Node1,Node2) <--
R is a resistor between Node1 and Node2.
resistor(r1,power,n1).
resistor(r2,power,n2).
transistor(T,Gate,Source,Drain) <--
T is a transistor whose gate is Gate,
source is Source, and drain is Drain.
transistor(t1,n2,ground,n1).
transistor(t2,n3,n4,n2).
transistor(t3,n5,ground,n4).
inverter(I,Input,Output) <---
I is an inverter that inverts Input to Output.
inverter(inv(T,R),Input,Output) <--
transistor(T,Input,ground,Outpu),
resistor(R,power,Output).
nand_gate(Nand,Input1,Input2,Output) <--
Nand is a gate forming the logical nand, Output,
of Input1 and Input2.
nand_gate(nand(T1,T2,R),Input1,Input2,Output) <--
transistor(T1,Input1,X,Output),
transistor(T2,Input2,ground,X),
resistor(R,power,Output).
and_gate(And,Input1,Input2,Output) <-
And is a gate forming the logical and, Output, of Input1 and Input2.
and_gate(and(N,I),Input1,Input2,Output) <--
nand_gate(N,Input1,Input2,Output),
inverter(I,X,Output).
Programa 2.3.
A consulta and_gate(G,In1,In2,Out)? tem a solução {G=and(nand(t2,t3,r2),inv(t1,r1)),
In1=n3, In2=n5, Out=n1}
a estruturação de dados é importante em programação em geral e em programação
em lógica em particular. É usada para organizar dados duma forma significativa. As
regras podem ser escritas duma forma mais abstracta, ignorando pormenores
irrelevantes. Programas mais modulares podem ser conseguidos.

ex:
a) course(complexity, monday, 9,11,david,harel,feinberg,a).
b) course(complexity,time(Monday,9,11),lecturer(david,harel),location(feinberg,a)).
As regras que não usem valores particulares de um argumento estruturado não
precisam saber como o argumento está estruturado. Ver ex: abaixo no prog.2.4. no
caso de time.
Não há regras definitivas: Não usar dados estruturados conduz a uma maior
uniformidade de representação quando os dados são simples. As vantagens da
estruturação dos dados são a compactação da representação, que mais
apuradamente reflecte a nossa perspectiva da situação e modularidade.

lecturer(Lecturer,Course) <--
course(Course,Time,Lecturer,Location).
duration(Course,Length) <--
course(Course,time(Day,Start,Finish),Lecturer,Location),
plus(Start,Length,Finish).
teaches(Lecturer,Day) <--
course(Course,time(Day,Start,Finish),Lecturer,Location).
ocupied(Room,Day,Time) <--
course(Course,time(Day,Start,Finish),Lecturer,Room),
Start ≤ Time, Time ≤ Finish.
Programa 2.4.

2.3. REGRAS RECURSIVAS


As regras até agora descritas definem novas relações em termos de relações já
existentes. Uma extensão interessante são as definições reursivas de relações que
definem relações em termos delas próprias.
Uma maneira de ver as regras recursivas é como generalização de um conjunto de
regras não recursivas.
ancestor(ancestor,Descendant) <--
ancestor is an ancestor of Descendant.
ancestor(ancestor,Descendant) <--
parent(Ancestor,Descendant).
ancestor(ancestor,Descendant) <--
parent(ancestor,Person), ancestor(Person,Descendant).
Programa 2.5.
3.2. LISTAS
A estrutura básica para a aritmética é o functor sucessor unário, o que é limitado para
funções recursivas mais complicadas.
Por isso definimos agora a estrutura binária lista.
O primeiro argumento de uma lista guarda um elemento e o segundo argumento é
recursivamente o resto da lista.
As listas são suficientes para a amior parte das comptuações
Para as listas, tal como para os números, um símbolo constante é necessário para
terminar a recursão. Esta lista vazia, refrerida como nil, será denotada pelo sínmbolo [
].
:(X,Y) é denotado por [X|Y] X é chamada a cabeça da lista e Y a cauda

Definindo uma lista:


list(Xs) <--
list([ ]).
list([X|Xs]) <-- list(Xs).
A figura seguinte dá uma árvore de prova para a meta list([a,b,c]).

list([a,b,c])
list([b,c])
list([c])
list([ ])

[a,b,c] é uma instância de [X|Xs] debaixo da substituição {X=a, Xs=[b,c]}.


Porque as listas são estruturas de dados mais ricas do que os números, uma grande
variedade de interessantes relações podem ser especificadas com elas. Talvez a mais
básica operação com listas seja determinar se um dado elemento está nalista. O
predicado é member(Element,List)

Prog. 3.12.
member(Element, List) <--
member(X,[X|Xs]).
member(X,[Y|Ys]) <-- member(X,Ys).

alternativamente:
member(X,[X|Xs]) <-- list(Xs)

Este programa tem muitos usos como por exemplo: verificar se um elemento está
numa lista; encontrar um elemento duma lista; encontrar uma lista que contém um
elemento.
X denota a cabeça da lista Xs denota a cauda. Mais em geral variáveis no plural
denota listas de elementos e nomes singulares denota elementos individuais. Sufixos
numéricos denotam variantes de listas.

sublist(Sub,List) determina se Sub é uma sublista de List mas requer elementos


consecutivos.
Dois casos especiais de sublistas são os sufixos e prefixos de uma lista.

Prog. 3.13.
prefix(Prefix, List) <--
prefix([ ], Ys).
prefix([X|Xs], [X|Ys]) <-- prefix(Xs,Ys).

suffix(Suffix, List) <--


suffix((Xs,Xs).
suffix(Xs,[Y|Ys]) <-- suffix(Xs,Ys).

Uma sublista arbitrária pode ser dada em termos de sufixos de prefixos ou prefixos de
sufixos, como mostra o programas seguinte:
este programa expressa a regra lógica que Xs é uma sublista de Ys se existe Ps tal
que Ps é um prefixo de Ys e Xs é um sufixo de Ps. O programa seguinte é dual:
O predicado prefix também pode ser usado como base para uma definição recursiva
de sublista (prog. 3.14c.) A regra básica diz que um prefixo de uma lista é uma sublista
da lista. A regra recursiva diz que a sublista de uma cauda de uma lista é uma sublista
da lista.
O predicado member pode ser visto como um caso especial de uma sublista definida
pela regra: member(X,Xs) <-- sublist([X],Xs).

sublist(Sub,List) <--
sublist(Xs,Ys) <-- prefix(Ps,Ys), suffix(Xs,Ps).
sufixo de um prefixo
sublist(Xs,Ys) <-- prefix(Xs,Ss), suffix(Ss,Ys).
prefixo de um sufixo
sublist(Xs,Ys) <-- prefix(Xs,Ys).
sublist(Xs,[Y|Ys]) <-- (Xs,Ys).
Definição recursiva de uma sublista

A operação básica com listas é concatenar duas listas para dar uma terceira lista:
Prog. 3.15.
append(Xs,Ys,XsYs) <--
append([ ], Ys, Ys).
append([X|Xs], Ys, [X|Zs]) <-- append(Xs,Ys,Zs).

Árvore de prova para a meta append([a,b],[c,d],[a,b,c,d]).


append([a,b],[c,d],[a,b,c,d])
append([b],[c,d],[b,c,d])
append([ ],[c,d],[c,d])

Se Xs tem n elementos, o comprimento da árvore de prova tem n+1 nós.

Há muitos usos para append. O mais básico é concatenar 2 listas pondo uma query
como: append([a,b,c],[d,e],Xs)?
Uma query como append(Xs, [c,d],[a,b,c,d])? encontra a diferença (esta não é
simétrica como em plus) Xs=[a,b]

O processo análogo a partir um número é spliting uma lista. A consulta


append(As,Bs,[a,b,c,d])?
As queries sobre partir listas tornam-se mais interessantes se especificarmos a
natureza das listas partidas.
Os predicados member, sublist, prefix, suffix, podem todos ser definidos em termos de
append vendo o processo como splitar lista.
ex:
prefix(Xs,Ys) <-- append(Xs,As,Ys).
suffix(Xs,Ys) <-- append(As,Xs,Ys).
sublist(Xs,AsXsBs) <-- append(As,XsBs,AsXsBs), append(Xs,Bs,XsBs).
prefixo de um sufixo usando append
sublist(Xs,AsXsBs) <- append(AsXs,Bs,AsXsBs), append(As,Xs,AsXs).
sufixo de um prefixo usando append
member(X,Ys) <-- append(As,[X|Xs],Ys).
adjacent(X,Y,Zs) <-- append(As,[X,Y|Ys],Zs).
last(X,Xs) <-- append(As,[X],Xs).

Program 3.16.
reverse(List,Tsil) <--
reverse([ ],[ ]).
reverse([X|Xs],Zs) <-- reverse(Xs,Ys), append(Ys,[X],Zs).
(naive reverse)

reverse(Xs,Ys) <-- reverse(Xs,[ ],Ys).


reverse([X|Xs],Acc,Ys) <-- reverse(Xs,[X|Acc],Ys).
reverse([ ],Ys,Ys).
(reverse-acumulado)
Nesta definição definimos um predicado auxiliar reverse(Xs,Ys,Zs), que dá true se Zs
for o resultado de appending Ys aos elementos de Xs invertidos. É + eficiente.
O tamanho da árvore de prova deste é linear com nº de elementos e da 1ª é
quadrático. Usa uma melhor estrutura de dados para representar a sequência de
elementos (ver cap. 7 e 15).
árvore da 1ª versão (naive)
reverse([a,b,c],[c,b,a])
reverse([b,c],[c,b]) append([c,b],[a],[c,b,a])
reverse([c],[c]) append([c],[b],[c,b]) append([b],[a],[b,a])
reverse([ ], [ ]) append([ ],[c],[c] append([ ],[b],[b] append([ ],[a],[a])
árvore da 2ª versão (acc)
reverse([a,b,c],[c,b,a])
reverse([a,b,c],[ ],[c,b,a])
reverse([b,c],[a],[c,b,a])
reverse([c],[b,a],[c,b,a])
reverse([ ],[c,b,a],[c,b,a])

Program 3.17.
lenght(Xs,N) <--
lenght([ ], 0).
lenght(X|Xs], s(N)) <-- lenght(Xs,N).

3.3. COMPOSIÇÂO DE PROGRAMAS RECURSIVOS


Até agora não foi dada qualquer explicação da forma como se constróem os
programas recursivos que mostrámos anteriormente.
É uma tarefa que tem de ser aprendida por aprendizagem ou osmose, mas sobretudo
através da prática.
Nesta secção iremos ver mais programas com listas pondo mais ênfase na forma
como se podem construir os programas.
2 princípios serão ilustrados:
Como pensar de forma declarativa e procedimental
Como desenvolver um programa top-down

Pragmaticamente, nós pensamos procedimentalmente quando programamos e


pensamos declarativamente quando consideramos em termos de verdade e
significado. Entã, devemos programar procedimentalmente e interpretar o resultado
declarativamente.
Aplicando a um programa para deletar elementos duma lista.
Facilmente chegamos à conclusão que o esquema é delete(L1,X,L2)
também ajuda se, quando compomos um programa pensarmos em usos para ele.
ex: delete([a,b,c,b],b,[X)? que dará X=[a,c]
O programa será recursivo no 1º argumento.
Começamos com a parte recursiva. A forma habitual para argumentos recursivos de
listas é [X|Xs]. Há duas possibilidades a considerar: quando X é o elemento a deletar e
quando não é.
No primeiro caso, o resultado de recursivamente deletarmos X de Xs é o resultado
desejado para a query. A regra apropriada é:
delete([X|Xs],X,Ys) <-- delete(Xs,X,Ys)
Declarativamente.”O apagamento de X de [X|Xs] é Ys se o apagamento de X de Xs for
Ys”
Para o 2ª caso a regra é: delete([X|Xs],Z,[X|Ys] <-- X≠Z, delete(Xs,Z,Ys).
Declarativamente: “O apagamento de Z de [X|Xs] é [X|Ys] se Z for diferente de X e ao
apagamento de Z de Xs for Ys”.
O caso base é directo. Não podem ser apagados elementos da lista vazia e o
resultado é também uma lista vazia.

Programa 3.18.
delete(List,X,HasNoXs) <--
delete([X|Xs],X,Ys) <-- (Xs,X,Ys).
delete([X|Xs],X,[X|Ys]) <-- X≠Z, delete(Xs,Z,Ys).
delete([ ],X,[ ]).
Uma variante é não pormos X≠Z. Mas isso tem um significado menos natural, pois
qualquer número (1,2,3,etc.) de ocorrências de X será apagado.

Em ambas delete([a],b[a]) é true. Há aplicações em que isto não é desejado. Então


definimos select:
select(X,L1,L2) que dá todas as instâncias ground onde L2 é a lista L1 onde foi
removida apenas uma ocorrência de X.

Prog.3.19.
select(X,HasX,OneLessXs) <--
select(X,[X|Xs],Xs).
select(X,[Y|Ys],[Y|Zs] <-- select(X,Ys,Zs).

Uma coisa importante na programação é a metodologia de design top-down, junta com


o refinamento passo a passo. Esta metodologia consiste em partir o problema em
vários subproblemas e resolver as várias partes.
Vamos aplicar a permutation sort e quicksort.

Uma especificação lógica de ordenar uma lista é encontrar uma permutação ordenada
duma lista.
sort(Xs,Ys) – ordem ascendente - <-- permutation(Xs,Ys), ordered(Ys).
Agora temos de definir permutation e ordered.
Testar se uma lista está ordenada:
ordered([X]).
ordered([X,Y|Ys]) <-- X ≤ Y, ordered([Y|Ys]).
A permutação é mais delicada: uma maneira é seleccionar um elemento ao calhas
para ser o 1º elemento da lista permutada, e depois, recursivamente, permutar o resto
da lista:
permutation(Xs,[Z|Zs]) <-- select(Z,Xs,Ys), permutation(Ys,Zs).
permutation([ ], [ ]).
Uma outra maneira procedimental é permutar recursivamente a cauda da lista e inserir
a cabeça numa posição arbitrária:
permutation([X|Xs],Zs) <-- permutation(Xs,Ys), insert(X,Ys,Zs).
permutation([ ],[ ]).
com insert(X,Ys,Zs) <-- select(X,Zs,Ys).
Prog.3.20.
versão naive/permutações – paradigma “gerar-e-testar”
sort(Xs,Ys) <--
sort(Xs,Ys) <--permutation(Xs,Ys), ordered(Ys).
permutation(Xs,[Z|Zs]) <-- select(Z,Xs,Ys), permutation(Ys,Zs).
permutation([ ], [ ]).
ordered([ ]).
ordered([X]).
ordered([X,Y|Ys]) <-- X≤Y, ordered([Y|Ys]).

Melhores algoritmos resultam de apliocar o paradigma “dividir para conquistar”. O


objectivo é ordenar uma lista dividindo-a em 2 partes e recursivamente ordenar as
partes. No fim juntam-se as partes.
Há 2 posições extremas para fazer isso:
A primeira é tornar a divisão difícil a e a junção fácil -> quicksort algoritmo
A segunda é o contrário -> merge sort
No quicksort, escolhe-se um elemento arbitrário (no caso abaixo é o primeiro) e parte-
se a lista em 2 partes (uma contendo elementos maiores que ele e outra menores).

Programa 3.22.
quicksort(Xs,Ys) <--
(a lista Ys é uma permutação ordenada da lista Xs)
quicksort([X|Xs],Ys) <--
partition(Xs,X,Littles,Bigs),
quicksort(Littles,Ls),
quicksort(Bigs,Bs),
append(Ls,[X|Bs],Ys).
quicksort([ ],[ ]).
partition([X|Xs],Y,[X|Ls],Bs) <-- X≤Y, partition(Xs,Y,Ls,Bs).
partition([X|Xs],Y,Ls,[X\Bs] <-- X > Y, partition(Xs,Y,Ls,Bs).
partition([ ],Y,[ ],[ ]).

A regra recursiva para quicksort lê-se: “Ys é uma versão ordenada de [X|Xs] se Littles
e Bigs forem o resultado de particionar Xs de acordo com X; Ls e Bs são o resultado
de ordenar Littles e Bigs recursivamente; e Ys é o resultado de appending [X|Bs] a Ls.”
A leitura declarativa da primeira cláusula de partition é: “Particionar uma lista cuja
cabeça é X e cuja cauda é Xs, de acordo com um elemento Y dá as listas [X|Littles] e
Bigs se X é menor ou igual a Y, e particonar Xs de acordo com Y dá as listas Littles e
Bigs.”
II – A Linguagem Prolog
Para implementar uma linguagem de programação prática e baseada no modelo
computacional da programação lógica, três aspectos precisam de atenção_
Resolver as escolhas que permanecem na interpretação abstracta para os programas
lógicos (ver cap.4)
Enriquecer as expressividades da computação pura do modelo, acrescentando
facilidades meta-lógicas e extra-lógicas.
Aceder a algumas capacidades do computador, como aritmética rápida e prover
input/output.

6 – Prolog Puro
É um programa lógico no qual uma ordem é definida, quer para as cláusulas (clauses)
quer para as metas (goals) no corpo da cláusula.
O interpretador está preparado para tirar vantagens da ordenação da informação.
O Prolog puro é uma realização aproximada da programação lógica do modelo
computacional, numa máquina sequencial.

6.1. O Modelo de Execução do Prolog


Maiores decisões para converter o interpretador abstracto para uma forma de sucesso
para uma linguagem de programação concreta:
A secolha arbitrária de qual meta no resolvente, reduzir, nomeadamente a política de
agendamento, tem de ser especificada.
A escolha não de terminista da cláusula do programa para efectuar a redução, tem de
ser implementado.
O Prolog está baseado na execução sequencial.
O Prolog escolhe a meta mais à esquerda e escolhe a cláusula por procura sequencial
para uma cláusula unificável e backtracking.
Por outras palavras, adopta uma política de agendamento tipo stack. Mantém o
resolvente como uma pilha: tira a meta de topo para redução e põe as metas
derivadas no stack do resolvente.
Também e em adição, Prolog simula a escolha não determinista da cláusula de
redução por procura sequencial e backtacking. Quando tenta reduzir uma meta, a
primeira cláusula cuja cabeça unifica com a meta é escolhida. Se não encontra
cláusula unificável para a meta tirada do stack, volta à última escolha efectuada e a
próxima cláusula unificável é escolhida.
Uma computação duma meta G com respeito a um programa Prolog P é a geração de
todas as soluções de G com respeito a P. .... = particular árvore de procura de G
obtida escolhendo sempre a meta mais à esquerda.
Edimburgh = standard
:- igual <--
Um trace de um programa Polog é uma extensão do trace duma computação dum
programa lógico debaixo do interpretador abstractyo (ver 4.2.)
A notação usada para traces tem de ser estendida para manipular falhas e
backtracking
f é falha, isto é, não há cláusula cuja cabeça unifique com a meta. A segui há
backtacking se ocorre uma falha.
; indica uma computação para procurar mais soluções.
ex:
father(abraham, isaac). male(isaac).
father(haran, lot). male(lot).
father(haran, milcah). female(yiscah).
father(haran, yscah). female(milcah).

son(X,Y) <-- father(Y,X), male(X).


daughter(X,Y) <-- father(Y,X), female(X).

son(X,haran)?
father(haran, X) X=lot
male(lot)
true
Output: X=lot
;
father(haran,X) X=milcah
male(milcah) f
father(haran, X) X=yiscah
male(yiscah) f
no more solutions

fig. 6.1. Tracing se uma computação Prolog simples.

Tracing computações é uma boa maneira de ganhar compreensão da execução do


modelo do Prolog.

append([X|Xs], Ys, [X|Zs]) <-- append(Xs, Ys, Zs).


append([ ], Ys, Ys).

append(Xs, Ys, [a,b,c]) Xs=[a|Xs1]


append(Xs1, Ys, [b,c]) Xs1=[b|Xs2]
append(Xs2,Ys,[c]) Xs2=[c|Xs3]
append(Xs3,Ys,[ ]) Xs3=[ ], Ys=[ ]
true
Output: (Xs=[a,b,c], Ys=[ ])
;
append(Xs2,Ys,[c]) Xs2=[ ], Ys[c]
true
Output: (Xs=[a,b], Ys=[c]
;
append(Xs1,Ys,[b,c]) Xs1=[ ], Ys=[b,c]
true
Output: (Xs=[a], Ys=[b,c])
;
append(Xs,Ys,[a,b,c]) Xs=[ ], Ys=[a,b,c]
true
Output: (Xs=[ ], Ys=[a,b,c])
;
no (more) solutions

fig. 6.2. Multiple solutions for splitting a list

quicksort([X|Xs], Ys) <--


partition(Xs,X,Littles, Bigs),
quicksort(Littles,Ls),
quicksort(Bigs,Bs),
append(Ls,[X|Bs],Ys).
quicksort([ ], [ ]).
partition([X|Xs,Y,[X|Ls],Bs) <--
X ≤ Y, partition(Xs,Y,Ls,Bs).
partition([X|Xs], Y, Ls,[X|Bs] <--
X > Y, partition (Xs,Y,Ls,Bs).
partition([ ], Y, [ ] , 8 ]).
quicksort ([2,1,3], Qs)
partition([1,3],2,Ls,Bs) Ls=[1|Ls1]
1≤2
partition([3],2,Ls1,Bs) Ls1=[3|Ls2]
3 ≤ 2 false
partition([3],2,Ls1,Bs) Bs=[3|Bs1]
3>2
partition([ ],2,Ls1,Bs1) Ls1=[ ]=Bs1
quicksort([1],Qs1)
partition([ ],1,Ls2,Bs2) Ls2=[ ] =Bs2
quicksort([ ],Qs2) Qs2=[ ]
quicksort([ ],Qs3) Qs3= [ ]
append([ ], [1], Qs1) Qs1=[1]
quicksort([3],Qs4)
partition([ ],3,Ls3,Bs3) Ls3=[ ] =Bs3
quicksort([ ],Qs5) Qs5=[ ]
quicksort([ ],Qs6) Qs6= [ ]
append([ ], [3], Qs4) Qs4=[3]
append([19,[2,3],Qs) Qs=[1,Ys]
append([ ],[2,3],Ys) Ys=[2,3]
true
Output: (Qs=[1,2,3])

fig. 6.3. Tracing uma computação de quicksorting (ver livro pg.123)

Introduzimos agora uma distinção entre shallow e deep backtacking.


Shallow backtracking ocorre quando a unificação entre uma meta e uma cláusula falha
e uma cláusula alternativa é tentada.
Deep backtracking ocorre quando a unificação da última cláusula dum procedimento
com uma meta, falaha e o controlo retorna a outra meta na árvore de computação.

6.2. Comparação com Linguagens de Programação Convencionais


Uma linguagem de programação é caracterizada pelos seus mecanismos de controlo
e de manipulação de dados.
O controlo em Prolog é como nas linguagens procedimentais.
Invocações de metas/goals correspondem a invocação de procedimentos e a ordem
das metas no corpo da cláusula correspondem a uma sequência de instruções.
ex:
A <-- B1, ..., Bn corresponde a:
procedimento A
call B1,
call B2,
...
call Bn,
end.

Invocação de metas recursivas em Prolog é similar em comportamento e


implementação às das linguagens convencionais. As diferenças aparecem quando o
backtracking aparece: Numa linguagem convencional, se uma computação não pode
prosseguir (ex: todas as condições duma instrução case são falsas) ocorre um run-
time error. Em Prolog a computação é simplesmente não-realizada para a última
escolha feita e um caminho de computação diferente é tentado.
As estruturas de dados manipuladas pelos programas lógicos, termos, correspondem
a estruturas de registo gerais. O manuseamento de estruturas de dados é muito
flexível em Prolog. Como no Lisp, Prolog é uma linguagem de declarações livres,
linguagem sem tipos.

A maior diferença no uso das estruturas de dados vem da natureza das variáveis
lógicas. Elas referem-se a indivíduos em vez de localizações de memória. Não suporta
pois assignment destrutivo.

A manipulação de dados nos programas lógicos é atingida inteiramente via algoritmo


de unificação. Unificação subsume:
assignement único; Passagem de parâmetros; Alocação de registos;
leitura/escrita-uma vez acesso aos campos nos registos.

No exemplo da figura 6.3. a unificação da meta inicial quicksort([2,1,3], Qs) com a


cabeça da definição de procedimento quicksort([X|Xs], Ys) ilustra uma data de coisas.
A unificação de [2,1,39 com o termo [X|Xs] consegue acesso ao registo à lista e
também selecção dos dosi campos, a cabeça e a cauda.
A unificação de [1,3] com Xs é uma passagem de parâmetros para o procedimento
partition, por causa da partilha de variáveis.
A criação dum registo pode ser vista com a unificação da meta partition([1,3],2,Ls,Bs)
com a cabeça do procedimento partition([X|Xs],Z,[X|Ls1],Bs1). Como resultado Ls é
instanciado a [1|Ls1]. Especificamente, Ls é feita numa lista e a sua cabeça é
assignada ao valor 1, nomeadamente, criação de registo e assignamento de campo
via unificação.

A ideia básica da compilação do Prolog é traduzir casos especiais de unificação para


manipulação de memória convencional na máquina de von Neumann.

O algoritmo recursivo incorporado em quicksort pode ser visto como manipulação de


ponteiros em listas.

O Prolog não tem manipulação de erros e excepções. Dá sempre falha.

O que vimos até aqui não responde à questão mais interessante: Como se pode
comparar programar em Prolog com programar nas convencionais linguagens de
programação. É o que iremos ver até ao final do livro.

CAP. 7
Programação em Prolog Puro
O maior objectivo da programação em lógica é permitir ao programador programar a
um alto nível. Idealmente deveríamos escrever axiomas que definem as relações
desejadas, mantendo ignorância do caminho que serão usadas pelo mecanismo de
execução.
As linguagens correntes, como o Prolog, ainda estão longe desse objectivo ideal da
programação declarativa.
Programas lógicos efectivos requerem conhecimento de como os mecanismos de
execução fazem as escolhas.
Este capítulo discute as consequências do modelo de execução do Prolog para o
programador.

7.1. Ordem das Regras


Duas coisas irrelevantes para os programas em lógica, são importantes em
composição de programss em Prolog: A ordem das regras, ou a ordem das cláusulas.
Também a ordem das metas nos corpos de cada clúsula deve ser determinado.
As decisões destas escolhas pode ser imensa no que respeita a eficiência, que pode
até ir à não terminação em casos mais extremos.
A ordem das regras determina a ordem em que as soluções são encontradas.
Mudar a ordem das regras num procedimento permuta as derivações em qualquer
árvore de procura para uma meta usando aquele procedimento.
Este facto é claramente visto quando usamos factos para responder a uma consulta
existencial. Mudando a ordem dos factos na nossa árvore bíblica altera a ordem das
soluções encontradas. Mas, a ordem dos factos não é importante.

A ordem de soluções de consultas resolvidas por recursividade também é determinada


pela ordem das cláusulas.
Ex:
parent(terach,abraham). parent(abraham,isaac).
parent(isaac,jacob). parent(jacob,benjamin).

ancestor(X,Y) <-- parent(X,Y).


ancestor(X,Z) <-- parent(X,Y), ancestor(Y,Z).

Para a consulta ancestor(terach,X)? as soluções são dadas pela ordem: X=abraham,


X=isaac, X=jacob e X=benjamin.
Se alterasse a ordem das cláusulas, a ordem das soluções seria exactamente ao
contrário. Isto porque a ordem de procura na árvore de procura seria alterada. Num
caso, à medida que vai procurando vai dando soluções, noutro vai até ao fim da árvore
e depois vai dando as soluções de trás para a frente.
A ordem desejada para as soluções é determinada pela aplicação.

Quando a árvore de procura para uma dada meta tem ramificações infinitas, a ordem
das cláusulas determina se alguma solução é dada ou não.
ex: append(Xs,[c,d],Ys)? em 5.4. não dá nada, mas se mudarmos dá uma infinidade.

Não há consenso de como ordenar as cláusulas em Prolog. Nas linguagens


cionvencionais deve-se testar a condição terminal antes de prosseguir com a recursão
ou iteração. Isto não é mandatório em prolog. A causa é que o teste é feito pela
unificação e as cláusulas são independentes.
Há outros controlos, nomeadamente o cut (cap.11) que dependem da ordem das
cláusulas, mas com estas construções a scláusulas perdema sua indepedência e a
ordem das cláusulas ganha sentido.

Para já adoptamos a regra que as cláusulas recursivas precedem as cláusulas base.

7.2. Terminação.
O atravessamento em profundidade das árvores de procura tem um sério problema.
Se a procura de uma meta em respeito a um programa, tem derivações infinitas, a
computação não pára. Prolog pode falhar a procura de uma solução mesmo sabendo
que a meta tem computação finita.
A não terminação cresce com as regras recursivas.
Ex: acrescento
married(Male,Female)
um facto seria:
married(abraham,sarah)
como a ordem não interessa, acrescentaríamos:
married(X,Y) <-- married(Y,X)
Mas então nenhuma computação envolvendo married terminaria.
ex:
married(X,Y) <--married(Y,X).
married(abraham, sarah).
married(abraham,sarah)
married(sarah,abraham)
married(abraham,sarah)
married(sarah,abraham)
....

Regras recursivas que têm uma meta recursiva como primeira meta são conhecidas
como regras recursivas à esquerda, que são inerentemente causadoras de problemas
em Prolog, pois causam computações não terminais se chamadas com argumentos
impróprios. O melhor é evitá-las.
As relações comutativas (como o caso anterior) são melhor manejáveis definindo um
novo predicado que tem uma cláusula para cada permutação dos argumentos. No
nosso caso poderíamos resolver com:

are_married(X,Y) <-- married(X,Y).


are_married(X,Y) <-- married(Y,X).

Infelizmente não é geralmente possível remover todas as ocorrências de recursividade


à esquerda. Todos os elegantes e minimal recursivos programs lógicos do capítulo 3
são recursivos à esquerda e podem causar não-terminação.
Em todo o caso, uma análise apropriada, usando os conceitos de domínios e
compeltitude dados na secção 5.2. podem determinar quais as consultas que
terminam, com respeito a programas recursivos.
Por exemplo: programa 3.15. para append duas listas. Ele é sempre teminável se o
conjunto de metas para os quais o primeiro e/ou último argumento seja uma lista
completa. Também termina se o 1º ou 3º argumento é um termo ground (não lista).
Ele não termina se ambos os 1º e 3º termos são listas incompletas que são unificáveis.
outro ex: no programa 3.12. (member) se o 2º argumento é uma lista incompleta
também não termina.

Uma outra garantida forma de gerar programas não termináveis é usar definições
circulares, como por ex:
parent(X,Y) <-- child(Y,X).
child(X,Y) <-- parent (Y,X).
parent(haran,lot)?, não termina.

7.3. Ordem das Metas.


A ordem das metas é mais significativa do que a ordem das cláusulas. É o principal
meio de expressar a sequência do fluxo de controlo em programas Prolog.
Por ex. o programa 3.22. para ordenar listas explora a ordem das metas para indicar a
sequência de passos nos algoritmos de sort.
1º - Ordem das metas na perspectiva da programação de bases de dados
A ordem das metas pode afectar a ordem das soluções.
ex: daughter(X,haran)? se a ordem dos factos female(milcah) e female(yiscah) for
alterada. As soluções vêm na ordem X=milcah e X=Yiscah.
Se a ordem das metas for alterada para daughter(X,Y) <-- female(X), father(Y,X) a
ordem das soluções muda também.
A razão é diferente da anterior (ordem das cláususlas). Alterar a ordem das regras não
altera a árvore de procura. A árvore é apenas atravessada numa ordem diferente.
Alterara ordem das metas altera a árvore.
A ordem das metas determina a árvore de procura.
Isto pode alterar o tamanho da árvore e consequentemente o tempo de
procura/eficiência. A ideia é tentar que a procura falhe o mais rapidamente possível
quando isso tem de acontecer.
A ordem de metas óptima varia com os diferentes usos.
EX: Para a definição de grandparent há duas possíveis regras.
grandparent(X,Z) <-- parent(X,Y), parent(Y,Z).
grandparent(X,Z) <-- parent(Y,Z), parent(X,Y).
se quiser encontrar o neto de alguém, grandparent(abraham,X)? a primeira das regras
procura mais directamente.
Se quiser encontrar o avô de alguém, grandparent(X,isaac)? a segunda é mais rápida.
Se a eficiência é importante justifica-se usar duas relações distintas, grandparent e
grandchild.

Em contraste com a ordem das cláusulas, a ordem das metas pode determinar se a
computação termina.
Ex:
ancestor(X,Y) <-- parent(X,Z), ancestor(Z,Y).
Se as metas no corpo forem alteradas, o programa ancestor torna-se recursivo à
esquerda e não termina.

No programa de quicksorting também não posso alterar a ordem da partição e


quicksort recursivo.

No programa 3.16. para inverter uma lista:


reverse([ ], [ ]).
reverse([X|Xs],Zs) <-- reverse(Xs,Ys), append(Ys,[X],Zs).
A ordem das metas é importante. Como dito, o programa termina com metas onde o 1º
argumento é uma lista completa.
Metas onde o 1º argumento é uma lista incompleta não terminam. Se as metas na
regra recursiva forem trocadas o factor determinante é o 2º argumento.

Um mais subtil exemplo vem da definição do predicado sublist em termos de 2 metas


append especificando a sublist como sufixa ou prefixa (prog. 3.14e.)
Considerando a query sublist([2,3],[1,2,3,49)?
A query é reduzida a append(AsXs, Bs, [1,2,3,4]), append(As,[2,3],AsXs)? e tem uma
árvore finita de procura.
Mas se trocarmos a ordem das metas a consulta inicial será reduzida a
append(As,[2,3],AsXs),append(AsXs,Bs,[1,2,3,4])?, o que leva a uma computação não
terminativa devido à primeira meta.

Uma regra heurística para a ordem das metas no caso de programas recursivos com
testes tais como comparações aritméticas ou determinando onde duas constantes são
diferentes: Colocar os testes o mais cedo possível.
Um exemplo é a partition
partition([X|Xs],Y,[X|Ls9,Bs) <-- X ≤ Y, partition(Xs,Y,Ls,Bs).
No Prolog, em contraste com as outras linguagens de programação, a ideia é falhar o
mais cedo possível.

7.4. Soluções Redundantes


É importante em Prolog, embora irrelevante nos programas lógicos, devido aos
problemas de eficiência. É desejável manter a árvore de procura o mais pequena
possível. Pode resultar em crescimento exponencial em runtime, no backtracking.

Uma maneira de fazer a redundância aparecer é cobrir o mesmo caso com várias
regras.
Ex:
minimum(X,Y,X) <-- X ≤ Y.
minimum(X,Y,Y) <-- Y ≤ X.
a consulta minimum(2,2,M)? tem duas soluções, sendo uma redundante.
Uma solução seria, após exame minucioso, alterar a segunda regra para
minimum(X,Y,Y) <-- Y < X.

Um cuidado similar é necessário na definição de partition como parte da definição de


quicksort. O programador deve assegurar-se que apenas uma das regras recursivas
de partition cobre o caso quando o número a ser comparado é o mesmo que o número
a ser usado para partir a lista.

Uma outra maneira de introduzir redundância aparece em programas que têm muitos
casos especiais.
Se adicionássemos um facto extra append(Xs,[ ],Xs) , isso aconteceria.
Para remover redundância, cada uma das outras cláusulas para append teria de cobrir
apenas listas com, pelo menos, um elemento como segundo argumento.

ex:
merge(Xs,Ys,Zs) <--
Zs é uma lista ordenada de inteiros obtida de mergir as listas ordenadas Xs e
Ys
merge([X|Xs], [Y|Ys],[X|Zs]) <-- X < Y, merge(Xs,[Y|Ys],Zs).
merge([X|Xs],[Y|Ys],[X,X|Zs]) <-- X=:=Y, merge(Xs,Ys,Zs).
merge([X|Xs], [Y|Ys], 8Y|Zs]) <-- X > Y, merge([X|Xs], Ys,Zs).
merge([ ], [X|Xs], [X|Xs]).
merge(Xs,[ ],Xs).

Há 3 cláusula recursivas separadas, que cobrem os 3 possíveis casos: quando a


cabeça da primeira lista é menor que a da segunda, igual ou maior.
São precisos mais dois casos para quando os elementos de cada lista chegaram ao
fim.
De notar que tivémos o cuidado de que a meta merge([ ], [ ], [ 9) ser coberta apenas
por um facto (o último).

outro exemplo é o member definido em 3.12 que é redundante para queries do tipo:
member(a,[a,b,a,c]).
Definimos então uma versão não redundante:
member_check(X,Xs) <--
X é um membro da lista Xs
member_check(X,[X|Xs).
member_check(X,[Y|Ys]) <-- X ≠ Y, member_check(X,Xs).
Isto tem a ver como a maneira que ≠ é tratado no Prolog (cap.11).

7.5. Programação Recursiva em Prolog Puro


As listas são uma muito útil estrutura de dados para muitas aplicações em Prolog.
ex1:
select_first(X,Xs,Ys) <--
Ys é a lista obtida por remoção da primeira ocorrência de X da lista Xs.
select_first(X,[X|Xs],Xs).
select_first(X,[Y|Ys],[Y|Zs]) <-- X ≠ Y, select_first(X,Ys,Zs).

ex2:
select(X,[X|Xs],Xs).
select(X,[Y|Ys,[Y|Ys]) <-- select(X,Ys,Zs).
este programa é similar ao member de 3.19. e não há problema com a ordem das
metas pois ele é minimal recursivo.
se select(X,[a,b,c],Xs) , dá 3 soluções para X e Xs
O programa só não termina se ambos os 2º e 3º argumentos forem lista incompletas.
Uma variante para este programa é se acrescentarmos o teste X≠Y à cláususla
recursiva. Como antes, assumimos que ≠ está apenas definido para argumento
ground. Somos conduzidos ao programa select_first, que tem um signifaicado
diferente de select, ao passo que member_check tinha um significado igual a member.
ex3:
permutation(Xs,[X|Ys]) <-- select(X,Xs,Zs), permutation(Zs,Ys).
permutation([ ], [ ]).
computações de permutation metas onde o primeiro argumento for uma lista completa,
terminam. Se for incompleta, porque o goal select não termina, é interminável.
Se a ordem das metas em permutation for trocada, é o 2º argumento que é
significativo para a terminação.
CAP.8 – ARITMÉTICA
Os programas lógicos para trabalhar a aritmética, dados na secção 3.1. são muito
elegantes mas não são práticos.
Isto porque não se pode ignorar os recursos que qualquer computador tem para as
operações aritméticas. Então temos de estender o Prolog puro para ter também
system predicates (evaluable predicates, builtin predicates, bips)

8.1. PREDICADOS DE SISTEMA PARA ARITMÉTICA


O preço a pagar é que algumas operações aritméticas orientadas pela máquina não
são tão gerais como as lógicas, mas ganha-se eficiência.
ex: Standard Prolog tem um sp is(Value,Expression). is é infixo.
Os operadores são usados para tornarem os programas mais legíveis.
Os programadores também podem construir os seus próprios operadores, usando o
built-in op/3.
Queries usando o avaliador aritmético disponibilizado pelo Prolog têm a seguinte
forma: Value is Expression?. A expressão aritmética é a valiada e o resultado é
dado/unificado em Value.
ex: (X is 3+5)? tem a solução X=8 (3+5 is 3+5) falaha pois o primeiro argumento não
unifica com 8, que é o resultado da avaliação da expressão.

O que acontece se o termo a ser avaliado não é uma válida expressão aritmética?
Pode ser inválida por duas razões:
- Um termo como 3+x para uma constante x não pode ser avaliado. Em
contraste, um termo 3+Y para a variável Y pode ser ou não avaliado,
dependendo do valor de Y.
A semântica de um programa e, lógica é completaente definida e assim não pode
haver runtime errors. Por exemplo, a meta X is 3+Y tem as soluções {X=3, Y=0}.
Todavia quando interfaceamos com um computador temos de contar com as suas
limitações. Um runtime error acontece quando a computação não pode ser acabada
por falta de informações, isto é, variáveis não instanciadas. Isto é distinto das metas
que simplesmente falham.
Extensões do Prolog manejam com estes erros, suspendendo até que os valores das
variáveis em causa sejam conhecidos.
O modelo de Prolog introduzido não permite suspensões.
A query (X is 3+x) falha porque o argumento do lado direito não pode ser avaliado
como uma expressão aritmética.
Um erro comum dos iniciados é pensarem que is é o mesmo que assignar. Há a
tentação de escrever a meta, por exemplo, (N is N+1).

Outros predicados de sistema são <, ≤ (escrito =<), > e ≥ (escrito >=). O Prolog chama
directamente as operações aritméticas.
(A <B)?, A e B são avaliadas como expressões aritméticas. Os dois números
resultantes são comparados. ex: (N < 1)? gera um erro quando N é uma variável.
Testes de igualdade ou desigualdade devem usar os predicados: =:= e =/=.

8.2. PROGRAMAS ARITMÉTICO-LÓGICOS REVISITADOS


Realizar aritmética via avaliação em vez de perguntas lógicas causa uma
reconsideração dos programas de aritmética tal como foram apresentados na secção
3.1. Os cálculos podem ser feitos mais eficientemente. Por exemplo, achar o minimo
de 2 valores, pode ser feito usando comparação.
Similarmente, o maior divisor comum pode ser calculado usando o algoritmo de
Euclides:
Prog. 8.1.
greatest_common_divisor(X,Y,Z) <--
Z é o maior divisor comum dos inteiros X e Y.
greatest_common_divisor(I,0,I).
greatest_common_divisor(I,J,Gcd) <--
J > 0, R is mod J, greatest_common_divisor(J,R,Gcd).

plus(X,Y,Z) <-- Z is X+Y.


funciona se X e Y estiverem instanciadas como inteiros. Mas não funcionará se
quisermos usar a definição para a subtracção: plus(3,X,8)? – dá erro. São
necessários testes meta-lógicos (ver cap.9) para usar o mesmo programa para a
adição e subtracção.

Outra capacidade dos programas Prolog para aritmética é a estrutura recursiva dos
números. Nos programas lógicos, a estrutura é usada para determinar que regra
aplicar e para garantir terminação da computação.

Prog. 8.2.
factorial(N,F) <-
F é o inteiro N factorial
factorial(N,F) <--
N > 0, N1 is N-1, factorial(N1,F1), F is N*F1.
factorial(0,1).

factorial(Y,12)? dá erro

Temos de dar uma nova definição de correctitude de um programa, para acomodar o


comportamento dos testes aritméticos: Um programa Prolog está totalmente correcto
sobre um domíno D de metas se todas as metas em D a computação terminar, não
produzir um runtime error e tiver o significado correcto. o prog 8.2. está correcto sobre
o domínio de metas onde o 1º argumento é um inteiro.

8.3. TRANSFORMAR RECURSÃO EM ITERAÇÃO


Em Prolog não há constructos iterativos com tal e um conceito mais geral, como a
recursão, é usado para especificar quer algoritmos recursivos quer iterativos. A
principal vantagem da iteração sobre a recursão é a eficiência, sobretudo eficiência em
espaço. Recursividade requer stack. Espaço linear em n. Iteração usa um valor
constante de memória.
Há uma classe restrita de programas reursivos que correspondem quase a iterações.
Sob algumas condições, explicadas em 11.2., com optimização da cauda, esses
programas podem ser implementados quase com a mesma eficiência do que os
iterativos. Usam-se acumuladores.
Relembre-se que uma cláusula de puro Prolog é iterativa se tiver apenas uma regra
recursiva no seu corpo.
A maior parte dos cálculos simples aritméticos podem ser implementados por
programas iterativos.
ex:
Prog. 8.3.
factorial(N,F) <-- factorial(0,N,1,F).
factorial(I,N,T,F) <--
I < N, I1 is I+1, T1 is T*I1, factorial(I1,N,T1,F).
factorial(N,N,F,F).
O Prolog não tem variáveis de aramzenamento para colocar os resultados
intermédios. Então usamos argumentos adicionais (acumuladores)

Prog. 8.4.
factorial(N,F) <-- factorial(N,1,F).
factorial(N,T,F) <--
N > 0, T1 is T*N, N1 is N-1, factorial(N1,T1,F).
factorial(0,F,F).
versão alternativa, marginalmente mais eficiente e de mais fácil leitura (menos
argumentos).

Um predicado iterativo útil:


Prog.8.5.
between(I,J,K) <-
K é um inteiro entre os inteiros I e J, inclusive.
between(I,J,I) <-- I ≤ J.
between(I,J,K) <-- I < J, I1 is I+1, between(I1,J,K).
Gerando um range de números.

Prog. 8.6. a (recursivo)


sumlist(Is,Sum) <--
sumlist([I|Is], Sum) <-- sumlist(Is,IsSum), Sum is I+IsSum.
sumlist([ ],0).

Prog 8.6. b (iterativo)


sumlist(Is,Sum) <-- sumlist(Is,0,Sum).
sumlist([I|Is], Temp, Sum) <--
Temp1 is Temp+1, sumlist(Is,Temp1,Sum).
sumlist([ ], Sum,Sum).

Um predicado auxiliar (última linha) é introduzido com um argumento extra para o


acumulador, cujo valor inicial, 0, é metido na chamada inicial à última linha. A soma é
passada para fora na chamada final por unificação com o facto base.

Para multiplicar vectores com o mesmo comprimento (representados por listas, as


usual):
Prog. 8.7.a
inner_product(Xs,Ys,Value) <--
inner_product([X|Xs], [Y|Ys],IP) <--
inner_product(Xs,Ys,IP1), IP is X*Y+IP.
inner_product([ ], [ ], 0).

Prog 8.7.b
inner_product(Xs,Ys,IP) <-- inner_product(Xs,Ys,0,IP=.
inner_product([X|Xs],[Y|Ys],Temp,IP) <--
Temp1 is X*Y+Temp, inner_product(Xs,Ys,Temp1,IP).
inner_product([ ], [ ], IP,IP).

Prog. 8.8. é para calcular área dum polígono (difícil)...


Prog. 8.9.
maxlist([X|Xs], M) <-- maxlist(Xs,X,M).
maxlist([X|Xs], Y,M) <-- maximum(X,Y,Y1), maxlist(Xs,Y1,M).
maxlist([ 9,M,M).
maximum(X,Y,Y) <-- X ≤ Y.
maximum(X,Y,X) <-- X > Y.

Prog. 8.10.
lenght(Xs,N) <--
lenght([X|Xs],N) <-- N > 0, N1 is N-1, lenght(Xs,N1).
lenght([ ],0).

Prog. 8.11.
lenght([X|Xs],N) <-- lenght(Xs,N1), N is N1+1.
lenght([ ],0).

Prog. 8.12.
range(M,N,[M|Ns]) <-- M < N, M1 is M+1, range(M1,N,Ns).
range(N,N,[N]).
CAP. 9 – Inspecção de Estruturas

O Prolog standard tem vários predicados ligados a estruturas de termos. Estes


predicados são usados para reconhecer os diferentes tipos de termos, para decompor
os termos nos seus functores e argumentos e para criar novos termos.

9.1. PREDICADOS DE TIPOS

São relações unárias que distinguem entre tipos diferentes de termos. O próprio
sistema já traz alguns e que dizem se um termo é uma estrutura ou uma constante, e
mais, se a constante é um átomo, um inteiro ou real (vírgula flutuante).
Os 4 predicados básicos do Prolog standard são:
integer(X) <-- X é um inteiro
atom(X) <-- X é um átomo
real(X) <-- X é um número em vírgula flutuante
compound(X) <-- X é um termo composto
Cada predicado básico pode ser visto como uma tabela infinita de factos.
Outros tipos de predicados podem ser construídos a partir dos básicos. Por exemplo,
para saber se um número é inteiro ou real:
number(X) <-- integer(X).
number(X) <-- real(X).
Também inclui atomic(X). que dá sim se for um átomo ou um número.
Neste livro chamamos-lhe antes constant. constant(X) <-- atomic(X).
atom(3)? falha; integer(X)? falha
Os únicos termos não abarcados são as variáveis. Isso será feito nos predicados
meta-lógicos do cap. seguinte.

Prog. 9.1.
flatten(Xs,Ys) <--
Ys é uma lista de elementos de Xs, que é uma lista em que os elemenetos
podem ser átomos ou listas.
flatten([X|Xs],Ys) <--
flatten(X,Ys1), flatten(Xs,Ys2), append(Ys1,Ys2,Ys).
flatten(X,[X]) <--
constant(X), X≠[ ].
flatten([ ], [ ]).

A condição constant(X) é necessária para prevenir que a regra seja usada quando X é
uma lista.

Este programa usando dupla recursão não é o mais eficiente. Pode requerer um nº de
reduções de ordem quadrática em relação ao número de membros.

Se usarmos o predicado auxiliar flatten(Xs,Stack,Ys), onde Ys é a lista alisada


contendo os elementos em Xs e uma pilha Stack é usada para guardar orasto do que
precisa de ser alisado. A Stack é representada como uma lista. É construção top-
down.
Um uso geral da técnica do uso de stack é mostrado neste programa.

Prog. 9.1.b
flatten(Xs,Ys) <--
Ys é uma lista dos elementos de Xs.
flatten(Xs,Ys) <-- flatten(Xs,[ ],Ys).
flatten([X|Xs],S,Ys) <--
list(X), flatten(X,[Xs|S],Ys).
flatten([X|Xs],S,[X|Ys]) <--
constant(X), X≠8 ], flatten(Xs,S,Ys).
flatten([ ],[X|S],Ys) <--
flatten(X,S,Ys).
flatten([ ], [ ], [ ]).
list([X|Xs]).
(o livro tem explicação por menorizada do funcionamento – pg.166)

9.2. ACESSANDO TERMOS COMPOSTOS


Reconhecer um termo como composto é um aspecto da inspecção de estruturas. Um
outro aspecto é prover acesso ao nome do functor, aridade e argumentos de um termo
composto.
Um prdeicado de sistema para termos compostos é: functor(Term,F,Arity).
ex: de sucesso: functor(father(haran,lot),father,2)?
O predicado functor pode também ser, como os predicados de tipos, definido por uma
tabela de factos da forma functor(f(X1,...,Xn),f,N).
O Prolog considera as constantes como functores de aridade 0

As chamadas a um functor pode falhar por várias razões. Uma meta como
functor(father(X,Y),son,2) não unifica com um facto da tabela.
Também há restrições ao tipo dos argumentos a usar. p.ex: o 3º argumento de functor,
a aridade do termo, não pode ser atom ou termo composto.
Há ainda um outro problema de erro devido aos possíveis inumeras soluções:
functor(X,Y,2)?

O predicado functor é usado em 2 situações, comummente:


Decomposição de termos (encontra o nome do functor e aridade de um dado termo) e
criação (constrói um termo com um nome de functor particular e aridade:
ex: do 1º : functor(father(haran,lot),X,Y)? tem a solução {X=father, Y=2)
ex: do 2º: functor(T,father,2)? com solução T=father(X,Y).

O predicado companheiro de functor é arg(N,Term,Arg), que acessa aos argumentos


de um termo em vez de ao nome do functor.
A meta arg(N,Term,Arg) é true se Arg é o nésimo argumento do termo.
por ex: arg(1,father(haran,lot),haran) é true.
também tem 2 usos:
1 – encontrar um argumento particular de um termo composto:
arg(2,father(haran,lot),X)?, com solução X=lot.
2 – Criação de termos arg(1,father(X,lot),haran)? tem sucesso instanciando X a
haran.
O predicado arg também é definido como se houvesse uma tabela infinita de factos,
arg(1,father(X,Y),X) ....
A chamada a arg falha se não houver uma unificação com o facto apropriado da
tabela, assim como se as restrições de tipo forem violadas, como por exemplo se o 1º
argumento for um átomo.

Vamos considerar um exemplo de uso de functor e arg para inspeccionar termos:


Prog. 9.2.
subterm(Sub,Term) <-
Sub é um subtermo do termo ground Term
subterm(Term,Term).
subterm(sub,Term) <--
compound(Term), functor(Term,F,N), subterm(N,Sub,Term).
subterm(N,Sub,Term) <--
N > 1, N1 is N-1, subterm(N1,Sub,Term).
subterm(N,Sub,Term) <--
arg(N,Term,Arg), subterm(Sub,Arg).

O procedimento subtermo pode ser usado para testar se o 1º argumento é um


subtermo do 2º, ou para gerar subtermos de um dado termo. De notar que a ordem
das cláususlas determina a ordem por que os subtermos são ferados. No caso
apresentado os subtermos do 1º argumento são dados antes dos do 2º.
ex: subterm(a,f(X,Y))? onde mo 2º argumento não é ground. Eventualmente a submeta
subterm(a,X9 é atingida. Isto sucede pela 1º regra instanciando X em a.

Outro exemplo: substituir subtermos


o esquema é: substitute(Old,New,OldTerm,NewTerm), onde NewTerm é o resultado
de substituir todas as ocorrências de Old em OldTerm por New

Prog.9.3.
substitute(Old,New,OldTerm,NewTerm) <--
NewTerm é o resultado de substituir todas as ocorrências de Old em OldTerm
por New.
substitute(Old,New,Old,New).
substitute(Old,New,Term,Term) <--
constant(Term), Term≠Old.
substitute(Old,New,Term,Term1) <--
compound(Term),
functor(Term,F,N),
functor(Term1,F,N),
substitute(N,Old,New,term,Term1).
substitute(N,Old,New,Term,Term1) <--
N > 0,
arg(N,Term,arg,Arg1),
arg(N,Term1,Arg1),
N1 is N-1,
substitute(N1,Old,New,Term,Term1).
substitute(0,Old,New,Term,Term1).

ex. de aplicação: substitute(cat,dog,owns(jane,cat),X)?


a query falha a unificação com o facto (regra 1). A segunda regra também não é
aplicável pois owns(jane,cat) não é uma constante. A 3ª regra aplica-se.

Um outro predicado para inspecção de estruturas é um operador binário =.. ,


chamado, por razões históricas, univ.
A meta Term=..List tem sucesso se a lista cuja cabeça é o nome do functor do termo
Term e cuja cauda é a lista de argumentos de Term. Por exemplo, a query
father(haran,lot) =.. [father,haran,lot]? tem sucesso.
Como functor e arg, univ tem dois usos: Construir um termo dada uma lista, por ex:
(X=..[father,haran,lot])? com solução X=father(haran.lot), ou construir uma lista dado
um termo.
Em geral, programas escritos com functor e arg podem também ser escritos com univ
e, geralmente, mais simples mas menos eficientes desde que os outros evitem
construir estruturas intermédias.
Um uso é formular a cadeia da regra para a diferenciação simbólica d/dx{f(g(x)} =
d/dg(x)[f(g(x)} X d/dx{g(x)}
derivative(F_G_X,X,DF*DG) <--
F_G_X =.. [F,G,X],
derivative(F_G_X,G_X,DF),
derivative(G_X,X,DG).
falta a figura 9.2. , o prog 9.4. e o prog. 9.5.a

CAP. 10 – PREDICADOS META-LÓGICOS


Uma extensão poderosa do poder expressivo dos programas lógicos é dado pelos
predicados meta-lógicos. Estes predicados estão fora do alcance da lógica de 1ª
ordem, porque eles consultam o estado da prova, tratam variáveis (em vez dos termos
que elas denotam) como objectos da linguagem, e permitem conver~soa de estruturas
de dados em metas.
Permite-nos ultrapasssar duas dificuldades que sentimos nos cap. anteriores a
trabalhar com variáveis:
1 – O comportamento das variáveis nos predicados de sistema. Por exemplo, avaliar
uma expressão com uma variável dá erro. Assim como chamar predicados de tipo com
argumentos como variáveis.
2 – Instanciação acidental de variáveis durante a inspecção de estruturas. No cap. 9
restringimos a inspecção a termos ground.
Este cap. tem 4 secções: 1- discute predicados tipo que determinam quando um termo
é uma variável. 2- Comparação de termos. 3- predicados que permitem as variáveis
ser manipuladas como objectos. 4 – converter dados em metas executáveis.

10.1. PREDICADOS META-LÓGICOS DE TIPO


O básico é var(Term), que testa se um dado termo é, no presente momento, uma
variável não instanciada. O seu comportamento é similar aos predicados de tipo
discutidos no capítulo anterior.
A query var(Term)? tem sucesso se Term é uma variável e falha caso contrário. ex:
var(X)? tem sucesso e var(a)? e var([X|Xs])? falham.
O predicado var é uma extensão ao Prolog puro. Uma tabela não pode agora ser
usada para dar todos os nomes de variáveis.
Um facto var(X) quer dizer que todas as instâncias de X são variáveis, e não que a
letra X denota uma variável. Ser capaz de referir a um nome de variável está fora do
alcance da lógica do 1º grau.
O predicado nonvar(Term) tem o comportamento contrário.

Este tipo de predicados pode ser usado para dar alguma flexibilidade aos programas
que usam predicados de sistema e também para controlar a ordem das metas. É o
que veremos usando programas já estudados atrás.

Por ex: o programa plus que vamos ver abaixo pode ser usado para soma e
subtracção. A ideia é checar quais os argumentos que estão instanciados antes,
chamando o avaliador aritmético. Por exe. a 2ª regra diz que se o 1º e 3º argumentos,
X e Z, não são variáveis, o 2º argumento, Y, pode ser determinado como a sua
diferença. De notar que se os argumentos não forem inteiros isto falha.
Não dá erros mas ainda não pode ser usado para gerar partição de um número. Isso
requer geração de números.

Prog. 10.1.
plus(X,Y,Z) <--
A soma dos números X e Y é Z.
plus(X,Y,Z) <-- nonvar(X), nonvar(Y), Z is X+Y.
plus(X,Y,Z) <-- nonvar(X), nonvar(Z), Y is Z-Y.
plus(X,Y,Z) <-- nonvar(Y), nonvar(Z), X is Z-Y.

Predicados meta-lógicos colocados inicialmente no corpo de uma cláusula para decidir


qual a cláusula no procedimento deve ser usada são chamados testes meta-lógicos.
Poderíamos fazer um teste mais fraco, mas preferível:
plus(X,Y,Z) <-- integer(X), integer(Y), Z is X+Y.
Prog. 10.2.
lenght(Xs,N) <-- A lista Xs tem comprimento N.
lenght(Xs,N) <-- nonvar(Xs), lenght1(Xs,N).
lenght(Xs,N) <-- var(Xs), nonvar(N), lenght2(Xs,N).
lenght1(Xs,N) <-- ver programa 8.11.
lenght(Xs,N) <-- ver programa 8.10.

São precisos programas separados para encontrar o tamanho de uma lista ou para
gerar listas arbitrárias de comprimento N. Este programa usa testes meta-lógicos para
definir só uma relação. Ele evita a situação de não-terminação dos programas 8.10. e
8.11. caso ambos os argumentos não estivessem instanciados.

Os predicados meta-lógicos também podem ser usados para fazer a melhor escolha
da ordem das metas numa cláusula.

Prog.10.3.
grandparent(X,Z) <-- X é avô de Z.
grandparent(X,Z) <-- nonvar(X), parent(X,Y), parent(Y,Z).
grandparent(X,Z) <-- nonvar(Z), parent(Y,Z), parent(X,Y).
(procura + eficiente, se netos de um avô ou avô de um neto).

Prog.10.4.
ground(term) <-- Term é um termo ground.
ground(Term) <-- nonvar(Term), constant(term).
ground(Term) <--
nonvar(Term), ...para garantir que não gera erro...
compound(Term),
functor(Term,F,N),
ground(N,Term).
ground(N,Term) <--
N > 0,
arg(N,Term,Arg),
ground(Arg),
N1 is N-1,
ground(N1,Term).
ground(0,Term).

Prog.10.5.
unify(Term1,Term2) <-- Term1 e Term2 são unificados, ignorando o check de
ocorrência
unify(X,Y) <-- var(X), var(Y), X=Y.
unify(X,Y) <-- var(X), nonvar(Y), X=Y.
unify(X,Y <-- var(Y), nonvar(X), Y=X.
unify(X,Y) <-- nonvar(X), nonvar(Y), constant(X), constant(Y), X=Y.
unify(X,Y) <-- nonvar(X), nonvar(Y), compound(X), compound(Y),
term_unify(X,Y).
term_unify(X,Y) <-- functor(X,F,N), functor(Y,F,N), unify_args(N1,X,Y).
unify_args(N,X,Y) <-- N > 0, unify_arg(N,X,Y), N1 is N-1, unify_args(N1,X,Y).
unify_args(0,X,Y).
unify_arg(N,X,Y) <-- arg(N,X,ArgX), arg(N,Y,ArgY), unify(ArgX,ArgY).

10.2. COMPARAÇÃO DE TERMOS NÃO-GROUND


Considerando o problema de estender a definição anterior de unificação para abarcar
a verificação de ocorrência. Recorde-se que a verificação de ocorrência é parte da
formal definição de unificação: Requer que uma variável não seja unificada com um
termo contendo essa variável. Para implementar isso tem de se verificar se duas
variáveis são idênticas (não apenas unificáveis, como quaisquer duas variáveis são). É
este o teste meta-lógico.
O Prolog standard dá-nos um predicado de sistema, == , para este propósito. A query
X == Y? tem sucesso se X e Y são constantes, variáveis idênticas ou estrururas cujos
principais functores têm a mesmo nome e aridade, e recursivamente Xi == Yi? têm
sucesso para todos os correspondentes argumentos Xi e Yi de X e Y.
Por exemplo X == 5? falha, em contraste com X=5?
Também há um predicado de sistema que tem o comportamento contrário a ==. A
query X\==Y? tem sucesso a não ser que X e Y sejam termos idênticos.

Prog.10.5.
unify(Term1,Term2) <-- Term1 e Term2 são unificados, com o check de
ocorrência
unify(X,Y) <-- var(X), var(Y), X=Y.
unify(X,Y) <-- var(X), nonvar(Y), not_occurs_in(X,Y), X=Y.
unify(X,Y <-- var(Y), nonvar(X), not_occurs_in(Y,X), Y=X.
unify(X,Y) <-- nonvar(X), nonvar(Y), constant(X), constant(Y), X=Y.
unify(X,Y) <-- nonvar(X), nonvar(Y), compound(X), compound(Y),
term_unify(X,Y).

not_occurs_in(X,Term) <-- A variável X não ocorre em Term


not_occurs_in(X,Y) <-- var(Y), X \== Y.
not_occurs_in(X,Y) <-- nonvar(Y), constant(Y).
not_occurs_in(X,Y) <-- nonvar(Y), compound(Y), functor(Y,F.N),
not_occurs_in(N,X,Y).
not_occurs_in(N,X,Y) <-- N>0, arg(N,Y,Arg), not_occurs_in(X,Arg), N1 is N-1,
not_occurs_in(N1,X,Y).
not_occurs_in(0,X,Y).

term_unify(X,Y) <-- functor(X,F,N), functor(Y,F,N), unify_args(N1,X,Y).


unify_args(N,X,Y) <-- N > 0, unify_arg(N,X,Y), N1 is N-1, unify_args(N1,X,Y).
unify_args(0,X,Y).
unify_arg(N,X,Y) <-- arg(N,X,ArgX), arg(N,Y,ArgY), unify(ArgX,ArgY).

Prog.10.7.
occurs_in(Sub,Term) <-- Sub é um subtermo de Term.
a: usando ==
occurs_in(X,Term) <-- subterm(Sub,Term), X==Sub.
b: usando freeze
occurs_in(X,Term) <-- freeze(X,Xf), freeze(Term,Termf), subterm(Xf,Termf).
subterm(X,Term) <-- ver programa 9.2.

10.3. VARIÁVEIS COMO OBJECTOS


O delicado manejamento de variáveis que precisam de definir occurs_in, mostra uma
deficiência no poder esxpressivo do Prolog. As variáveis não são facilmente
manipuláveis. Quando tentamos inspeccionar, criar e reason acerca de termos,
variáveis podem ser indesejavelmente instanciadas.
ex: consideremos a meta substitute(a,b,X,Y), substituindo a por b na variável X para
dar Y. Há 2 possíveis comportamentos.
Logicamente há uma solução quando X é a e Y é b. Esta é a solução dada pelo
programa 9.3. atingida por unificação com o facto base substitute(Old,New,Old,New).
Na prática, outro comportamento é habitualmente preferido. Os dosi termos X e a
devem ser considerados diferentes, e Y deve ser instanciado por X. O outro caso base
do programa 9.3.
substitute(Old,New,Term,Term) <-- constant(Term), Term ≠ Old. cobre este
comportamento. Todavia, a meta falha porque a variável não é uma constante.
Podemos prevenir a 1ª (lógica) solução usando um teste meta-lógico para assegurar
que o termo a ser substituído é ground. A unificação implícita na cabeça da cláusula é
então só realizada se o teste tiver sucesso.
substitute(Old,New,Term,New) <-- ground(Term), Old=Term.
Tratar uma variável diferentemente duma constante é feito por uma regra especial:
substitute(Old,New,Var,Var) <-- var(Var).
Acrescentando estas duas cláususlas ao programa 9.3. e acrescentando outros testes
meta-lógicos, permite ao programa trabalhar com termos não ground. Todavia, o
programa resultante é deselegante (mistura de estilos declarativos e procedimentais).
Para usar uma linguagem médica – os sintomas foram tratados (indesejável
instanciação de variáveis) mas a doença não foi curada (incapacidade de nos
referirmos a variáveis como objectos). São necessárias mais primitivas para curar o
problema.
A dificuldade de misturar a manipulação de termos de objectos de nível e de meta-
level traz um problema teórico. Falando estritamente: programas meta-level devem ver
as variáveis object-level como constantes e ser capazes de se lhes refrir pelo nome.
Sugerimos dois predicados de sistema: freeze(Term,Frozen) e
melt(Frozen,Thawed), para permitir manipulações explícitas de variáveis.

Freezing um termo faz uma cópia desse termo, Frozen, onde todas as variáveis não
instanciadas se tronam em constantes únicas. Um termo frozen parece, e pode ser
manipulado, como um termo ground. Frozen variáveis são vistas como átomos ground
durante as unificações. Duas variáveis frozen unificam se e só se são idênticas.
Similarmente, se um termo frozen e uma variável não instanciada forem unifficados,
eles tronam-se num termo idêntico frozen. O comportamento é o mesmo das
constantes. Por exemplo avaliação aritmética envolvendo variáveis frozen, falha.

Freezing dá-nos a capacidade de saber quando dois termos são idênticos. Dois
termos frozen, X e Y, unificam se e só se as suas versões unfrozen são idênticas, isto
é, X==Y. Esta propriedade é essencial ao correcto comportamento do prog.10.7.
melt é o contrário de frozen e qualquer instanciação de variável que tenha sido feita
enquanto esta estava frozen é tida em conta.

Versão alternativa de substitute, com não ground termos. O termo é frozen antes da
substituição para não haver instanciações acidentais.
non_ground_substitute(X,Y,Old,New) <-- freeze(Old,Old1), substitute(X,Y,Old1,Old2),
melt(Old2,New).

O termo frozen também pode ser usado como template para fazer cópias. O predicado
de sistema melt_new(Frozen,Term) faz uma cópia Term do termo frozen, onde as
variáveis frozen são substituídas por nopvas variáveis.
Um dos usos de melt_new é para copiar um termo:
copy(Term,Copy) <-- freeze(Term,Frozen), melt_new(Frozen,Copy).
O Prolog providencia o predicado copy_term(Term1,Term2).
Infelizmente freeze, melt e melt_new não estã presentes em implementações Prolog
existentes. São úteis para o cap.12 – predicados extra-lógicos.

Uma útil aproximação ao freeze é o predicado numbervars(Term,N1,N2). Uma


chamada ao predicado é true se as variáveis que aparecem no termo podem ser
numeradas de N1 a N2-1. O efeito é substituir cada variável do termo por um termo da
forma ‘$VAR’(N). ver prog.10.8.

10.4. A FACILIDADE META-VARIÁVEL


Uma característica do Prolog é a equivalência dos programas e dados – ambos podem
ser representados em termos lógicos. Para explorar isto, os programas precisam de
ser tratados como dados e os dados ttransformados em programas.
Vamos ver uma facilidade que permite transformar um termo numa meta:
call(X) , chama a meta X para o Prolog resolver.
Na prática, a maioria das implementações Prolog relaxam a imposição que as metas
no corpo de uma cláusula têm de ser termos não variáveis. A meta-variável facilidade
permite uma variável aparecer como meta numa meta conjuntiva ou no corpo de uma
cláusula. Durante
Cap. 11
Cuts and Negation (Cortes e Negação)
O Prolog fornece um sistema de predicados simples, chamados cut, para afectar o
comportamento procedimental dos programas. A sua função principal é reduzir o
espaço de procura das computações Prolog, pruning dinamicamente a árvore de
procura.
O cut pode ser usado para prevenir o Prolog de seguir caminhos sem frutos, que o
programador sabe que não podem dar resultados.
Podem também ser usados, inadvertidamente ou propositadamente, para aprumar os
caminhos de computação que contêm soluções. Fazendo isto, uma forma fraca de
negação pode ser ser efectuada.
O uso do cut é controverso pois a maior parte dos seus usos só pode ser interpretado
procedimentalmente, em contraste com o que aconselhamos, que é um estilo
declarativo de programação que encorajamos. Usado com parcimónia pode, no
entanto, aumentar a eficiência dos programas sem comprometer a sua clareza.

11.1. Green Cuts: Expressing Determinism (Cortes Verdes: Expressando


Determinismo)
Consideremos o programa merge (Xs, Ys,Zs). É uma operação determinista. Se X<Y,
X>Y, X=:=Y, só uma pode ser true.

merge(Xs, Ys, Zs) <--


Zs é uma lista ordenada de inteiros,
obtida da junção das listas ordenadas de
inteiros Xs e Ys
merge([X|Xs], [Y|Ys], [X|Zs]) <-- X < Y, merge(Xs,[Y|Ys], Zs).
merge([X|Xs], [Y|Ys], [X,Y|Zs] <-- X=:=Y, merge(Xs,Ys,Zs).
merge([X|Xs], [Y|Ys],[Y|Zs]) <-- X > Y, merge([X|Xs],Ys,Zs).
merge(Xs,[ ], Xs).
merge([ ],Ys,Ys).

O corte, denotado por !, pode ser usado para expressar a exclusividade mútua dos
testes. É colocado depois dos testes aritméticos. Por exemplo, a primeira cláusula da
função merge é escrita:

merge ([X|Xs], [Y|Ys], [X|Zs]) <-- X < Y, !, merge(Xs,[Y|Ys], Zs).

Operacionalmente o corte funciona assim: A meta tem sucesso e compromete o


Prolog para todas as escolhas feitas desde que o golo pai unifique com a cabeça da
cláususla em que ocorre o cut.
Apesar da definição clara, as ramificações e implicações não o são.
Mal entendidos acerca dos efeitos do cut são uma das maiores causas de bugs por
programadores experientes e inexperientes.
Os erros caiem em duas categorias:
Assumir que o corte apruma caminhos de computação que ele não o faz, e assumir
que não apruma soluções onde ele efectivamente o faz.
As seguintes implicações podem ajudar a clarificar:
Primeiro, um corte apruma todas as cláusulas abaixo dele. Um golo p unificasdo com
uma cláusula contendo um corte que teve sucesso, não será capaz de produzir
soluções usando cláusulas que ocorrem sob aquela cláusula.
Segundo, um cut apruma todas as soluções alternativas à conjunção de golos que
aparece à sua esquerda na cláusula. Por exemplo, uma meta conjuntiva seguida de de
um corte produzirá no máximo uma solução.
Por outro lado, o corte não afecta as metas à sua direita na cláususla. Eles podem
produzir mais que uma solução no evento do backtracking. Contudo, uma vez que a
conjunção falhe, a procura prossegue desde a última alternativa antes da escolha da
cláusula contendo o corte.

ex: merge com cortes


merge(Xs,Ys,Zs) <--
Zs é uma lista ordenada de inteiros,
obtida da junção das listas ordenadas
de inteiros Xs e Ys.
merge([X|Xs], [Y|Ys], [X|Zs]) <--
X < Y, !, merge(Xs,[Y|Ys],Zs).
merge([X|Xs], [Y|Ys], [X,Y|Zs] <--
X=:=Ys, !, merge(Xs,Ys,Zs)
merge([X|Xs], [Y|Ys], [Y|Zs]) <--
X > Y, !, merge([X|Xs, Ys, Zs).
merge(Xs,[ ], Xs) <-- !.
merge([ ], Ys, Ys) <-- !.

ex: na query merge([1,3,5],[2,3],Xs)?


evita dois ramos da àrvore (0s que 1=:=2 e 1>2)
A query é primeiro reduzida para para a query conjuntiva 1<2,!,merge([3,5],[2,3],Xs1)?;
o golo 1<2 é resolvido com sucesso, chegando ao nó merge... e evitando os outros
ramos.
Os cortes são colocados depois do teste.

Efeito do corte numa cláusula geral: C = A <-- B1,...,Bk,!,Bk+2,...,Bn num


procedimento definindo A.
Se o golo corrente G unifica com a cabeça de C, e B1,...,Bk sucede, o corte tem o
seguinte efeito. O programa está comprometido à escolha de C para reduzir G.
Qualquer cláusula alternativa para A que poderá unificar com G é ignorada. Depois,
deve Bi falhar para 1>K+1, bactracking vai só até !
Outras escolhas restantes da computação de Bi, i>=k, sdão evitadas da árvore de
procura. Se o backtracking atinge o corte, então o corte falha, e a procura prossegue
desde a última escolha feita antes de G para reduzir C.

No ex: do merge vemos que é determinista. O corte compromete a computação a uma


única cláusula.
--> cut reduz o tempo de computação e poupa espaço. Determinismo impõe menos
informação a ser guardada para eventual futuro backtracking.
Isto pode ser explorado em implementações de Prolog com optimização de
recursividade da cauda (secção seguinte).

Outro ex: no mesmo tipo do merge. Calcular o mínimo. Uma vez que o teste aritmético
tem sucesso não há possibilidade dos outros teste o terem

minimum(X,Y,Min) <--
Min é o mínimo dos números X e Y
minimum(X,Y,X) <-- X ≤, !.
minimum(X,Y,Y) <-- X > Y, !.

Um exemplo mais substancial onde os cortes podem ser acrescentados para indicar
que o programa é determinista é dado pelo programa 3.29. O programa define a
relação polynomial(Term,X) para reconhecer se o Term é um polinómio em X
O resultado é um programa determinista que tem uma mistura de cortes depois das
condições e depois da unificação.
polynomial(Term, X) <--
Term é um polinómio em X.
polynomial(X,X) <-- !.
polynomial(Term,X) <--
constant(Term), !.
polynomial(Term1+term2,X) <--
!, polynomial(Term1,X), polynomial(Term2,X).
polynomial(Term1-Term2,X) <--
!, polynomial(Term1,X), polynomial(Term2,X).
polynomial(Term1*Term2,X) <--
!, polynomial(Term1,X), polynomial(Term2,X).
polynomial(Term1/Term2,X) <--
!, polynomial(Term1,X), constant(Term2).
polynomial(Term1↑N,X) <--
!, integer(N), N≥0, polynomial(Term,X).

Normalmente diz-se que usando as capacidades aritméticas do computador, em vez


de programas lógicos recursivos, se perde em flexibilidade o que se ganha em
eficiência. Os programas lógicos perdem os seus usos múltiplos quando expresos em
Prolog. Os programas em Prolog com cuts têm ainda menos flexibilidade dos que os
que não têm cortes. Isto não é problema se o uso que se pretende para o rpograma for
único, como é a maioria das vezes.
Ex: onde se poupa muito:
sort(Xs,Ys) <--
append(as,[X,Y|Bs],Xs),
X > Y,
append(As,[Y,X|Bs],Xs1),
sort(Xs1,Ys).
O programa procura por um par de elementos adjacentes que estejam fora de ordem,
troca-os, e continua até a lista estar ordenada.
A cláusula base é sort(Xs,Xs) <-- ordered(Xs).
Consideremos o golo sort([3,2,1],Xs).
Temos de fazer 3 trocas, mas há várias maneiras de chegar lá. Como sabemos que só
há uma lista final, logo que é feita uma troca, sabemos que não vale a pena procurar
uma outra alternativa. Assim pomos o cut logo que isso acontece ( omais cedo
possível):
Ficamos com:
sort(Xs,Ys) <--
Ys é uma permutação ordenada da lista de inteiros Xs.
sort(Xs,Ys) <--
append(As,[X,Y|Bs],Xs),
X > Y,
!,
append(As,[Y,X|Bs],Xs1),
sort(Xs1,Ys).
sort(Xs,Xs) <--
ordered(Xs),
!.
ordered(Xs) <-- ver programa 3.20.

Os (estes) green cuts não interferem com o significado do programa. Já o mesmo não
acontece com o red cuts que veremos à frente.
Os cortes interagem com os predicados de sistema tais como clall e ; introduzidos no
cap.10 e predicados com not que introduziremos mais à frente.
A questão é que alcance deve o cut ter. Isso vai para o cap.17. (não interessa).

11.2. Tail Recursion Optimization (Optimização por recursão da cauda)


Como se viu em 8.3. a diferença de perfomance entre a recursividade e a iteração é
que, enquanto a iteração requer um espaço constante, qualquer que seja o número de
iterações usadas, a recursividade exige um espaço linearmente dependente do
número de chamadas recursivas a executar.
Os programas recursivos serão mais elegantes, mas a perda de espaço não é
compensadora. Felizmente há uma espécie de programas recursivos que podem ser
executados em espaço constante.
A técnica é a Tail Recursion Optimization ou Last Call Optimization. Intuitivamente é
tentar executar um programa recursivo como se fosse iterativo.
...
...
11.3. Negation (Negação)
O corte pode ser usado para implementar uma versão da negação como falha. O
programa 11.6. define um predicado not(Goal), que tem sucesso se o Goal falha.
Também usando o corte, o programa usa a facilidade meta variável descrita no
cap.10, e o predicado de sistema fail que falha sempre.
not X <--
X is not provable.
not X <-- X, !, fail.
not X.
Programa 11.6 Negação como falha

O Prolog standard provê o predicado fail_if(Goal), que tem o mesmo comportamento


que not/1.
A razão porque não chamamos not a este tipo de negação é que ele não é
veradeiramente a negação lógica.
No programa acima, se X tem sucesso, o corte é encontrado, not X falha, se X falha a
2ª regra é aplicada e tem sucesso. A ordem das regras é essencial. O que não era
pretendido: Antes a troca da ordem das regras só fazia com que as soluções fossem
encontradas por outra ordem, agora o próprio significado do programa é alterado.
ex:
married(abraham,sarah).
merried(X,Y) <-- married(Y,X).
A query not married(abraham,sarah)? termina (com falha) mesmo sabendo que
married(abraham,sarah)? não termina.
O programa 11.6 está incompleto como implementação de negação por falha. A
incompletude vem da incompletude do Prolog em realizar o modelo computacional de
programas lógicos. A definição de negação como falaha para programas lógicos é em
termos de uma finitamente falha árvore de procura. Uma computação em Prolog não
encontra garantidamente uma, mesmo que exista. Há goals que podem falhar pela
negação como falha, que não acabam nas regras de computação do Prolog. Por
exemplo, a query not(p(X),q(X)))? não termina com respeito ao programa:
p(s(X)) <-- p(X).
q(a).
A query tem sucesso se o goal q(X) for seleccionado primeiro, pois isso dá uma árvore
de procuara que falah finitamente.
A incorrectude do rpograma 11.6 vem da ordem do not quando usado em conjunção
com outros goals. EX:
unmarried_student(X9 <-- not married(X), student(X).
student(bill).
married(joe).
A query unmarried_student(X)? falha com respeito aos dados, ignorando que X=bill é
uma solução logicamente implicada pela regra e pelos dois factos. A falha ocorre np
goal not married(X), pois há uma solução X=joe. O problema pode ser evitado aqui por
troca da ordem dos goals no corpo da regra.
Um exemplo similar é a query not(X=1), X=2?, que falah apesar de haver uma solução
X=2.
A implementação da negação como falha não trabalha garantidamente de forma
correcta para goals não ground.
É então da responsabilidade do programador garantir que as metas negadas são
ground antes de estarem resolvidas.
Isto pode ser feito ou por análise estática do programa ou por um runtime check,
usando o predicado ground definido no programa 10.4.
O predicado not é muito útil. Permite-nos definir conceitos interessantes. Por ex.
consideremos o predicado disjoint(Xs,Ys), verdadeiro se duas listas Xs e Ys não têm
elementos em comum. Pode ser definido como:
disjoint(Xs,Ys) <-- not (member(Z,Xs), member(Z,Ys)).

Uma propriedade interessante de not(Goal) é que nunca instancia os argumentos em


Goal. Isto é devido à falha explícita depois da chamada ao Goal ter sucesso, que
desfaz qualquer bindings feitos. esta propriedade pode ser explorada para definir um
procedimento verify(Goal), dado como parate do programa 11.7., que determina se um
goal é true sem afectar os estado corrente das variáveis. A dupla negação provê os
meios.

variants(Term1, Term2) <--


Term1 e Term2 são variants.
variants(Term1, Term2) <--
verify(numbervars(Term1,0,N),
numbervars(Term2,0,N),
Term1=Term2)).
verify(Goal) <--
Goal tem uma instancia verdadeira. Verificar que isto não é feito
construtivamente, então variáveis não são instanciadas no processo.
verify(Goal) <-- not(not Goal).
numbervars(Term,N,N1) <-- Ver o program 10.8

Uma frase duplamente negada não é a mesma coisa que a afirmação equivalente. Isto
é como na linguagem natural.
O programa para verify pode ser usado em conjunto com o 10.8. para numbervars
para definir a noção de igualdade intermédia entre unificabilidade provida por =/2 e
igualdade sintáctica provida por ==/2. O predicado variants(X,Y) definido no programa
11.7 é verdade se os dois termos X e Y são variants.
Dois termos são variants se são instâncias um do outro (cap.4).
Isto pode ser conseguido com o seguinte truque, implementado no programa 11.7:
Instanciar as variáveis usando numbervars, testar se os termos unificam e desfazer a
instanciação.
As três formas de comparação =/2 variant/2 e ==/2 são progressivamente mais fortes,
com a unificabilidade sendo a mais fraca e mais geral.
Termos idênticos são variants e termos variantes são unificáveis. Para termos ground
todas as três comparações dão os mesmos resultados.

A conjunção do corte e falha usado na primeira cláusula do not no Programa 11.6 é


conheciada como a cut-fail combination e é uma técnica que pode ser usada mais
gearalmente. Permite falha antecipada. Uma cláusula com uma cut-fail combination diz
que a procura não precisa (e não prosseguirá) prosseguir.
No programa 11.6 o cut não é green mas red. O programa não terá o mesmo
significado se o cut for removido.
A combinação cut-fail é usada para implementar outros predicados de sistema
envolvendo a negação. Por exemplo o predicado ≠ (escrito como \= no Prolog
standard) pode ser facilmente implementado via unificação e cut-fail, em vez de uma
tabela infinita, com o programa 11.8. Este programa também só funciona
garantidamente para goals ground.
Com ingenuidade, e um bom entendimento da unificação e do mecanismo de
execução do Prolog, definições interessantes podem ser encontradas para muitos
predicados meta-lógicos.
Prog.11.8. Implementando o ≠
X≠Y <--
X e Y não são unificáveis.
X ≠ X (?erro?) <-- !, fail.
X ≠ Y.

11.6. Cortes para eficiência


Neste capítulo dissemos que a eficiência de alguns programas pode ser aumentada
através do uso sparing do cut. Esta secção explora essa frase. Duas coisas serão
vistas: a primeira é o significado de eficiência no contexto do Prolog. A segunda é o
uso apropriado do cut.
Eficiência relaciona-se com o uso dos recursos (espaço e tempo).
As duas maiores áreas de memória manipuladas durante uma computação Prolog são
o stack e o heap. O stack para governar o fluxo de controlo. O heap é usado para criar
estruturas de dados que são necessárias durante a computação.
Stack: cada vez que um goal é escolhido para redução, uma frame de stack é
colocada no stack. São usados pointers para especificar o subsequente fluxo de
controlo uma vez que o goal falha ou tem sucesso. Os apontadores dependem se
outras cláusula podem ser usadas para reduzir o goal escolhido. A manipulação do
syack frame é simplificada consideravelmente se for conhecido que apenas uma
cláusula é aplicável. Tecnicamente, um ponto de escolha precisa ser colocado no
stack se mais do que uma cláusula é aplicável.
tecnicamente o Prolog avançou para determinismo e pode correr quase tão
eficientemente como as linguagens convencionais.
Os cortes são uma maneira das implementações do Prolog saberem que apenas uma
cláusula é aplicável. Outra é o efectivo uso de indexamento.
Se um corte é necessa´rio para dizer a uma implementação particular de Prolog que
apenas uma cláusula é aplicável depende do particular esquema de indexamento.
Neste livro nós usamos frequentemente o primeiro argumento para diferenciar entre
cláusula. Indexar ao primeiro argumento é o mais comum modo entre as
implementações Prolog.
O uso eficiente do espaço é determinado primeiramente pelo controlo do crescimento
da pilha. Já discutimos as vantagens do código iterativo da optimização da última
chamada.
A complexidade em tempo é aproximada pelo número de reduções (tamanho das
árvores de prova – parte I (no Prolog-tamanho das árvores de procura)). Para
melhorar o melhor é usar bons algoritmos: ver ex:3.16a e 3.16b e 3.20 e 3.22).
A velocidade é medida em LIPS. Uma inferência lógica consiste numa redução em
computação.
Regras:
- Boa ordenação dos goals, onde a regra é “falhar o mais cedo possível”
- Exploração da facilidade de indexamento, ordenando os argumentos
adequadamente.
- Eliminação do não determinismo usando condições explícitas e cortes.
Vamos ver as normas para o uso de cuts:
As implementações do Prolog são mais eficientes se souberem que o predicado é
determinista. O primeiro objectivo do corte é dizer que o predicado é determinista e
não para controlar o backtracking. As duas regras básicas para usar o cut são:
- Fazer o cut tão local quanto possível
- Colocar o cut tão cedo assim que for sabido que a cláusula correcta foi escolhida.
Ex: do quicksort de 3.22.
quicksort([X|Xs],Ys) <--
partition(Xs,X,Littles,Bigs), quicksort(Littles,Ls),
quicksort(Bigs,Bs), append(Ls,[X|Bs],Ys).
Sabemos que só há uma solução para a partição da lista. Em vez de colocar o corte
na cláususla de quicksort, o predicado partition deve ser tornado determinista. Uma
das cláusulas da partição é:
partition([X|Xs],Y,[X|Ls],Bs) <--
X ≤ Y, partition(Xs,Y,Ls,Bs).
Se a cláusula tiver sucesso, então nenhuma outra será aplicada. mas o cut deve ser
aplicado antes da chamada recursiva em vez de depois, de acordo com o 2º princípio.
Em sistemas sem indexamento o corte é necessário.
A inserção de cortes deve ser feita depois do programa ter funcionado bem, pois é só
para eficiência.
...
CAP. 12 – Predicados Extra-Lógicos
caem fora do modelo de programação lógica.
Há três tipos: concernentes a I/O, para acessar e manipular o programa e para
interfacear com o sistema operativo. Só vamos dar os dois primeiros.
12.1. Input/Outpu
O predicado básico de input é read(X). Este goal lê um um termo do stream de entrada
corrente, usualmente o terminal. O termo lido unifica com X e o read tem sucesso ou
falha resultante da unificação.
Para output é write(X).
Nem o read nem o write dão soluções alternativas em backtracking.

Prog. 12.1.
writeln([X|Xs]) <-- write(X), writeln(Xs).
writeln([ ]) <-- nl.

nl é o predicado construído, que causa que o próximo carácter de output seja escrito
numa nova linha.
ex: executar o goal conjuntivo
(X=3, writeln([‘The value of X is ‘, X]) produz o output The value of X is 3.
O read e o write operam ao nível do termo. Um nível mais baixo é o do carácter, que é
assumido na maioria das implementações do Prolog como ASCII.
O predicado básico de output é:
put_char(Char).
O Prolog standard permite especificar o stream de output mas não vamos dar.
de entrada é o get_char(Char).

Prog.12.2 – Lendo uma lista de palavras


read_word_list(Words) <--
Words é uma lista de palavras lidas do stream de entrada via efeitos laterais.
read_word_list(Words) <--
get_char(FirstChar),
read_words(FirstChar,Words).
read_words(Char,[Word|Words]) <--
word_char(Char),
read_word(Char,Word,NextChar),
read_words(NextChar,Words).
read_words(Char,Words) <--
fill_char(Char),
get_char(NextChar),
read_words(NextChar,Words).
read_words(Char,[ ]) <--
end_of_words_char(Char).
read_word(Char,Word,NextChar) <--
word_chars(Char,Chars,NextChar),
atom_list(Word,Chars).
word_chars(Char,[Char|Chars],FinalChar) <--
word_char(Char), !,
get_char(NextChar),
word_chars(NextChar,Chars,FinalChar).
word_chars(Char,8 ],Char) <--
not word_char(Char.
A leitura das palavras termina com um carácter end-of-words, um ponto final por
exemplo. Neste programa as palavras podem ser separadas por um número arbitrário
de caracteres de preenchimento.
Será preciso ainda acrescentar as definições de word_char/1, fill_char/1 e
end_of_words_char/1.
É importante que o programa leia um carácter à frente e o teste para decidir o que
fazer (tem 3 hipóteses conforme o carácter seja duma palavra, de preenchimento ou
de final de lista de palavras).
ex:
process([ ]) <--
get_char(C), end_of_words_char(C).
process([W|Words]) <--
get_char(C), word_cahr(C), get_word(C,W), process(Words).

12.2. Acesso e Manipulação de Programas.


Foi assumido que os programas estão residentes na memória do computador mas não
como eles são representados ou como chegaram aí.
Muitas aplicações dependem do acesso às cláusulas do programa. Mais ainda, se os
programas são para ser modificados durant a sua execução, tem de haver uma
maneira de acrescentar e apagar cláusulas.
As primeiras implementações do Prolog eram como simples sistemas interpretativos,
classificando os predicados como construídos interiormente e estáticos ou definidos
pelo utilizador e dinâmicos. Hoje em dia, a classificação é mais sofisticada.
Cada predicado definido pelo utilizador pode ser dinâmico ou estático. Os
procedimentos de um dinâmico predicado pode ser alterado enquanto de um estático
não pode.
Os predicados builtin são assumidos como estáticos.
Os predicados de sistema introduzidos nesta secção aplicam-se apenas aos
predicados dinâmicos e causarão provavelmente mensagens de erro se aplicados a
predicados estáticos. Neste livro assumiremos que todos os predicados são dinâmicos
a menos que se especifique o contrário. Em muitas implementações do Prolog,
declarações são necessa´rias para fazer um predicado dinâmico.
O predicado sistema para aceder a um programa é:
clause(Head,Body).
e tem de ser chamado com a Head instanciada. O programa será procurado até
encontrar a primeira cláusula cuja head unifique com Head. A head e o body desta
cláususla são depois unificados com Head e Body.
Em backtracking, o goal tem sucesso uma vez para cada cláusula unificável no
procedimento. De notar que as cláusulas no programa não podem ser acessadas via o
seu body.
Os factos têm o átomo true no seu body. Os goals conjuntivos são representados
usando o functor binário , .
ex:
member(X, [X|Xs]).
member(X, [Y|Ys]) <-- member(X,Ys).
O goal clause(member(X,Ys),Body) tem duas soluções: {Ys=[X|Xs], Body=true} e
{Ys=[Y|Ys1], Body=member(X,Ys1)}.

Predicados de sistema são providos para adicionar cláusulas e remover cláusulas.


Para adicionar: assertz(Clause), que adiciona uma cláusula como a última da
correspondente procedimento.
Ex: assertz(father(haran,lot)?
Quando se tratar de descrição de regras é necessário um par de parêntesis adicional
devido a questões técnicas concernentes à precedência de termos.
ex: assertz((parent(X,Y <-- father(X,Y)))
asserta – adiciona no princípio em vez do fim.
Para remover usa-se o predicado retract(C), remove a primeira cláusula que unifique
com C.
Se for do tipo a <-- b,c,d retract((a <-- C))
...
12.4. Programas Interactivos
Uma forma comum de programa requer efeitos laterais é um loop interactivo.
Um comando é lido do terminal, respondido, e o próximo comando lido do terminal.
Nas linguagens convencionais os loops interactivos são implementados pela instrução
while.

Prog.12.4. Loop Interactivo Básico. Um comando é lido e depois ecoado no ecrã.


echo <-- read(X), echo(X).
echo(X) <-- last_input(X), !.
echo(X) <-- write(X), nl, read(Y), !, echo(Y).

O loop echo/read é invocado pelo goal echo. O coração do programa é a relação


echo(X), onde X é o termo a ser ecoado. O programa assume um predicado definido
pelo utilizador last_input/1, que tem sucesso se o argumento satisfaz a condição
terminal de input. Se a condição terminal for satisfeita pelo input, o loop termina. Caso
contrário o termo é escrito e um novo termo é lido.
De notar que o teste do termo é separado da sua leitura. Isto é necessário para evitar
a perda do termo. Termos não podem ser relidos. O mesmo já acontecia no programa
12.2. O carácter era lido e depois processado separadamente.

vamos ver dois exemplos de programas que usam o ciclo básico de leitura de um
termo, e depois processam-no. O primeiro é um editor de linha. O segundo é um
programa interactivo que é uma shell para comandos Prolog, que é essencialmente
um interpretador de nível de topo para Prolog, em Prolog.

A primeira coisa a pensar quando se escreve o editor é como representar o ficheiro.


cada linha do editor tem de estar acessível, junto com a posiçã do cursor, que é a
posição corrente dentro do ficheiro.
Usamos uma estrutura file(Before,After), onde antes é uma lista de linhas antes do
cursor. A posição do cursor é restrita a estar no fim da linha.
As linhas antes do cursor aparecem pela ordem invertida.
O loop básico aceita um comando do teclado e aplica-o para produzir uma nova
versão do ficheiro.

edit <-- edit(file([ ],[ ])).


edit(File) <--
write_prompt, read(Command), edit(File,Command).
edit(File,exit) <-- !.
edit(File,Command) <--
apply(Command,File,File1), !, edit(File1).
edit(File,Command) <--
writeln([Command, ‘ is not applicable’]), !, edit(File).
apply(up,file([X|Xs],Ys), file(Xs,[X|Xs])).
apply(up(N), file(Xs,Ys),file(Xs1,Ys1)) <--
N>0, up(N,Xs,Ys,Xs1,Ys1).
apply(down,file

Você também pode gostar