Guião para a Oitava Aula Prática de IPL

Delfim F. Marado Torres

5 de Maio de 1999

Agradeço comentários, sugestões ou correccões. O meu endereço de correio electrónico é < delfim@mat.ua.pt > . Desde já obrigado.

1  Objectivos da aula

São dois os objectivos desta aula:

2  Acesso a ficheiros

Introdução

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''.

Filosofia

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ã.

Manipulação de ficheiros

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.

Exercício

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).

Uma resolução

         % 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).

3   PARENTES

Árvore Genealógica

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.

Tarefa

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.

Os Dados

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).

Os Resultados

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.

Exemplos

Para a base de conhecimento acima, temos

         ?- consanguineos(joao,hugo,G).
            G = 2
         ?- consanguineos(bruno,teresa,G).
            G = 0

Uma resolução

         % 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.