Agradeço comentários, sugestões ou correccões. O meu endereço de correio electrónico é < delfim@mat.ua.pt > . Desde já obrigado.
São dois os objectivos desta aula:
Até agora, o meio de comunicação entre o utilizador e os nossos programas Prolog têm sido as questões que colocamos ao intepretador e as respostas que ele nos dá por instanciação de variáveis ou por mensagens escritas no ecrã (pelo write ou put). Vamos agora ver um outro método de comunicação: uso de ficheiros para entrada e saída de informação e não apenas o teclado e o ecrã.
Muitos dos predicados de acesso a ficheiros dependem do interpretador Prolog usado. No seguimento da filosofia do nosso curso, limitamo-nos aqui aos predicados ``standard''.
No Prolog, tal como no C, os dados introduzidos pelo utilizador via teclado e os dados de saída que aparecem no ecrã, são tratados como casos particulares, respectivamente, de dados provenientes de ficheiros de entrada e de dados a escrever em ficheiros de saída. Estes pseudo ficheiros (teclado e ecrã) são referidos (ambos) por user. O nome dos outros ficheiros podem ser escolhidos pelo programador.
Em cada instante, o Prolog tem (apenas) dois ficheiros activos: um para entrada (input) e outro para saída (output). Estes dois ficheiros são designados por ficheiro de entrada corrente e ficheiro de saída corrente. Por defeito, o ficheiro de entrada corrente é o teclado e o ficheiro de saída corrente é o ecrã.
O ficheiro de entrada corrente pode ser mudado para um ficheiro de nome FichL com o predicado see/1:
see(FichL)
A menos que ocorra algo de errado com o sistema operativo, a chamada ao átomo see(FichL) tem sucesso e todo o read ou get posterior irá buscar a informação ao ficheiro de nome FichL em vez do habitual teclado.
De um modo geral, queremos ler algo de um ficheiro e no fim tornar a colocar o teclado como o ficheiro de entrada corrente. Assim sendo, um exemplo típico é:
... see(fich1), le_do_ficheiro(Informacao), % predicado vosso seen, see(user), ...
O comando seen serve para fechar o ficheiro de leitura corrente.
O ficheiro de saída corrente pode ser mudado para um ficheiro de nome FichE com o predicado tell/1:
tell(FichE)
Uma sequência típica é:
... tell(fich2), escreve_no_ficheiro(Informacao), % predicado vosso told, tell(user), ...
O comando told fecha o ficheiro de escrita corrente.
Todos os ficheiros são ficheiros de texto. Estes ficheiros são processados sequencialmente. Significa isto que depois de lermos uma informação, não podemos voltar atrás e tornar a lê-la; ou escrever algo e voltar atrás (o comportamento é idêntico ao do ecrã).
Após o comando see a posição corrente coincide com o início do ficheiro. Depois de uma leitura, a posição corrente é movida para o próximo item não lido, de maneira que a próxima leitura começará nesta nova posição. Se fizermos um pedido de leitura e a posição corrente coincidir com o fim do ficheiro, então a informação retornada por tal pedido será a constante end_of_file.
Com a escrita passa-se o equivalente. Logo após o comando tell, a posição
corrente coincide com o início do ficheiro. A cada pedido de escrita corresponde o
acrescento dessa informação a seguir à posição corrente e a mudança da posição
para o final da informação no ficheiro. Não é possível mexer a posição corrente
para trás e sobrepor parte do ficheiro.
Dependendo da forma da informação, podemos olhar para os ficheiros de duas maneiras. Uma consiste em olhar para o caracter como o elemento básico do ficheiro. Nesse caso usamos os predicados get (ou get0) e put para, respectivamente, ler e escrever um (único) caracter. Estes predicados lidam com códigos ASCII. Um exemplo:
?- put(65), put(66), put(67). ABC yes
A diferença entre o get e get0 é que o primeiro despreza os espaços brancos (na verdade todos os caracteres não visíveis) enquanto o segundo não.
A segunda maneira consiste em considerar unidades de informação maiores (por exemplo cláusulas) como as unidades de informação básicas do ficheiro. Neste caso, um pedido de leitura ou escrita corresponderá à transferência da cláusula completa do ficheiro de entrada corrente ou para o ficheiro de saída corrente. Os predicados de leitura e escrita são neste caso o read e o write.
Suponhamos que temos um ficheiro constituído por factos da forma
item(NumeroDoItem, Descricao, Preco, Fornecedor).
Pretendemos produzir um outro ficheiro que contém apenas os itens de um dado fornecedor. Como o fornecedor, neste novo ficheiro, será sempre o mesmo, basta que o seu nome seja escrito apenas uma vez, no início do ficheiro, e omitido nos factos seguintes.
Se camaras.dat contiver a seguinte informação
item(3122800,'QV-7000SX',161,casio). item(9991700,'PhotoPC',130,epson). item(3421122,'MX-500',100,fuji). item(3421132,'MX-700',145,fuji). item(1112233,'Q-M100V',130,konica).
e introduzirmos no interpretador
?- see('camaras.dat'), tell('fuji.dat'), selecciona(fuji), seen, told, see(user), tell(user). yes
queremos obter o ficheiro fuji.dat com a seguinte informação:
fornecedor(fuji). item(3421122, MX-500, 100). item(3421132, MX-700, 145).
% Delfim F. Marado Torres % Testado no SWI-Prolog em 4/Maio/1999 selecciona(Fornecedor) :- write(fornecedor(Fornecedor)), write('.'), nl, sel(Fornecedor). sel(F) :- read(Item), processa(Item,F). processa(end_of_file,_). processa(item(N,D,P,F),F):- % o Item lido e' do fornecedor desejado write(item(N,D,P)), write('.'), nl, sel(F). processa(_,F) :- % o Item lido n~ao e' do fornecedor desejado sel(F).
Familias é um grafo orientado não-pesado em que os vértices (ou nodos) representam indivíduos (identificados por um nome único) e a relação binária que define os ramos (ou arestas) do grafo é eFilhoDe. Assim, se os indivíduos I1 e I2 formam um ramo (o par (I1,I2) pertence à relação), quer dizer que I1 é filho de I2.
A sua tarefa é escrever um programa Prolog que, usando Familias, seja capaz
de verificar se há laços de sangue entre dois indivíduos dados, isto é se eles são
parentes.
Além de verificar a consanguinidade entre os indivíduos, interessa também
detectar a proximidade ou afastamento dessa ligação, determinando o grau de parentesco
entre ambos. Para esse efeito, considera-se o grau de parentesco entre dois
indivíduos como sendo a soma das distâncias de cada um ao parente comum mais
próximo; note, para isso, que a distância de um filho ao seu progenitor é igual a
1.
Uma instância do grafo Familias, pode ser, por exemplo,
que será representado como:
eFilhoDe(teresa,belmira). eFilhoDe(teresa,manuel). eFilhoDe(joao,manuel). eFilhoDe(joao,ana). eFilhoDe(bruno,ana). eFilhoDe(manuel,antonio). eFilhoDe(hugo,antonio). eFilhoDe(hugo,ana). eFilhoDe(jose,luis).
O programa deve ser chamado através do seguinte predicado:
consanguineos( Ind1,Ind2,Grau ).
em que se supõe instanciados Ind1 e Ind2 com nomes de indivíduos que figuram em Familias como vértices; o resultado virá em Grau, que será igual a 0 (zero) se não houver qualquer laço de sangue entre os dois, ou então indicará o grau de parentesco entre eles.
Para a base de conhecimento acima, temos
?- consanguineos(joao,hugo,G). G = 2 ?- consanguineos(bruno,teresa,G). G = 0
% Delfim F. Marado Torres; 5/Maio/1999 consanguineos(I1,I2,G) :- vertice(I1), vertice(I2), I1 \= I2, dist_todos_caminhos(I1,I2,Lista), menor(Lista,G). consanguineos(_,_,0). vertice(X) :- eFilhoDe(X,_). vertice(X) :- eFilhoDe(_,X). dist_todos_caminhos(I1,I2,L) :- findall( G, ( vertice(X), X \= I1, X \= I2, caminho(I1,X,D1), caminho(I2,X,D2), G is D1+D2 ), L ). menor([X],X). menor([X|R],Z) :- menor(R,Y), (X =< Y, !, Z = X ; Z = Y). caminho(X,Y,D) :- cam(X,Y,D,[]). cam(X,Y,1,_) :- eFilhoDe(X,Y). cam(X,Y,D,L) :- eFilhoDe(X,Z), not member(Z,L), cam(Z,Y,D1,[Z|L]), D is D1 + 1.