1. Środowisko ECL i PS e Języki programowania Prolog zadanie projektowe nr. 3 (2016/17) T. Goluch W projekcie wykorzystane będzie środowisko ECL i PS e. Dostępne jest ono pod adresem: http://eclipseclp.org/. Po zainstalowaniu środowiska będziemy mieli dostępne dwie jego wersje: konsolową (dla systemu Windows jest to <katalog-instalacji>\lib\i386_nt\eclipse.exe). Możemy dodać ścieżkę dostępu za pomocą komendy: PATH = %PATH%;<kataloginstalacji>\lib\i386_nt. Kompilacji dokonujemy komendą: eclipse -f <nazwa-pliku>. Plik powinien mieć rozszerzenie.ecl albo.pl i znajdować się w bieżącym katalogu, natomiast nazwa pliku może być podana z pominięciem rozszerzenia. Niemniej jednak po poprawnej instalacji środowiska ECL i PS e do uruchomienia plików.ecl powinno wystarczyć dwukrotne ich kliknięcie. Po uruchomieniu powinna się wyświetlić informacja o licencji i wersji kompilatora, oraz napis: [eclipse 1]: _ po którym można wpisywać kolejne zapytania. Aby opuścić program i powrócić do linii komend należy zastosować instrukcję halt.. graficzną (TkEclipse). Po utworzeniu pliku z programem (w ulubionym edytorze lub przy pomocy File Edit new...) możemy go skompilować poleceniem File Compile... lub wpisując [<nazwa-pliku-bez-rozszerzenia>]. w polu Query Entry. W tym wypadku plik powinien mieć rozszerzenie.ecl albo.pl i znajdować się w bieżącym katalogu. Nową ścieżkę bieżącego katalogu możemy podać wybierając opcję menu File Change Directory.... Zapytania wpisujemy w polu Query Entry. Jeśli chcemy wyczyścić wszystkie dotychczas skompilowane moduły możemy posłużyć się opcją menu File Clear toplevel module. 2. Programowanie w logice Program w języku PROLOG składa się z zestawu definicji klauzul. Najprostsza definicja klauzuli p/1 (1 oznacza liczbę parametrów klauzuli) ma postać: p(1). Oznacza ona, że klauzula ta jest spełniona dla parametru równego 1 (i tylko dla takiego). Jeżeli teraz wykonamy zapytanie: p(x). otrzymamy odpowiedź: programowania{c91e19c4-8b08-4d63-9b7c-0913aab2a7ce} 1
X = 1 Yes Po zadaniu zapytania PROLOG przegląda wszystkie zdefiniowane klauzule i próbuje dopasować do nich zapytanie. Na przykład, gdy p jest spełnione dla 1 i 2: p(1). p(2). > p(x) X = 1 \\ Yes, maybe more X = 2 \\ Yes, maybe more Aby uzyskać kolejne dopasowania należy, w zależności od wersji, nacisnąć średnik (wersja konsolowa eclipse.exe) albo nacisnąć przycisk more (wersja graficzna TkEclipse). Warto również zauważyć, że kolejność klauzul ma znaczenie, zamieniając ją otrzymamy następującą odpowiedź: p(2). p(1). > p(x) X = 2 \\ Yes, maybe more X = 1 \\ Yes, maybe more Ogólna definicja klauzuli ma postać: predykat(zmienne) :- cel. i oznacza, że klauzula dla zmiennych Zmienne jest spełniona, jeżeli spełniony jest cel, przy czym cel jest listą pod-celi połączonych koniunkcją (w postaci cel-1, cel-2,..., cel-n) lub alternatywą (w postaci (cel-1; cel-2;...; cel-n)). Np. klauzula: q( 0,0 ). q( X,Y ) :- X > 0, T is X 1, q( T,Y ). jest spełniona dla parametrów 0,0 oraz dla wszystkich X,Y takich, że q( X-1,Y ) jest spełnione. Do obliczenia wartości wyrażenia potrzebna jest klauzula is/2. W przeciwnym wypadku zmiennej logicznej T zostałoby przypisane wyrażenie symboliczne X 1. Klauzule postaci p(x). (tzn. z pustą listą celów) nazywamy faktami, pozostałe klauzule regułami. Zadajmy teraz zapytanie q( 7,0 ). Nie udało się go dopasować do pierwszej klauzuli, jednak dopasowanie do drugiej powiedzie się: zmienna X zostanie związana z wartością 7, Y zaś z zerem, po czym nastąpi sprawdzenie prawdziwości pod-celi. Pierwsze dwa są spełnione. Aby sprawdzić prawdziwość trzeciego podcelu, należy sprawdzić, czy q( 6,0 ) jest prawdziwe. Nastąpi ponowna próba dopasowania zapytania (tym razem q( 6,0 )). Będzie ono prawdziwe, jeżeli prawdziwe będzie q( 5,0 ) itd., aż do zapytania q( 0,0 ), które jest prawdziwe z definicji (pierwszy fakt). Nazwy zmiennych zawsze zaczynają się od wielkiej, a predykatów od małej litery. 3. Debugger W celu lepszego zrozumienia działania programu bądź analizy błędów możemy posłużyć się debuggerem. Wybieramy z menu opcję: Tools Tracer i ponownie uruchamiamy zapytanie np.: programowania{c91e19c4-8b08-4d63-9b7c-0913aab2a7ce} 2
q(7,0). W oknie stosu wywołań Call Stack wyświetlony zostanie aktualny (kolor niebieski) oraz potomne cele. Cele spełnione oznaczane są na zielono a niespełnione na czerwono. Aby kontynuować śledzenie należy wykorzystać przycisk Creep. Wszystkie wywołane cele w porządku chronologicznym można obejrzeć przeglądając zakładkę Trace Log. 4. Typy danych W prologu możemy wyróżnić następujące typy danych: a) liczby możemy wyróżnić cztery rodzaje: całkowite, w przypadku wykorzystania biblioteki GMP ograniczeniem na ich wielkość jest jedynie pamięć urządzenia: 123-27 34923748927492749495867593039484746374859589174 zmiennoprzecinkowe, do reprezentacji wykorzystywany jest typ double z języka C: 3.141592653589793 6.02e23-35e-12-1.0Inf wymierne (wartość reprezentowana jest jako iloraz dwóch nieograniczonych liczb całkowitych): 2_6, rational(2.5), 4_3 + rational(1.5) bounded reals (liczba rzeczywista która zawiera się pomiędzy dwoma wartościami zmiennoprzecinkowymi): 1.5 2.0, 1.0 1.0, 3.1415926535897927 3.1415926535897936 b) napisy są reprezentacją dowolnej sekwencji bajtów i zapisywane są w cudzysłowach: "hello" "I am a string!" "string with a newline \n and a null \000 character" c) atomy to proste stałe symboliczne (podobne do typów wyliczeniowych z innych języków). Składniowo, wszystkie słowa zaczynające się od małej litery oznaczają atomy, atomami są również sekwencje symboli są oraz cokolwiek w apostrofach: atom quark i486 -*-??? 'Atom' 'an atom' d) listy to uporządkowany ciąg elementów zawartych między nawiasami kwadratowymi i rozdzielonych przecinkami, np.: [1,2,3,5]. Szczególnym przypadkiem jest pusta lista, oznaczana przy pomocy []. Każda niepusta lista skonstruowana jest z głowy (pierwszego elementu) i ogona (listy pozostałych elementów, potencjalnie pustej). Konstrukcja listy z programowania{c91e19c4-8b08-4d63-9b7c-0913aab2a7ce} 3
głowy H i ogona T ma postać [H T]. Zatem lista [1,2,3] może być zapisana na jeden z równoważnych sposobów: [1,2,3] [1 [ 2,3 ]] [1 [2 [3]]] [1 [2 [3 []]]] Przykładowy predykat, wyznaczający długość listy ma postać: dlugosc( [],0 ). dlugosc( [_ T],L ) :- dlugosc( T,P ), L is P + 1. czyli długością pustej listy jest zero, zaś lista niepusta ma długość o jeden większą niż długość jej ogona. _ oznacza, że głowy listy nie wiążemy z żadną zmienną logiczną. Kolejny przykład dołączenie jednej listy na koniec drugiej: dolacz( [],L,L ). dolacz( [H T],L,[H X] ) :- dolacz( T,L,X ). Lista L dołączona do pustej listy daje listę L. Lista L dołączona na koniec listy składającej się z głowy H i ogona T tworzy listę zbudowaną z głowy H i ogona X będącego wynikiem dołączenia na koniec T listy L. Kolejny przykład sprawdzenie, czy X jest równy jednemu z elementów listy: nalezy( X,[X _] ). nalezy( X,[H T] ) :- nalezy( X,T ). e) struktury to nazwany zbiór o stałej liczbie argumentów o następującej składni: <name>(<arg>1,...<arg>n) przykłady struktur: 5. Operatory data(grudzien, 25, "swieto") pierwiastek(wodor, sklad(1,0)) lot(londyn, nowy_york, 12.05, 17.55) W prologu możemy łatwo uzyskać listę wszystkich 87 operatorów wraz z informacją o ich priorytecie i łączności. Wystarczy wykonać następujące zapytanie: gdzie: current_op(pierwszenstwo, Typ, Nazwa). Pierwszenstwo liczba całkowita z zakresu 0..1200, wyższa liczba oznacza wyższy priorytet, programowania{c91e19c4-8b08-4d63-9b7c-0913aab2a7ce} 4
Typ określa ilu argumentowy jest operator i czy jest łączny. Ponadto w przypadku operatorów dwuargumentowych jeśli jest łączny to czy prawo czy lewo stronnie. Np. yfx to operator dwuargumentowy (infiksowy) lewostronnie łączny gdzie: o f miejsce operatora, o y strona od której zostanie rozpoczęte wykonywanie wyrażenia, o x brak łączności z tej strony. Operator to symbol operatora. Jeśli chcemy np. zapytać o wszystkie operatory prefixowe łączne (jednoargumentowe) należy wykonać zapytanie: current_op(pierwszenstwo, fy, Nazwa). W szczególności, znając symbol operatora możemy uzyskać dodatkowe informacje, np: 6. Komentarze current_op(pierwszenstwo, Typ, :-). Komentarze w PROLOG u zawarte są pomiędzy /* i */ (jak w C) lub pomiędzy % a końcem linii, np.: p(1). /* to jest komentarz */ p(2). % to tez jest komentarz 7. Predykat nawrotu fail/0 oraz operator odcięcia! Wykonanie predykatu fail zawsze kończy się niepowodzeniem i wymusza nawrót sterowania. Operator odcięcia! dzieli klauzulę na dwie części i nie pozwala ma ponowne uzgadnianie (nawracanie) do lewej części. rozne(x, Y) :- X=Y,!, fail. rozne(_, _). Rozważmy powyższy przykład, operator odcięcia nie pozwala na Jeśli zmienne X i Y będą różne to pierwszy pod-cel nie będzie spełniony a zatem nastąpi sprawdzenie dopasowania kolejnego predykatu, który dla dowolnych dwóch zmiennych zawsze jest spełniony. Odpowiedź będzie poprawna Yes. W przypadku kiedy zmienne będą różne pierwszy pod-cel będzie spełniony ale drugi zawsze będzie niespełniony, Jednak operator odcięcia nie pozwoli na powrót do lewej części klauzuli i na ponowne uzgodnienie predykatu. 8. Programowanie z ograniczeniami Aby wykorzystać programowanie z ograniczeniami, należy zaimportować bibliotekę ic, przy pomocy polecenia (podanego na początku programu): :- lib(ic). programowania{c91e19c4-8b08-4d63-9b7c-0913aab2a7ce} 5
Typowe rozwiązanie problemu przy użyciu CLP ma postać: rozwiaz(zmienne):- wczytaj_dane(dane), ustaw_ograniczenia(dane,zmienne), labeling(zmienne). gdzie ustaw_ograniczenia/2 definiuje model problemu. Predykat labeling/1 próbuje znaleźć rozwiązania sprawdzając wszystkie podstawienia dla zmiennych. Rozważmy następujący problem, w którym mamy 8 zmiennych: S, E, N, D, M, O, R, Y, każda z nich oznacza inną cyfrę, oraz spełniona jest równość: S E N D + M O R E = M O N E Y Rozwiązanie tego problemu będzie miało postać: :- lib(ic). model(zmienne) :- Zmienne = [S,E,N,D,M,O,R,Y], Zmienne :: 0..9, alldifferent(zmienne), S #\= 0, M #\= 0, 1000 * S + 100 * E + 10 * N + D + 1000 * M + 100 * O + 10 * R + E #= 10000 * M + 1000 * O + 100 * N + 10 * E + Y. rozwiaz(zmienne) :- model(zmienne), labeling(zmienne). Predykat model najpierw tworzy listę zmiennych w naszym problemie. W drugim kroku przypisujemy wszystkim zmiennym (przy pomocy predykatu ::/2) jako domenę zbiór liczb naturalnych z przedziału <0, 9>. Aby stworzyć domenę będącą podzbiorem liczb rzeczywistych, przedział 0..9 należałoby zmienić na 0.0..9.0 lub użyć predykatu $::/2. W następnej linii określamy, że wszystkie zmienne muszą być różne. Na koniec stwierdzamy że ani S ani M nie mogą być równe zero (ograniczenia wyrażone operatorami porównania tworzy się dodając znak # przed operatorem) oraz opisujemy nasze równanie. 9. Literatura http://eclipseclp.org/ ECL i PS e A Tutorial Introduction (http://eclipseclp.org/doc/tutorial.pdf) ECL i PS e User Manual (http://eclipseclp.org/doc/userman.pdf) programowania{c91e19c4-8b08-4d63-9b7c-0913aab2a7ce} 6