języki przetwarzania symbolicznego
Transkrypt
języki przetwarzania symbolicznego
Agata Gawlik G1-ISI Warszawa dn. 16.05.2008 r. JĘZYKI PRZETWARZANIA SYMBOLICZNEGO Opracowanie tematów z PROLOGu UZGADNIANIE Uzgadnianie jest najważniejszą operacją wykonywaną na obiektach w języku PROLOG. Jest ono analogiczne do porównywania w językach proceduralnych, jednakże jest operacją o wiele potężniejszą oferującą szerokie spektrum możliwości. Prosty przykład uzgadniania służący wyszukiwaniu bezpośrednich połączeo autobusowych: Rozpatrzmy związek kurs (start, stop, odjazd). Zdefiniujmy fakty: kurs(warszawa, radom, 8.00). kurs(warszawa, kraków, 9.00). kurs(warszawa, kraków, 10.00). kurs(warszawa, radom, 11.00). kurs(warszawa, kielce, 12.00). Następnie definiujemy związek połączenie: połączenie( Sta, Sto, Godz) :kurs(Sta, Sto, Godz). Wyszukując połączenia z Warszawy do Radomia wprowadzamy cel: ?_połączenie(warszawa, radom, Godz). Przy tak sformułowanym celu następowad będzie uzgadnianie zmiennej Godz poprzez porównywanie celu z podanymi faktami – następuje przeszukanie przestrzeni rozwiązao. Aby podobny problem rozwiązad w dowolnym języku strukturalnym należałoby porównywad łaocuchy znakowe, co w przypadku rozpatrywanego problemu sprowadziłoby się do stworzenia odpowiedniej pętli warunkowej, przy czym konieczne byłoby wykrywanie zakooczenia wyszukiwania. Cały ten mechanizm jest wbudowany w interpreter języka PROLOG. Pokazany przykład prostego wnioskowania nie obrazuje w całości wszechstronności mechanizmu uzgadniania. Bardziej zaawansowane operacje można wykonywad w przypadku przetwarzania struktur danych przy pomocy procedur PROLOGowych. W szczególności dotyczy to list, które są wbudowaną w ten język strukturą. Operowanie na listach w językach deklaratywnych jest o tyle prostsze od podobnych działao w językach proceduralnych, że uwalnia programistę od konieczności zarządzania pamięcią, czy przechowywania wskaźników. Najprostszym przykładem przetwarzania list może byd wypisanie jej elementów. Odpowiedni predykat wygląda następująco: pisz_liste([]). pisz_liste([X|R) :print([X]), pisz_liste([R]). Ciekawszym przykładem może byd procedura dzieląca listę na dwie części: C1. C2. C3. dziel([ ], [ ], [ ]). dziel([X], [X], [ ]). dziel([X, Y | L], [X | L1], [Y | L2]) :dziel(L, L1, L2). W przykładzie tym następuje podział zadanej listy na dwie, gdzie jedna z nich zawiera elementy o indeksach parzystych a druga o nieparzystych. Załóżmy, że badaną listą jest : [1,2,3,4,5]. ?_ dziel([1,2,3,4,5], L1, L2). W przypadku C1 brak jest uzgodnienia ponieważ zadana lista jest niepusta. W przypadku C2 brak jest uzgodnienia ponieważ zadana lista ma więcej niż jeden element. Uzgodnienie z C3 daje następujące przypisanie zmiennych: X = 1, Y = 2, L = 3,4,5. Dodatkowo automatycznie do list wynikowych dołączane SA dwa pierwsze elementy listy zadanej. Następuje ponowne wywołanie procedury: dziel([3,4,5], L1, L2) Ponowne dopasowania dają taki sam rezultat – uzgodnienie z C3, zatem następuje przypisanie X = 3, Y = 4 oraz L = 5 oraz dołączenie elementów do list wynikowych. W trzecim kroku uzgodnienie następuje dla C2, ponieważ L jest listą jednoelementową. Daje to pusta funkcję celu i kooczy proces wnioskowania. Istotnym jest fakt, że nie jest już konieczny powrót, ponieważ kolejne etapy wnioskowania automatycznie zapisywały wynik. Przeanalizujmy procedurę zapisaną w pseudokodzie, wykonującą tę samą operację: dziel (char * L, char * L1, char * L2) int k=0; for(i; i < length(L); i++) { if(length(L) == 1) L1[0] = L.[0]; else { L1[k] = L[i]; L2[k] = L[i++]; k++; } } } { Jak widad już tak proste zadanie jak podział listy daje zgoła odmienne pod względem objętości rozwiązania. Reguła zapisana w PROLOGU ogranicza się do trzech linii, procedura zapisana w pseudokodzie rośnie do rozmiaru kilkunastu. Wynika to głównie z faktu nieprzystosowania języków proceduralnych do obsługi struktur listowych. Żonglowanie indeksami listy jest nie tylko uciążliwe, ale także może łatwo prowadzid do pomyłek. W szczególności różnica ta zauważalna może byd przy wszelkiego rodzaju sortowniach list. NAWROTY Mechanizm nawrotów został zastosowany w poprzednim przykładzie. Opisany teraz on zostanie dokładniej. W ogólności mechanizm nawrotów znajduje zastosowanie w przypadkach, kiedy cel złożony jest z kilku podcelów. W procesie wnioskowania dochodzi wtedy do prób uzgodnienia celu z kolejnymi podcelami. W przypadku braku uzgodnienia następuje nawrót i próba uzgodnienia z kolejnym podcelem. W celu zobrazowania mechanizmu nawrotów przytoczona zostanie reguła dzielenia całkowitego. Pomocniczo użyto reguły plus(X, Y, Z) która przypisuje do Z sumę X i Y. mod(X, Y, X) :X < Y. mod( X, Y, Z) :plus(X1, Y, X), mod(X1, Y, Z). Jak widad przy takim zapisie za każdym razem jako pierwsza podejmowana będzie próba uzgodnienia z podcelem pierwszym, sprawdzającym relację między zadanymi X i Y. Tak długo, jak X będzie miało większą wartośd uzgodnienie nie będzie możliwe, następowad będzie nawrót i próba uzgodnienia z podcelem drugim. Tu wartośd pierwszego argumentu wywołania dla procedury mod jest dekrementowana o wartośd Y. W ogólnym przypadku mechanizm podcelów w języku PROLOG odpowiada mechanizmowi pętli if then bądź switch case w językach proceduralnych. Sterowanie nawrotami możliwe jest dzięki operatorowi !, który wymusza nawrót. Poniżej przykład zastosowania wspomnianego operatora – procedura swap oraz bubblesort. swap([X, Y | R], [Y, X | R]) :X>Y, !. swap([X | R1], [X | R2]) :swap(R1, R2). bsort(L, Sorted) :swap(L, L1), !, bsort(L1, Sorted). bsort(Sorted, Sorted). % lista posortowana W ogólności – jeśli dwa przystające element listy są ułożone w kolejności malejącej należy zamienid je miejscami i powrócid do sortowania. W zaprezentowanym przypadku istnieje koniecznośd wymuszenia nawrotów ponieważ należy w każdym obiegu sprawdzid wszystkie elementy listy. Dla porównania przykładowa implementacja sortowania bąbelkowego: int BubbleSort(int num[], int numel) { int i, j, grade, moves = 0; for ( i = 0; i < (numel - 1); i++) { for(j = 1; j < numel; j++) { if (num[j] < num[j-1]) { grade = num[j]; num[j] = num[j-1]; num[j-1] = grade; moves++; } } } return moves; } Język proceduralny (tutaj C++) zmusza nie tylko do uciążliwego operowania na indeksach listy ale również do stosowania dużej liczby zmiennych pomocniczych, co nie jest konieczne w PROLOGu dzięki mechanizmowi uzgadniania. PROCEDURY NIEDETERMINISTYCZNE Obecnośd procedur niedeterministycznych w PROLOGu jest konsekwencją dopuszczalności wariantowości reguł. W proceduralnych językach programowania nie jest możliwa obecnośd więcej niż jednej procedury o takim samym nagłówku. Przykład procedury PROLOGowej będącej niedeterministyczną znajduje się poniżej: delete(_, [ ], [ ]). delete(A, [A | R], R). delete(A, [B | R], {B | W]) :delete (A, R, W). Tak przedstawiona procedura usuwa wystąpienia element A w zadanej liście. Pierwszy cel określony jest dla listy pustej, drugi dla przypadku, kiedy szykany element jest elementem pierwszym zadanej listy, a ostatni dla pozostałych przypadków. Niedeterminizm przedstawionej procedury polega na tym, że zawarte w podlecach warunki ni wykluczają się, zatem drzewo wnioskowania będzie posiadało rozgałęzienia. W takim przypadku możliwe jest otrzymanie wielu różnych wyników. Dla podanego przykładu w zależności od numeru wyniku będzie to lista pozbawiona iluś pierwszych wystąpieo elementu A. Kolejny przykład generuje nieskooczoną ilośd coraz dłuższych ciągów bitów: bit(0). bit(1). bits([]). bits([B | L]) :bits(L), bit(B). Dla zapytania ?- bits(X) generowane są ciągi bitów, począwszy od pustego. Warunkiem wypisania kolejnego wyniku przez interpreter jest użycie znaku „ ; ” każdorazowo po otrzymaniu wyniku. Procedury niedeterministyczne można zamienid na deterministyczne poprzez dodanie warunków wykluczających w podlecach bądź zastosowanie mechanizmu cut przedstawionego wyżej.