Laboratorul 5
(Deadline: 05.05.2019 23:59:59)
Daca nu sunteti logati exercitiile nu se mai afiseaza.
Predicatul repeat
Predicatul repeat repeta interogarea expresiei (formata din predicate) care urmeaza dupa el. Repetarea se intampla atata timp cat valoarea expresiei de dupa repeat este falsa (rezultatul este no). Acest lucru e datorat faptului ca expresia de dupa repeat care a rezultat in esec trimite backtrackingul inapoi la predicatul anterior, adica repeat, care intotdeauna se termina cu succes, si are aceasta proprietate de a forta interogarea expresiei ce urmeaza dupa el. Daca rezultatul expresiei este yes, repetarea se opreste deoarece nu se mai intoarce din backtracking la repeat care sa forteze reinterogarea. Atentie, reinteorgarea e de fapt aceeasi interogare facuta din nou si nu o reinterogare ceruta de backtracking prin intoarcerea la expresia respectiva (asa cum veti observa in exemplele ce urmeaza).
Sa observam drept exemplu interogarea urmatoare prin care citim numere de la tastatura atata timp cat sunt nenule:
| ?- repeat,read(X),X==0,write(X).
|: 7.
|: 8.
|: 4.
|: 0.
0
X = 0 ? yes
| ?-
Dupa ce a fost citit 7, testul X==0 care da no
impinge backtrackingul inapoi la read, dar read prin felul in care e definit
nu se poate reinteroga la revenirea din backtracking
(este evaluat o singura data si revenirea din backtracking la el va rezulta cu no), asa cum se vede in exemplu:
| ?- read(X),X==0,write(X).
|: 7.
no
| ?-
Dar, in exemplul pentru repeat, dupa ce si read(X) a esuat cand a revenit backtrackingul la el, se trece cu inca un pas
inapoi, deci ajunge la repeat
care forteaza o noua interogare a lui read si ceea ce urmeaza dupa el. Procedeul se repeta pentru citirealui 8 si 4. Cand insa citeste
0, trece de conditia X==0, afiseaza numarul si apoi iese in sfarsit din regula.
Cel mai adesea repeat e insotit de un cut (atunci cand nu vrem sa repetam expresia pana la finalul regulii). Expresia de repetat se va gasi intre repeat si cut. De ce functioneaza asta?
De exemplu, extragem elemente din baza de cunostinte pana inalnim un p(0): | ?- listing(p).
p(1).
p(2).
p(0).
p(3).
p(4).
yes
| ?- repeat,retract(p(X)),write(X),nl,X==0,write(gata).
1
2
0
gata
X = 0 ? ;
3
4
Prolog interruption (h for help)? a
% Execution aborted
| ?-
Pentru prima solutie programul se comporta conform asteptarilor, sterge p(1), apoi repeta procedeul si sterge p(2), pentru 0 face la fel, dar fiind indeplinita si conditia X==0, ajunge la sfarsitul regulii si se termina cu succes. La cererea urmatoarei solutii, insa, (adica gasirea urmatorului 0), a fost nevoie de intreruperea executiei, deoarece, dupa ce a sters din baza de cunostinte si p(3) si p(4), nu a mai avut ce sa stearga, retract-ul dand mereu no si intorcandu-se la repeat care iar a fortat evaluarea lui. Observati ca dupa ce am cerut o noua solutie pentru X si deci i-am cerut sa continue in backtracking interogarea, s-a reintors in bucla creata de repeat. Practic repeatul folosit fara cut extinde bucla pana la sfarsitul regulii si in cazul in care se cer noi solutii, in multe din situatii ultima astfel de reinterogare va rezulta de fapt in bucla infinita.
Cu ajutorul unui cut ne putem delimita bucla sa nu mai fie pana la sfarsitul regulii si sa se efectueze intr-adevar doar pana la realizarea conditiei cerute:
| ?- listing(p).
p(1).
p(2).
p(0).
p(3).
p(4).
yes
| ?- repeat,retract(p(X)),write(X),nl,X==0,!,write(gata).
1
2
0
gata
X = 0 ? ;
no
| ?-
Atentie, deoarece reinterogarea expresiei de dupa repeat e identica fata de cea anterioara (e efectiv aceeasi interogare care se repeta), folosirea lui repeat are sens numai pentru predicate care schimba starea sistemului (care adauga ori sterg clauze din baza de cunostinte, care citesc de la tastura ori din fisier etc.), altfel daca se foloseste pentru predicate fara efecte asupra starii sistemului se va repeta acea interogare la nesfarsit cu rezultat identic.
Deci pana acum am vazut 3 moduri in care se poate realiza o actiune similara in mod repetat:
- recursivitate
- intoarcerea din backtracking (exemplu: metoda cu (...,fail);true)
- repeat
- recursivitatea o folosim cel mai bine pentru parcurgerea/construirea listelor, pentru executarea aceleiasi actiuni cu o variabila ce are la urmatorul pas valoarea incrementata sau decrementata etc,
- metoda cu intoarcerea
din bactracking e singura care permite trecerea la clauzele urmatoare ale unui predicat,
si deci parcurgerea bazei de cunostinte. De exemplu, daca avem:
p(5).
cu o interogare de forma p(X), write(X), fail; true am reusi sa afisam toate valorile pentru care p/1 este definit. In schimb daca am defini o clauza recursiva a unui predicat: afiseaza:-p(X), write(X), afiseaza. nu ar functiona corect, ci ar afisa la infinit numai prima valoare pentru ca la reinterogarea lui afiseaza in recursivitate, fiind o interogare noua si nu o intoarcere din backtracking va porni cu evaluatul clauzelor de la inceputul bazei de cunostinte. In cazul lui repeat, expresia se repeta cat timp rezultatul interogarii sale anterioare a fost no Daca scriem ceva in genul repeat, \+ (p(X),write(X)) negatia fiind necesara pentru a forta repetarea vom observa ca ne afiseaza mereu primul element (fiindca e practic vorba de o noua interogare si nu o intoarcere din backtracking). O alta idee ar fi o negare la sfarsitul expresiei, ceva in genul: repeat, p(X),write(X), fail. In cazul asta ne va afisa la infinit toate numerele din baza de cunostinte. Insa trecerea de la un element la altul in baza de cunostinte tot nu s-ar realiza datorita lui repeat, ci pur si simplu ar fi reinterogarea lui p(X) fortata de fail-ul de la final. Repeatul isi arata efectul doar prin faptul ca repeta afisarea tuturor elementelor din baza de cunostinte, deoarece cand nu mai are valori posibile pentru X din p(X) se intoarce in sfarsit si la repeat, care il trimite inapoi sa faca o interogare identica (si deci o noua afisare a tuturor elementelor).
p(2).
p(7).
Observatie: Pentru a-l opri ar trebui sa fortam expresia de dupa repeat sa dea yes cand a terminat de afisat, si cea mai naturala idee e sa adaugam un ;true pe care sa intre cand nu mai are ce afisa din p (adicarepeat, (p(X),write(X), fail;true)
), ceea ce de fapt face ca utilizarea lui repeat sa fie redundanta fiindca oricum backtrackingul reinterogheaza p(X) si cand nu mai are solutii se duce pe true si iese din clauza, nemaiajungand sa-l mai interogheze pe repeat. - repeat e folosit, cum se specifica si mai sus, pentru interogari care schimba starea mediului, cel mai adesea pentru citirea din fisiere (se schimba pozitia de unde se citeste). Citirea din fisiere, cum se va vedea mai jos, s-ar putea face si recursiv dar ar fi mult mai ineficienta.
Fisiere
Prologul ofera cateva predicate built-in care permit lucrul cu fisiere.
Citirea din fisier cu redirectare
Redirectarea inputului catre un fisier are drept efect citirea din acel fisier atunci
cand se apeleaza predicate care in mod obisnuit citesc din consola.
Pasii pentru realizarea citirii cu redirectare sunt urmatorii:
- (optional) Salvarea inputului curent intr-o variabila pentru a putea reveni la acesta dupa citirea din fisier, fapt care se realizeaza prin predicatul seeing(-InputVechi).
- Deschiderea fisierului prin redirectare, cu ajutorul predicatului see(+Cale_fisier). Parametrul lui see trebuie sa fie instantiat si poate reprezenta o cale relativa sau absoluta pentru fisier. Calea relativa are implicit ca director de referinta folderul in care se afla executabilul ce porneste consola Prolog. pentru a schimba folderul de lucru se merge la File -> Working Directory -> alegeti directorul pe care il doriti. Cand se da calea fisierului ca parametru pentru see, trebuie pusa intre apostroafe (altfel punctul dinaintea extensiei fisierului va va genera un Syntax error)
- Citirea elementelor din fisier. Citirea va folosi aceleasi predicate pe care le foloseam pentru a citi de la tastatura. Cel mai recomandat este sa facem citirea fisierului cu ajutorul unui repeat (repetitiv se va citi cate un element sau set de elemente din fisierul de intrare pana cand se ajunge la sfarsitul fisierului)
- (foarte important) Inchiderea fisierului de input cu predicatul seen.
- (optional) Revenirea la streamul de input anterior citirii fisierului, cu ajutorul predicatului see
avand ca parametru inputul anterior (acesta poate fi ori
user
, adica inputul standard ce permite citirea de la consola, ori un alt fisier).
La citirea pe termeni (cu ajutorul predicatului read, si cu elementele de citit scrise cu punct la sfarsit in fisier), cand se ajunge la sfarsitul fisierului read citeste termenul end_of_file. Deci citirea pe termeni se va opri cand termenul citit devine aceasta valoare. Atentie, in acest caz sfarsitul fisierului se poate "falsifica" adaugand in fisierul de intrare un termen de foma end_of_file., deoarece read va pune in parametrul sau aceasta valoare la fel ca si in cazul ajungerii la sfarsit de fisier. Acelasi lucru se intampla si cand cititi fisierul caracter de caracter cu get_char(X); cand ajunge la caracterul de sfarsit de fisier in X va pune end_of_file.
Exemplu de citire pe termeni (cu read):
%citeste_fis_r(+Fisin)
Avand fisierul de input:
citeste_fis_r(Fisin):- seeing(Input_curent),
see(Fisin),
repeat,
read(X),
(X==end_of_file
;
write(X),nl,fail),
!,
seen,
see(Input_curent).
1
abc
miau
yes
| ?-
Exemplu de citire caracter cu caracter:
%citeste_fis_chr_redir(+Fisin)
Avand fisierul de input:
citeste_fis_chr_redir(Fisin):-seeing(Input_curent),
see(Fisin),
repeat,
get_char(X),
(X==end_of_file
;
write(X),fail),
!,
seen,
see(Input_curent).
cip
1.abc
cip
yes
| ?-
Testarea faptului ca s-a ajuns la sfarsit de fisier se poate face si cu preficatul
at_end_of_stream(+Stream),
dar acesta nu
poate fi folosit in mod direct la citirea cu redirectare, deoarece nu se cunoaste streamul corespunzator fisierului redirectat,
insa acesta se poate afla astfel:
| ?- see('chestie.txt').
yes
| ?- current_stream(A,B,S).
A = 'e:/schimbate_pe site/inteligenta artificiala 2013/chestie.txt',
B = read,
S = '$stream'(14413488) ? ;
no
| ?-
Predicatul
current_stream
considera doar calea absoluta in primul paramteru. Deci desi working directory-ul e setat corespunzator:
| ?- current_stream('chestie.txt',_,S).
no
Pentru ca nu gaseste un stream deschis cu calea respectiva; trebuia data calea absoluta.
Atentie current_stream, in caz ca sunt mai multe fisiere deschise le va returna pe fiecare pe rand.
Aici in exemplu era doar un fisier deschis,
deci ca sa va asigurati ca va da streamul fisierului pe care il doriti, ii puneti si calea pe primul parametru.
Daca stiti ca predicatul de citire a fisierelor definit in programul vostru va primi mereu la interogare calea relativa,
si deci nu aveti acces direct la calea absoluta, va trebui prin program sa va construiti calea absoluta pornind de la
cea relativa. Practic trebuie sa concatenati valoarea
working_directory-ului cu calea relativa primita. Pentru a afla working directory-ul trebuie incarcata biblioteca file_systems, si
folosit predicatul curent_directory (despre biblioteci veti invata in curand in laboratorul 5).
| ?- use_module(library(file_systems)).
% module file_systems imported into user
yes
| ?- current_directory(D).
D = 'e:/schimbate_pe site/inteligenta artificiala 2013/' ?
yes
Dupa asta nu mai urmeaza decat sa concatenati valoarea directorului curent cu valoarea fisierului. Indicatie pentru concatenarea a 2 termeni: se
transforma in liste de caractere(cu atom_chars) se aplica concatenarea a 2 liste, se transforma lista rezultata in atom (tot cu atom_chars).
Scrierea in fisier cu redirectare
Redirectarea outputului este similara drept concept cu redirectarea inputului. Pur si simplu dupa
redirectare orice predicat de afisare in loc sa scrie in consola (in standard output) va scrie in fisier.
Pasii pentru realizarea scrierii cu redirectare sunt la fel ca si in cazul citirii:
- (optional) Salvarea streamului curent de output intr-o variabila pentru a putea reveni la acesta dupa scrierea in fisier, fapt care se realizeaza prin predicatul telling(-OutputVechi).
- Deschiderea fisierului prin redirectare, cu ajutorul predicatului tell(+Cale_fisier). Ca si in cazul lui see e vorba de o cale absoluta sau relativa ce trebuie scrisa intre apostroafe. Calea relativa foloseste ca director de referinta working directory.
- Scrierea in fisier. Datorita redirectarii outputului, toate predicatele de afisare in consola (de exemplu: write, format, put_char, nl) vor scrie acum in fisier.
- Inchiderea fisierului de output, cu ajutorul predicatului told.
- (optional) Revenirea la streamul de output anterior scrierii in fisier, cu ajutorul predicatului tell avand ca parametru outputul anterior .
Citirea si scrierea in fisiere fara redirectare
Desi folosirea redirectarii uneori ne usureaza munca (de exemplu cand scriem un predicat de afisare in consola pe care astfel il putem folosi si pentru a scrie intr-un fisier) uneori avem nevoie sa deschidem fisierele in modul clasic (exemplu: daca trebuie sa scriem si in consola si intr-un fisier in mod alternat in cadrul unui program).
In acest caz deschiderea fisierelor se face cu acelasi predicat open/3 dar bineinteles cu parametri diferiti. Primul parametru al lui open reprezinta calea catre fisier, al doilea reprezinta modul de deschidere al fisierului(read - citire, write - suprascriere, append - adaugare la sfarsitul fisierului), al treilea e un parametru calculat de catre open si reprezinta stream-ul prin care citim/scriem in fisier. Toate predicatele de citire si scriere in consola prezentate la laborator pana acum au si forma cu un parametru in plus (dat ca prim argument), si anume streamul, in rest ceilalti parametri ramanand la fel (de exemplu pentru read(X) exista si varianta read(Stream,X), pentru write(X) si nl, avem write(Stream,X) si nl(Stream) etc.). Streamurile se inchid cu predicatul close/1 care primeste ca argument chiar stream-ul.
Citirea din fisier
Sa luam drept exemplu codul:
%citeste_fis(+Fisin)
pentru fisierul de intrare:
citeste_fis(Fisin):- open(Fisin,read,Stream),
repeat,
read(Stream,X),
(X==end_of_file
;
write(X),nl,fail),
!,
close(Stream).
1
abc
miau
yes
| ?-
Atentie daca in urma unei interogari, un fisier ramane deschis (fie pentru citire fie pentru scriere),
nu-l veti mai putea deschide inca o data si nici nu va va mai lasa sa il modificati in editor si sa il salvati
pana nu inchideti fisierul sau chiar consola (care odata cu inchiderea sa elibereaza toate resursele).
| ?- open('fisier_output.txt',write,S).
S = '$stream'(14444752) ?
yes
| ?- open('fisier_output.txt',write,S).
! System error
! 'SPIO_E_FILE_ACCESS'
| ?-
Daca stiti valoarea streamului, il puteti inchide usor cu close, daca insa ati pierdut valoarea (de exemplu in urma unui
bug in program, prin care ati deschis fisierul dar nu l-ati mai inchis, si nu ati returnat/afisat valoarea streamului)
ori inchideti consola, ori definiti si interogati predicatul de mai jos, care inchide toate streamurile deschise(inclusiv cele
care au fost deschise cu see si tell, daca nu e vorba de unul din streamurile standard(de input, output si eroare)):
% module file_systems imported into user
yes
| ?- close_all_streams.
yes
| ?
Sa mai luam acum un exemplu. Vrem sa citim elementele dintr-un fisier, de exemplu:
b.
c.
d.
Si vrem sa punem aceste elemente intr-o lista. Ne vom folosi de un predicat dinamic in care ne vom cosntrui lista:
:-use_module(library(lists)).%incarcarea bibliotecii lists pentru a folosi predicatul reverse
:-dynamic lista/1. %predicat dinamic in care salvez lista de elemente
%citeste_fis_l(+Fisin,-L)
citeste_fis_l(Fisin, L):- retractall(lista(_)),%sterg faptele vechi
assert(lista([])),%valoarea initiala a listei
open(Fisin,read,Stream),
repeat,
read(Stream,X),
(X==end_of_file
;
retract(lista(Lv)),assert(lista([X|Lv])) %actualizez lista din baza de cunostinte, stergand lista veche si punand lista cu X-ul adaugat
,fail),
!,
close(Stream),
retract(lista(Li)),%obtin forma finala a listei
reverse(Li,L).%deoarece am adaugat la inceput lista e inversata
Atunci vom avea interogarea:
| ?- citeste_fis_l('fis_in.txt',L).L = [a,b,c,d] ?
yes
| ?-
Sa mai luam un exemplu, pentru a vedea si cum citim un fisier caracter de caracter. Predicatul urmator citeste continutul unui intreg fisier si il transforma in atom,
:-use_module(library(lists)).%incarcarea bibliotecii lists pentru a folosi predicatul reverse
:-dynamic lista/1. %predicat dinamic in care salvez lista de elemente
%citeste_fis_chr(+Fisin, - Atom)
citeste_fis_chr(Fisin, Atom):- retractall(lista(_)),%sterg faptele vechi
assert(lista([])),%valoarea initiala a listei
open(Fisin,read,Stream),
repeat,
get_char(Stream,X),%citesc doar cate un caracter
(X==end_of_file
;
retract(lista(Lv)),assert(lista([X|Lv])) %actualizez lista din baza de cunostinte, stergand lista veche si punand lista cu X-ul adaugat
,fail),
!,
close(Stream),
retract(lista(Li)),%obtin forma finala a listei
reverse(Li,L),%deoarece am adaugat la inceput lista e inversata
%daca doream doar lista cu caracterele ma opream la calculul lui L
%dar noi vrem atomul cu continutul fisierului
atom_chars(Atom, L).%am transformat lista de caractere in Atom
Consideram fisierul de intrare:
Si ca pisicile se tem de el.
A = 'Un soarece visa ca e dulau.\nSi ca pisicile se tem de el.' ?
yes
Scrierea in fisier
Sa luam ca exemplu:
scrie_fis(Fisout):- open(Fisout,write,Stream),
write(Stream,' o chestie'),
nl(Stream),
close(Stream).
%scrie_fis_ad(+Fisout)
scrie_fis_ad(Fisout):- open(Fisout,append,Stream),
write(Stream,' inca o chestie'),
close(Stream).
yes
| ?- scrie_fis_ad('chestie.txt').
yes
| ?- si fisierul rezultat: o chestie
inca o chestie