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.