Kunikowska Ewelina Grzechowski Ariel Zaawansowany prolog Kunikowska Ewelina Grzechowski Ariel
Krótkie przypomnienie Term to: stała, zmienna, struktura. Każdy term zapisywany jest jako ciąg znaków, tworzących cztery kategorie: duże litery: A-Z małe litery: a-z cyfry: 0-9 znaki specjalne
Krótkie przypomnienie Stałą nazywamy konkretne obiekty lub relacje. Istnieją dwa rodzaje stałych: atomy liczby Przykład: jas, malgosia, posiada, je Atomy składają się z: symboli, liter, cyfr znaku podkreślenia (zaczynamy zawsze małą literą), dowolnych znaków, jeśli ujęte są w pojedynczy cudzysłów. Liczby: -17, 23, 99.9, 123e-3
Krótkie przypomnienie Zmienne mają postać atomu złożonego z liter, cyfr lub znaku podkreślenia z zastrzeżeniem, że pierwszym znakiem musi być duża litera. Czasem interesuje nas tylko czy coś jest prawdą, ale zupełnie nie interesuje nas co. Jak sprawdzić, czy ktoś lubi Jasia? W takich sytuacjach możemy użyć zmiennej anonimowej zapisywanej jako jeden znak podkreślenia. lubi(_,jas).
Krótkie przypomnienie Struktura, inaczej term złożony, to obiekt złożony z innych obiektów. Przykład: posiada(‘Ewelina’,’prawo jazdy’).
Struktury a drzewa Zwykle łatwiej jest zrozumieć budowę struktury, gdy jest ona zapisana w postaci drzewa. W drzewie tym korzeniem jest funktor, a gałęziami elementy struktury. Ponadto struktury można zagnieżdżać.
Struktury a drzewa-przykład 1
Struktury a drzewa Drzewa również mogą opisywać zmienne w strukturach, w szczególności ukazując powiązania między nimi. Przykład:
Listy Lista to bardzo popularna struktura danych. Definiuje się ją jako ciąg uporządkowanych(tzn. kolejność ma znaczenie) elementów dowolnej długości. Elementy mogą być dowolnymi termami: stałymi zmiennymi innymi strukturami
Listy w Prologu Lista w Prologu to szczególny przypadek struktury, którą można zapisać w formie specjalnego rodzaju drzewa. Listę pustą oznaczamy poprzez [ ], a operatorem składania list jest . Ogólnie lista w Prologu dzieli się na dwie części : pierwszy element zwany głową oraz „resztę” zwaną ogonem
Listy w Prologu – przykłady [a, b, c] w zapisie formalnym .(a, .(b, .(c , []))) [[1,2],[3]] w zapisie formalnym .(.(1, .(2, [])), .(.(3, []), []))
Listy w Prologu – c.d. Wróćmy do kwestii głowy i ogona. W zapisie formalnym wygląda to następująco: .( głowa, ogon) . Zapis ten ułatwia operator | , więc wygląda to tak: [ głowa|ogon] W takim razie przykłady z poprzedniego slajdu można zapisać następująco: [a|b|c] [[1|2]|[3]]
polaczListy( G, O, [ G| O]). Listy w Prologu – c.d. Zapis ten uwidacznia się nie tyle dla konkretnych list, co dla dopasowań. Rozważmy przykładową klauzulę: polaczListy( G, O, [ G| O]). Klauzula ta umożliwia połączenie list G i O (polaczListy(1,[2,3],X) da wynik X=[1,2,3]) rozdzielenie gotowej listy na dwie pomniejsze (polaczListy(X,Y,[1,2,3]) da wynik X=1,Y=[2,3])
Przeszukiwanie rekurencyjne Często konieczne jest przeszukiwanie struktur w Prologu celem znalezienia pewnych informacji. Kiedy struktury mogą mieć inne struktury jako argumenty, konieczne jest przeszukiwanie rekurencyjne
Przeszukiwanie rekurencyjne Aby zrozumieć to zagadnienie, posłużymy się przykładem. Napiszmy ‘funkcję’, która sprawdzi przynależność elementu do listy. Najpierw sprawdźmy, czy poszukiwany element jest w ‘głowie’ naszej listy.
Przeszukiwanie rekurencyjne Zatem taka klauzula wygląda następująco nalezy( X, [X|_]). Jednak co w przypadku, gdy nasz element nie znajduje się w ‘głowie’?
Przeszukiwanie rekurencyjne Trzeba sprawdzić, czy poszukiwany element jest w ‘ogonie’. Najlepiej będzie sprawdzić, czy element jest w pierwszym elemencie ‘ogona’.
Przeszukiwanie rekurencyjne Klauzula sprawdzająca ten przypadek wygląda następująco: nalezy( X, [ _|O]) :- nalezy( X, O). Zauważmy, że klauzula ta odwołuje się do samej siebie.
Przeszukiwanie rekurencyjne W takim razie cała nasza ‘funkcja’ wygląda następująco: nalezy( X, [ X| _]). nalezy( X, [ _|O]) :- nalezy(X, O).
Przeszukiwanie rekurencyjne Oczywiście naszą ‘funkcję’ można wykorzystać dwojako: zgodnie z przeznaczeniem, czyli do sprawdzania, czy element należy do listy (nalezy(2,[1,2,3]). uzyskamy true.) wywołując nalezy(X,[1,2,3]). uzyskamy wszystkie elementy z listy [1,2,3] (czyli: X=1; X=2; X=3)
Przeszukiwanie rekurencyjne Istotną kwestią związaną z rekurencją jest to, czy jest ona lewostronna. Z taką sytuacją spotkamy się, jeśli reguła powoduje wywołanie takiego samego celu, jak cel, który ją wywołał. Zobaczmy to na przykładzie
Przeszukiwanie rekurencyjne- przykład Zdefiniujmy taką zależność: osoba(X) :- osoba(Y), matka(X, Y). osoba( adam). I zadajmy zapytanie osoba(X). Jak łatwo zauważyć, uzyskamy pętlę w teorii nieskończoną(w praktyce skończy się nam pamięć)
Przeszukiwanie rekurencyjne- przykład Jak to naprawić? Najłatwiej będzie po prostu zamienić kolejnością klauzule, tak aby rekurencja miała „na czym się zawiesić”. W większości ‘programów’ fakty, takie jak osoba(adam), powinny być umieszczane przed regułami.
Przeszukiwanie rekurencyjne Jakie są predykaty, w których fakty powinny być na końcu? Jednym z takich predykatów jest sprawdzenie, czy podany element jest listą.
Przeszukiwanie rekurencyjne Definicja tego predykatu wygląda następująco: jestLista([ X| Y]) :- jestLista( Y). jestLista( [] ). Polega on na sprawdzeniu, czy ostatni element ‘ogona’ jest listą pustą.
Przeszukiwanie rekurencyjne Niestety ten predykat nie jest odporny na zapętlenie, tzn. jeśli wywołamy jestLista(X). , to wpadniemy w pętlę. Istnieją owszem ‘ulepszenia’ tego predykatu, żeby się nie zapętlał, jednak odbywa się to kosztem osłabienia działania
Przeszukiwanie rekurencyjne- przykład polepszenia jestLista() mocnejestLista([]). mocnejestLista([_|_]).
Nawracanie i odcięcie Wyobraźmy sobie rekurencję jako pewnego rodzaju drzewo, tzn. każde kolejne wywołanie jej powoduje utworzenie nowego poziomu w drzewie. Jeżeli w którymś momencie osiągamy coś niepożądanego, to cofamy się w rekurencji do poziomu wyżej anulując dotychczasowe wyniki i wybieramy kolejną opcję.
Nawracanie i odcięcie Podobnie działa interpreter Prologa. Podczas działania tworzy on ‘drzewo’ w głąb. Jeżeli coś zawodzi, anuluje ostatni krok i wyniki z nim związane oraz wybiera następną pasującą klauzurę. Działanie to nazywamy nawrotem. Oczywiście może okazać się, że ‘piętro wyżej’ nie ma już wolnych klauzur, więc cofamy się o kolejne piętro do góry…
Nawracanie i odcięcie … i w górę i w górę … aż znajdziemy się w korzeniu i nie będzie alternatywnych klauzur do rozważenia. Wówczas osiągniemy wartość false oznaczającą ‘zawód globalny’.
Nawracanie i odcięcie Jednakże dzięki nawrotowi możemy uzyskać wszystkie możliwe rozwiązania danego zapytania, np. nalezy(X,[1,2,3]). dzięki mechanizmowi nawrotów wyświetli wszystkie elementy listy [1,2,3].
Nawracanie i odcięcie Pewną kontrolę nad nawrotami daje nam bezargumentowy funktor odcięcia wyrażany za pomocą ! . Dojście do niego powoduje, że od chwili wykorzystania w rekurencji klauzuli zawierającej owo odcięcie do chwili dojścia do niego jako podcelu zostają uznane za ostateczne, czyli nie mogą być anulowane.
Nawracanie i odcięcie Najlepiej będzie to widać na przykładzie: Załóżmy, że nasza klauzula wygląda następująco: a(X) :- b(X), c(X), !, d(X). Dojście do odcięcia uniemożliwia nawrót do b(X) i c(X), zatem jeśli zawiedzie d(X), to zawiedzie całe a(X).
Typowe zastosowanie odcięcia Można je podzielić na 3 grupy Kiedy chcemy poinformować, że znaleźliśmy odpowiednią regułę. Kiedy chcemy poinformować, że dany cel ma zawieść bez sprawdzenia alternatywnych rozwiązań Kiedy należy zaprzestać generowaniu alternatywnych rozwiązań przez nawracanie
Niebezpieczeństwa związane z odcięciem Posłużmy się przykładem. Mamy predykat stwierdzający ile rodziców ma dana osoba. Z Biblii wiemy, że ileRodzicow( adam, 0) :- !. ileRodzicow( ewa, 0) :- !. Natomiast reszta ludzi: ileRodzicow(X,2).
Niebezpieczeństwa związane z odcięciem Jeżeli zapytamy ileRodzicow( ewa, X) , otrzymamy X=0. Jeżeli zapytamy ileRodzicow( robert, X) , otrzymamy X=2. Jednak jeśli chcemy sprawdzić, czy Ewa ma dwoje rodziców, to zapiszemy ileRodzicow(ewa,2).
Niebezpieczeństwa związane z odcięciem I tu wyskakuje zonk. Okazuje się, że Ewa ma 2 rodziców. Dlaczego? Jest to konsekwencja sposobu przeszukiwania bazy przez Prolog. Jednak można temu zaradzić .
Niebezpieczeństwa związane z odcięciem ileRodzicow( adam, N) :- !, N=0. ileRodzicow( ewa, N) :- !, N=0. ileRodzicow( X, 2). albo ileRodzicow( adam, 0) :- !. ileRodzicow( ewa, 0) :- !. ileRodzicow( X, 2) :- \+(X= adam) , \+(X= ewa).
Wejście i wyjście w Prologu- wstęp Załóżmy, że mamy bazę wiedzy postaci: student(1, ’Ewelina’, 23). student(2, ’Robert’, 40). student(3, ‘Zuza’, 20).
Wejście i wyjście w Prologu- czytanie z klawiatury Do odczytu z klawiatury służy predykat read. Do naszej bazy zdefiniujmy relację czytaj(X) :- read(D), student(D, X). , polegającą na odczytaniu numeru studenta i pokazaniu jego danych.
Wejście i wyjście w Prologu- wypisywanie na ekran Do wypisywania na ekran służy predykat write. Do naszej bazy zdefiniujmy relację pisz(X) :- student( X, Y), write(Y). polegającą na wypisaniu na ekran danych studenta o numerze X.
Wejście i wyjście w Prologu- czytanie z pliku czytajzPliku :- open(‘sciezka_do_pliku',read,X), current_input(W), set_input(X), czytajKod, close(X), set_input(W). czytajKod :- read(T), write(T).
Wejście i wyjście w Prologu- czytanie z pliku czytajzPliku :- open(‘sciezka_do_pliku', read, X), //otworzenie pliku current_input(W), //zapis strumienia wejścia do W set_input(X), //zmiana wejścia na X czytajKod, //wykonanie czytajKod close(X), //zamknięcie wejścia X set_input(W). //zmiana wejścia na W czytajKod :- read(T), write(T).
Wejście i wyjście w Prologu- czytanie z pliku Domyślnym strumieniem wejściowym jest klawiatura. Jednakże czytajKod odczytuje tylko jeden wiersz z pliku. Aby odczytywać ich więcej należy go lekko zmodyfikować. czytajKod :- read(T), wykonaj(T). wykonaj( end_of_file ) :- !. wykonaj(T) :- write(T), nl, czytajKod. // nl- nowa linia
Wejście i wyjście w Prologu- zapis do pliku piszdoPliku :- open(‘sciezka_do_pliku',write,X), current_output(W), set_output(X), piszKod, close(X), set_output(W). piszKod :- write(T), nl.
Wejście i wyjście w Prologu- zapis do pliku piszdoPliku :- open(‘sciezka_do_pliku',write,X), //otworzenie pliku current_output(W), //zapis aktualnego wyjścia do W set_output(X), //zmiana aktualnego wyjścia na X piszKod, //wykonanie piszKod close(X), //zamkniecie wyjścia X set_output(W). //zmiana wyjścia na W piszKod :- write(T), nl.
Wejście i wyjście w Prologu- zapis do pliku Domyślnym strumieniem wyjściowym jest ekran. Oczywiście można zmodyfikować piszKod tak, aby końcem zapisu było wpisanie ‘pa’: piszKod :- read(X), \+(X=‘pa'), write(X), nl, flush, PiszKod. //flush czyści bufor
Predykaty wbudowane Predykaty wbudowane możemy podzielić na kilka kategorii, m.in.: Sukces i porażka Klasyfikacja termów Wpływ na nawracanie Równość Wejście i wyjście Obsługa plików Arytmetyka Porównanie I wiele, wiele innych …
Predykaty wbudowane- Sukces i porażka Do tych predykatów zaliczamy: true – ten cel nigdy nie zawodzi. Nie jest on niezbędny, gdyż klauzury i cele można tak ustawić, żeby był zbędny. Jednak dla wygody go zdefiniowano. fail – ten cel zawsze zawiedzie. Przydatny w parze z odcięciem albo gdy chcemy jawnie wywołać nawracanie.
Predykaty wbudowane- Klasyfikacja termów var(X) – nie zawodzi, gdy X jest zmienną nieukonkretnioną nonvar(X) – przeciwieństwo var(X) atom(X) – nie zawodzi, jeśli X jest atomem number(X) – nie zawodzi, jeśli X jest liczbą atomic(X) – nie zawodzi, jeśli X jest liczbą lub atomem
Predykaty wbudowane- Klasyfikacja termów integer(X) – jest spełniony, jeśli X jest stałą lub zmienną całkowitą real(X) – jest spełniony, jeśli X jest stałą lub zmienną stałoprzecinkową
Predykaty wbudowane- Wpływ na nawracanie repeat – to dodatkowa metoda generowania wielu rozwiązań przez nawracanie ! – na odcięcie można patrzeć jako na predykat wbudowany zatwierdzający pewne wybory dokonane przez interpreter.
Predykaty wbudowane- Równość X=Y – gdy Prolog napotyka taki cel, stara się zrównać X z Y dopasowując je do siebie. Jeśli to możliwe, to cel jest uzgodniony, w przeciwnym wypadku zawodzi. X==Y – powoduje on ‘lepsze’ zrównanie X i Y . Jeśli X==Y, to X=Y, ale nie odwrotnie.
Predykaty wbudowane- Wejście i wyjście get_char(X) – nie zawodzi, jeśli X można dopasować do następnego znaku z bieżącego strumienia wejściowego. Może być uzgodniony tylko raz! read(X) – odczytuje term z bieżącego strumienia wejściowego i dopasowuje go do X. Musi kończyć się kropką.
Predykaty wbudowane- Wejście i wyjście put_char(X) – wpisuje do bieżącego strumienia wyjściowego znak X nl – wpisuje do bieżącego strumienia wyjściowego sekwencję powodującą przejście do nowego wiersza. write(X) – powoduje zapisanie termu X do bieżącego strumienia wyjściowego.
Predykaty wbudowane- Obsługa plików open(X,Y,Z) – otwiera plik o nazwie X. Jeśli Y jest read, to w trybie odczytu, w przeciwnym razie w trybie zapisu. Z oznacza strumień, do którego należy używać podczas odwoływania się do pliku close(X) – używany do zamknięcia strumienia X
Predykaty wbudowane- Obsługa plików set_input(X) – ustawia bieżący strumień wejściowy na wskazany przez X set_output(X) – ustawia bieżący strumień wyjściowy na wskazany przez X current_input(X) – cel jest uzgodniony, jeśli nazwa bieżącego strumienia wejściowego pasuje do X. W przeciwnym razie zawodzi current_output(X) - cel jest uzgodniony, jeśli nazwa bieżącego strumienia wyjściowego pasuje do X. W przeciwnym razie zawodzi
Predykaty wbudowane- Arytmetyka X is Y X + Y X - Y X * Y X / Y X mod Y Pozostawię to bez omówienia
Predykaty wbudowane- Porównanie X = Y X < Y X > Y X >= Y X =< Y To również pozostawię bez omówienia
Predykaty wbudowane- Porównanie Zasady decydujące, które z termów są mniejsze: Wszystkie zmienne nieukonkretnione są mniejsze od wszystkich liczb zmiennoprzecinkowych. Te natomiast są mniejsze od wszystkich liczb całkowitych, które są mniejsze od wszystkich atomów, które są mniejsze od jakichkolwiek struktur
Predykaty wbudowane- Porównanie Zasady decydujące, które z termów są mniejsze: W przypadku dwóch niepowiązanych, nieukonkretnionych zmiennych zawsze jedna z nich będzie mniejsza od drugiej Porównywanie liczb zmiennoprzecinkowych i całkowitych odbywa się zawsze zgodnie z intuicją
Predykaty wbudowane- Porównanie Zasady decydujące, które z termów są mniejsze: Atom jest mniejszy od innego, jeśli występuje przed nim w zwykłym porządku słownikowym Jedna struktura jest mniejsza od drugiej, jeśli jej funktor ma mniej argumentów. Jeśli dwie struktury mają tyle samo argumentów, to mniejsza jest ta, której funktor jest mniejszy. Jeśli dwie struktury mają tyle samo argumentów i takie same funktory, to bada się po kolei pierwszy z pierwszym, drugi z drugim itd.
Predykaty wbudowane- Porównanie X @< Y – nie zawodzi, jeśli X jest mniejszy od Y w porządku opisanym na poprzednich slajdach. X @> Y – nie zawodzi, jeśli X jest większy od Y w porządku opisanym na poprzednich slajdach. X @>= Y – nie zawodzi, jeśli X jest większy lub równy Y w porządku opisanym na poprzednich slajdach. X @=< Y – nie zawodzi, jeśli X jest mniejszy lub równy Y w porządku opisanym na poprzednich slajdach.
Przykłady
Dziękujemy za uwagę