Prolog Praca z danymi zewnętrznymi Zastosowania
Transkrypt
Prolog Praca z danymi zewnętrznymi Zastosowania
Prolog Praca z danymi zewnętrznymi Zastosowania Piotr Jarzyński, Krystian Marynowski, Arkadiusz Nowacki, Mateusz Lewandowski PWSZ PŁOCK Domyślnie w Prologu wszystkie wprowadzone dane są napisane w składni tego języka. Założenie to jest utrudnia efektywną pracę. Niezbędne są algorytmy umożliwiające pracę z danymi w innych formatach. Odczytywanie danych Wprowadzenie linii jako string: read_str(String) :get0(Char), read_str_aux(Char,String). read_str_aux(-1,[]) :- !. % end of file read_str_aux(10,[]) :- !. % end of line (UNIX) read_str_aux(13,[]) :- !. % end of line (DOS) Konwersja stringa na stałą znakową: read_atom(Atom) :read_str(String), atom_codes(Atom,String). Konwersja stringa na liczbę: read_num(Atom) :read_str(String), number_codes(Atom,String). Zapożyczanie kodu z zewnętrznych źródeł Często pisząc skomplikowane algorytmy chcemy zachować porządek w kodzie przechowując różne części programu w odrębnych plikach. Załóżmy, że chcemy aby nasz program mógł korzystać z kodu zawartego w readstr.pl Wystarczy, że na początku naszego pliku dodamy: :- ensure_loaded("readstr.pl"). A kompilator załaduje wybrany plik. Znaczy to dokładnie "Jeśli plik readstr.pl nie jest jeszcze wczytany, wczytaj go teraz". Dzięki temu unikniemy niepotrzebnej duplikacji kodu. Dostęp do plików Wiele aplikacji wymaga aby wynik działania programu zapisany został do pliku. Omówimy jak zapisać dane do pliku oraz jak je odczytać. Prolog pozwala na dostęp plików poprzez uchwyt, wykorzystując predykat open np: open('file.txt',read,Handle). otworzy plik file.txt w trybie odczytu (lub do zapisu jeżeli drugi argument ustawimy na write) następnie przypisuje uchwyt do Handle. Może on być od teraz wykorzystywany do wykonywania operacji na pliku do czasu jego zamknięcia poprzez: close(Handle). Zapisywanie do pliku Poniższa procedura pozwoli użytkownikowi dodać tekst do pliku: appendLineToFile(FileName) :write('Append text to file '), write(FileName), write(': '), read_str(X), atom_codes(Atom,X), open(FileName,append,FileStream), nl(FileStream), write(FileStream, Atom), close(FileStream). Odczyt pliku Kolejna procedura wczyta plik i wyświetli na ekran: processFile(FileName) :see(FileName), processChar, seen. processChar :get0(Code), processCode(Code). processCode(-1):- !. processCode(Code):- name(L,[Code]), write(L), processChar. Pliki z rekordami o wartościach stałej długości Pisząc programy często możemy spotkać się z plikami tekstowymi zawierającymi ustalone pola o stałej długości. Pliki te często różnią się od siebie, mogą ale nie muszą kończyć się znakiem końca linii, zawierać puste pola, mogą to być pliki tekstowe lub innego formatu. Odczytanie pliku rekordów o polach stałej długości jest jednak stosunkowo proste. Zaczynamy od liczby bajtów która ma być odczytana i odliczamy w dół do 0: read_bytes(0,[]) :- !. read_bytes(N,[C|Rest]) :get_byte(C), NextN is N-1, read_bytes(NextN,Rest). Wersja read_bytes , której używamy jest bardziej skomplikowana. Wykorzystuje jeden znak LookAhead aby sprawdzić nieoczekiwany znak na końcu pliku(end-of-file). Możemy również zdefiniować skip_bytes, który pomija określoną liczba bajtów, bez zapisywania ich na liście. Pliki CSV (wartości oddzielone średnikiem) Odczyt plików, w których pola rekordu są oddzielone średnikiem (CSV - Comma Separated Value) jest jeszcze prostszy z uwagi na wbudowaną w prologu procedurę: csv_read_file(+File, -Rows, +Options) Przykładowo aby odczytać dane z pliku commadel.dat wykonajmy: csv_read_file('commadel.dat', Rows, [functor(record), arity(6)]). W rezultacie otrzymamy listę Rows z rekordami o sześciu polach. Przetwarzanie danych jako listy Rozważmy poniższą bazę wiedzy: ojciec(michał,kasia). ojciec(andrzej,michał). ojciec(daniel,monika). Możemy wyświetlić imiona wszystkich ojców poprzez zapytanie: ?- father(X,_), write(X), nl, fail. Czyli: znajdź X, dla którego ojciec(X, _), wypisz i zwróć fail aby znaleźć kolejne. Jednak co w przypadku jeśli zamiast wyświetlania imion ojców, chcemy je dalej przetwarzać jako lista? W celu uzyskania wszystkich imion, program musi wykonać powrót poprzez fail. Ale w celu zbudowania listy musi użyć rekurencji przekazując częściowo zbudowaną listę jako argument między iteracjami czego nie można zrobić korzystając z backtrakingu. Z pomocą przychodzi nam wbudowany predykat findall/3: findall(+Template, +Goals, -List) Możemy go użyć w następujący sposób: ?- findall(X,father(X,_),L). Czyli: znajdź wszystkie X, dla którego ojciec(X, _) i zbuduj z nich listę L. „bagof” i „setof” Największa różnicą między bagof i setof a findall jest to jak traktują one obsługę niezainicjalizowanych zmiennych w "Goals", findall je pomija dlatego instrukcja: ?- findall(X,parent(X,Y),L). Znajdzie, każdego kto jest rodzicem kogokolwiek. Y może mieć różne wartości dla każdego zrodziców. Tymczasem: ?- bagof(X,parent(X,Y),L). Znajdzie wszystkie wartości X, które są powiązane z jakąś konkretną wartością Y. Jeśli w miejscu Y pojawią się różne wartości zostaną one zwrócone jako alternatywne rozwiązania zamiast dodania ich do jednej listy. Setof zachowuje się podobnie do bagof z tą różnicą, że wyniki są posortowane alfabetycznie oraz kasowane są duplikaty. Operacja sortowania wymaga dodatkowego czasu czasu, ale może być korzystna, ponieważ chroni nas przed duplikatami w późniejszej pracy. parent(michael,cathy). parent(melody,cathy). parent(greg,stephanie). parent(crystal,stephanie). ?- findall(X,parent(X,Y),L). X = _0001, Y = _0002, L=[michael,melody,greg,crystal] ?- bagof(X,parent(X,Y),L). X = _0001, Y = cathy, L = [michael,melody] ; X = _0001, Y = stephanie, L = [greg,crystal] ?- setof(X,parent(X,Y),L). X = _0001, Y = cathy, L = [melody,michael] ; X = _0001, Y = stephanie, L = [crystal,greg] Oczywiście „setof” jest jak „bagof” z wyjątkiem, że sortuje listy i usuwa duplikaty (jeśli jakiekolwiek występują). Ale jest inny sposób. Można zrobić to: ?- bagof(X,Y^parent(X,Y),L). X = _0001, Y = _0002, L = [michael,melody,greg,crystal] Znajdowanie najmniejszego, największego, lub „najlepszego” rozwiązania "Najlepsze" oznacza różne rzeczy w różnych sytuacjach, ale podstawową ideą jest to, że ze wszystkich możliwych rozwiązań, chcesz to, które przewyższa wszystkie inne zgodnie z pewnymi kryteriami. Istnieją trzy główne metody: Użyć „setof” i wykorzystać wbudowany proces sortowania,tak że "najlepsze" rozwiązanie wychodzi na początku (amoże na końcu) listy; Użyć „bagof” lub „setof” a następnie pracować z listą , abywybrać rozwiązanie, którego potrzebujesz; Szukaj bezpośrednio „najlepszego” rozwiązania, porównując każdą alternatywę na tle wszystkich pozostałych Popracujmy z następującą bazą danych: age(cathy,8). age(sharon,4). age(aaron,3). age(stephanie,7). age(danielle,4). Które dziecko jest najmłodsze? Spróbujemy pierwszej i trzeciej strategii, pozostawiając drugą jako ćwiczenie. Jest to łatwe do wykonania. „setof” daje nam wiek najmłodszego dziecka. Weźmy pod uwagę te zapytania: ?- setof(A,N^age(N,A),L). L = [3,4,7,8] ?- setof(A,N^age(N,A),[Youngest|_]). Youngest = 3 Pierwsza kwerenda pobiera posortowaną listę wieku dzieci; druga kwerenda pobiera tylko pierwszy element tej listy. Definiowanie operatorów Większość funktorów w Prologu jest zapisana bezpośrednio przed nawiasami zawierającymi argumenty: functor(arg1, arg2). Funktory, które mogą być napisane w inny sposób nazywane są operatorami. Na przykład, struktura +(2,3) może być zapisana jako 2+3, ponieważ jej funktor + Jest operatorem. Nie należy mylić operatorów z operacjami. Niektóre operatory oznaczają arytmetyczne operacje(+-*/), ale inne operatory służą zupełnie innym celom. W rzeczywistości każdy funktor w Prologu może być zadeklarowany jako operator zmieniając w ten sposób swoją składnie, ale nigdy nie znaczenie. Powszechnie predefiniowane operatory w PROLOGu. Pierwszeństwo Specyfikator Operatory 1200 xfx :- --> 1200 fx :- ?1100 xfy ; 1050 xfy -> 1000 xfy , 900 fy \+ (lub w niektorych Prologach, not) 700 xfx = \= == \== @< @=< @> @>= is =:= =\= < =< > >= =.. 500 yfx +400 yfx * / // mod 200 xfy ^ 200 fy - Specyfikatory operatorów Specyfikator Fx Fy Xf Yf Xfx xfy yfx Znaczenie Prefix, not associative Prefix, right-associative (like \+) Postfix, not associative Postfix, left-associative Infix, not associative (like =) Infix, right-associative (like the comma in compound goals) Infix, left-associative (like +) Nadawanie znaczeń operatorom Definicje operatora określają jedynie jego składnie. Znaczenie lub semantyka operatora zależy już od programisty. W przeszłości niektóre Prologi używały ampersandu (&) zamiast przecinka aby łączyć elementy funkcji złożonych . Jest to wielką zaletą, ponieważ złożone funkcje nie wyglądają jak listy argumentów; zapytanie ?- call(p(a) & q(a) & r(a)). wyraźnie wywołuje „call” tylko z jednym argumentem. W zwykłym Prologu, musimy użyć dodatkowych nawiasów, aby uzyskać ten sam efekt, jak tutaj: ?- call( (p(a), q(a), r(a)) ). ponieważ bez dodatkowych nawiasów, „call” będzie traktowane jako posiadające trzy argumenty. Możemy zdefiniować ampersanda aby pracował w ten sposób nawet w zwykłym Prologu. Najpierw zdefiniujmy jego składnię. Chcemy aby & był operatorem wrostkowym z nieco niższym priorytetem niż przecinek, dzięki czemu f (& b, c) będzie oznaczać f ((& b), c),a nie f (i (b, c)). Ponadto, jak wkrótce zobaczymy, & powinno być w prawo-asocjacyjne. Tak więc, w większości Prologów stosowną definicją operatora będzie: :- op(950,xfy,&). Następnie musimy powiedzieć Prologowi jak rozwiązać funkcję zawierającą ampersandy. Oczywiście zapytanie GoalA & GoalB powinno zadziałać jeżeli GoalA zadziała a następnie GoalB zadziała z tymi samymi instancjami. Czyli: GoalA & GoalB :- call(GoalA), call(GoalB). To jest po prostu zwykła reguła Prologa. Równie dobrze może to być zapisane jako: '&'(GoalA,GoalB) :- call(GoalA), call(GoalB). PROLOG W PROLOGu Nasza definicja ampersandu „&” sugeruje strategię przepisania całego mechanizmu inferencji Prologa do Prologa, aby rozwinąć tym samym zmodyfikowaną wersję języka. Przypomnijmy, że predykat clause(Head, Body) może odzyskać jakąkolwiek z klauzul w bazie danych, a przynajmniej te, które są zadeklarowane dynamicznie; Robi to, próbując połączyć Head z nagłówkiem klauzuli oraz body z ciałem klauzuli(lub z „true” jeśli klauzula jest faktem). Klauzule alternatywne są otrzymywane jako wielokrotne rozwiązania „clause” przy pomocy backtrackingu. Ciało reguły jest zazwyczaj funkcją złożoną(compound goal), czyli struktura utrzymywana jest przy pomocy prawostronnych(right-associative) przecinków, które działają dokładnie jak ampersandy zaprezentowane wcześniej. Tak więc biorąc pod uwagę regułę: f(X) :-g(X), h(X), i(X), j(X). zapytanie: ?clause(f(abc),Body). Utworzy instancję „Body” g(abc), h(abc), i(abc), j(abc) która odpowiada: g(abc), (h(abc), (i(abc), j(abc))) Aby wykonać to zadanie postąpimy w ten sam sposób jak zrobiliśmy to w przypadku ampersandów. Możemy zdefniować interpret ( który pobiera funkcję jako argument i wykonuje ją). Następnie aby użyć „interpret” wystarczy napisać, np.: ?interpret(grandparent(X,Y)). Zamiast ?grandparent(X,Y). Jest to algorytm Clocksin'a i Mellish'a (1984:177) . Cięcia są „zielone” : zachowują kroki, ale nie wpływają na logikę programu. Zauważmy, że nie jest tu używane wywołanie; Każda udana funkcja kończy się wywołaniem interpret(true). Dostosowywanie interfejsu użytkownika Nie tylko silnik inferencji, ale również interfejs najwyższego poziomu Prologa może być spersonalizowany. W typowym najwyższym poziomie Prologa, komputer ustala:?a użytkownik odpowiada wpisując zapytanie. Niektóre Prologi pozwalają również użytkownikowi ustalić fakty i zasady, które są dodane do bazy danych. Plik TOPLEVEL.PL określa najwyższy poziom, którego dialogi z użytkownikiem wyglądają następująco: Type a query: father(X,cathy). Solution found: father(michael,cathy) Look for another? (Y/N): n Type a query: father(joe,cathy). No (more)solutions Type a query: parent(X,Y). Solution found: parent(michael,cathy) Look for another? (Y/N): y Solution found: parent(melody,cathy) Look for another? (Y/N): y No (more) solutions Dostosowywanie interfejsu użytkownika Jest to zwykły „najwyższy poziom” w Prologu z tym wyjątkiem, że komunikaty są znacznie bardziej jawne, a odpowiedzi są podawane poprzez wyświetlanie zapytań z wypełnionymi wartościami zmiennych. Wszystkie zapytania Prologa są dopuszczalne -nie tylko zapytania o bazę danych, ale również wywołania do wbudowanych predykatów takich jak „consult”. Kod określający ten „najwyższy poziom” ma tylko 18 linii. Procedura top_level jest nieskończoną pętlą repeat-fail, która akceptuje zapytania i przekazuje je do find_solutions. (Zauważ przy okazji, że nazwa top_level nie ma specjalnego znaczenia. Ta procedura staje się najwyższym poziomem środowiska Prologa zaraz po uruchomieniu go, wpisując '?-top_level.')W find_solutions procedura ma dwie klauzule. Pierwsza klauzula znajduje rozwiązanie, wypisuje je, i pyta użytkownika, czy szukać innych. Jeśli użytkownik wpisze N lub n (na "nie"), find_solutions wykonuje cięcie, i sterowanie powraca do top_level. W przeciwnym razie, find_solutions cofa się. Jeśli nie można znaleźć dalszych rozwiązań i cięcie nie zostało wykonane, sterowanie przechodzi do drugiej klauzuli i wyświetla się komunikat "Brak (dalszych) rozwiązań". Jak wyjść z top_level? Po prostu wpisz 'halt.' aby wyjść z Prologa, tak jakbyś używał zwykłego „najwyższego poziomu”. Spersonalizowany „najwyższy poziom” może uczynić Prolog znacznie bardziej przyjaznym dla użytkownika. Przydatnym okazało się przedstawienie Prologa początkującym programistom poprzez interfejs użytkownika kierowanym za pomocą menu, który pozwalał im wyświetlać bazę danych, dodawać lub usuwać klauzule, oraz wykonywać zapytania. Dzięki temu używanie edytora plików jest wyeliminowane. Ponadto spersonalizowany „najwyższy poziom” może być połączony z wzmocnionym silnikiem inferencji, po to aby zamienić Prologa w potężny system inżynierii danych. Wykorzystanie języka PROLOG w praktyce! Analiza semantyczna sceny wizyjnej (lub jakiegokolwiek innego zestawu bodźców), czyli wstępne przetwarzanie obrazu np. w C/C++ a później wnioskowanie na obrazie za pomocą bardziej ludzkich wyrażeń czyli np. "znajdź wszystkie niebieskie koła z dziurą w środku leżące blisko jasnych kształtów". "Odwracanie" działania złożonych algorytmów poprzez wykorzystanie tego, że prologowe predykaty są szeroko pojętymi relacjami a nie funkcjami a poza tym współczesny Prolog to także programowanie z ograniczeniami i propagowanie ograniczeń zamiast propagacji wartości, co może znacząco przyspieszyć niektóre zadania obliczeniowe. Wszędzie tam, gdzie mamy do czynienia ze zbiorami reguł (medycyna, prawo, biologia, matematyka, sieci semantyczne, systemy eksperckie, przetwarzanie języka naturalnego, semantic web, OWL/RDF) Prolog ma praktyczne zastosowanie i bywa używany Prolog bardzo przyjemnie realizuje się w dziedzinach związanych z programowaniem z ograniczeniami (zarówno w dziedzinie liczb całkowitych jak i rzeczywistych), co jest jego bardzo dużym plusem Prolog jest chętnie używany do prototypowania Rzeczywiście, jest używany także do tworzenia aplikacji webowych (np. SWI-Prolog zachęca do porzucenia PHP/Apache/MySQL na rzecz siebie samego!) Niżej podaję też trochę różnych współczesnych pakietów oprogramowania, które używają Prologu: DAVE™ – Free Development Platform for Code Generation Realtime Railway Station Control (Siemens AG R&D / Deutsche Bahn AG) Dopasowywanie kształtu okularów do twarzy w sklepach Paris Miki TextRazor InFlow - Software for Social Network Analysis & Organizational Network Analysis ContactExpress / DealBuilder GridMind / Mycroft Mind PROSYN (system ekspercki) Podobno też w Nokii N900 użyto prologu do zarządzania stanem GUI Dodatkowo: Dwa ciekawe projekty wykorzystujące Prologa: Watson - superkomputer, został zaimplementowany za pomocą Javy, C++ i Prologa. Clarissa - system sterowany głosem, który został użyty na Międzynarodowej Stacji Kosmicznej Prolog jest również wykorzystywany w aplikacjach webowych, często pomaga osiągnąć większą wydajność niż sam SQL. Na pewno też wiele systemów ekspertowych zostało napisanych w prologu.