CLIPS. Języki programowania obiektowego

PL
Data dodania: 2011-09-17, Autor: doc. dr inż. Wiesław Porębski, Dodał: Karol, Wyświetleń: 1049

doc. dr inż. Wiesław Porębski Wydział Elektroniki, Telekomunikacji i Informatyki Katedra Algorytmów i Modelowania Systemów


Korzenie języka CLIPS sięgają roku 1984, gdy oddział Artificial Intelligence Section w NASA Johnson Space Center opracował jego prototyp. Oparta o język C wersja 1.0 była pomyślana jako narzędzie dla oprogramowania ówczesnych systemów ekspertowych (tj. programów przeznaczonych do modelowania ludzkiego doświadczenia i wiedzy). Początkowo język służył głównie do celów treningowych; jednak już po roku, gdy poprawiono przenośność programów, ich wydajność i funkcjonalność oraz uporządkowano dokumentację, udostępniono wersję 3.0 dla grup spoza NASA. Dalsze rozszerzenia i ulepszenia, w szczególności dodanie możliwości pisania programów proceduralnych i obiektowych, przekształciły CLIPS w wersjii 6.1 z narzędzia treningowego w pełnowartościowe narzędzie dla projektowania i oprogramowania systemów ekspertowych. Następujące cechy zdecydowały o sukcesie języka:

  • Możliwość reprezentowania wiedzy. CLIPS stanowi spójne narzędzie dla przetwarzania wiedzy, wspierając trzy różne paradygmaty: regułowy, obiektowy i proceduralny. Programowanie oparte o reguły pozwala reprezentować wiedzę jako heurystyki lub ogólne zasady (reguły) oparte na doświadczeniu, które podają zbiór działań, jakie należy wykonać w danej sytuacji. Programowanie obiektowe pozwala modelować złożone systemy jako modularne komponenty (które można łatwo użyć ponownie do modelowania innych systemów lub do tworzenia nowych komponentów). Możliwości programowania proceduralnego są podobne, jak w językach C, Pascal, Ada i LISP.
  • Przenośność. CLIPS, dla osiągnięcia przenośności i szybkości, jest napisany w C i został zainstalowany na wielu różnych komputerach bez zmian kodu (IBM PC, Macintosh, VAX11/780, Sun 3/260). Może być przenoszony na dowolny system posiadający kompilator języka ANSI C; przenośność zapewnia dostarczany z systemem CLIPS kod źródłowy, który może być modyfikowany stosownie do potrzeb użytkownika.
  • Integracja/Rozszerzalność. Kod w języku CLIPS można wbudować wewnątrz kodu proceduralnego, wywoływanego jako podprogram, i zintegrowanego z takimi językami, jak C, C++, FORTRAN i ADA. Istnieją również ściśle zdefiniowane zasady tworzenia rozszerzeń jezyka.
  • Interakcja z użytkownikiem. Standardowa wersja języka CLIPS zawiera interakcyjne środowisko opracowywania programu, obejmujące zintegrowany edytor, narzędzia dla uruchamiania oraz natychmiastową pomoc. Dla środowisk Macintosh, Windows 3.1 i X Window opracowano rozwijalne menu, zintegrowane edytory i system wielookienkowy.
  • Weryfikacja/Walidacja. CLIPS zawiera szereg cech dla wsparcia weryfikacji i walidacji systemów ekspertowych: modularne projektowanie i podział bazy wiedzy, statyczną i dynamiczną kontrolę atrybutów pól i argumentów funkcji oraz analizę semantyczną wzorcowych reguł dla ustalenia, czy niespójności mogłyby przeszkodzić w odpaleniu reguły lub wygenerować błąd.
  • Pełna dokumentacja. W pakiecie języka CLIPS znajdziemy obszerną dokumentację, w tym źródłowy podręcznik języka i przewodnik użytkownika.

Program napisany w języku CLIPS składa się z reguł, faktów i obiektów. W terminologii języka słowem shell (powłoka) określa się tę jego część, która zawiera podstawowe elementy systemu ekspertowego:

  • Wykaz faktów (fact-list) i wykaz obiektów (instance-list) w pamięci roboczej.
  • Baza wiedzy (knowledge-base) w postaci zbioru reguł oraz agenda reguł.
  • Maszyna wnioskująca (inference-engine).

Maszyna wnioskująca decyduje, które reguły powinny być wykonane i kiedy. Regułowy system ekspertowy zakodowany w języku CLIPS oparty jest o koncepcję wnioskowania wstępującego (forward chaining, data-driven inference). Oznacza to, że zadaje się pewne dane, które po dopasowaniu do przesłanek pozwalają na sformułowanie wniosków. Można zatem powiedzieć, że system ekspertowy napisany w języku CLIPS jest programem sterowanym danymi, ponieważ fakty i obiekty są tymi danymi, które stymulują wykonanie poprzez maszynę wnioskującą. Zauważmy, że w językach proceduralnych (np. C, BASIC, FORTRAN, Pascal) oraz hybrydowych (np. C++) wystarczy mieć sekwencję instrukcji, które mogą być wykonane bez dostarczenia im jakichkolwiek danych.

Konstrukcje regułowe

Składnia języka CLIPS została w dużym stopniu zapożyczona z języka LISP, w którym poszczególne frazy jednostek syntaktycznych ujmuje się w pary nawiasów okrągłych. W konwencji regułowej definicja reguły ma postać:

(defrule nazwa-reguły "opcjonalny komentarz"
(wzorzec-1)
(wzorzec-2)
 
(wzorzec-N)
=>
(działanie-1)
(działanie-2)
 
(działanie-M)
)

Reguła jest podobna do powszechnie spotykanej w językach proceduralnych instrukcji warunkowej IF-THEN, przy czym strzałka "=>" pełni rolę symbolu implikacji; tak więc wzorce przed strzałką można traktować jako warunki, których spełnienie spowoduje wykonanie działań wymienionych po strzałce. Jednak w odróżnieniu od języków proceduralnych, w których warunki są wartościowane po dojściu sterowania do instrukcji IF-THEN, w języku CLIPS reguły działają podobnie do hipotetycznej instrukcji WHENEVER-THEN. Maszyna wnioskująca ocenia, które reguły mają spełnione warunki i te właśnie reguły są wykonywane. Kod źródłowy elementarnego programu, który drukuje tekst "Hello, World!", a w następnym wierszu tekst "Hi, I am here! ", w konwencji regułowej mógłby mieć postać:

(defrule R1 "Rule R1"
(declare (salience 300))
 (initial-fact)
 (fact1)
=>
 (printout t "Hello World! " crlf))

(defrule R2 "Rule R2"
(declare (salience 200))
 (fact2 pole2)
=>
 (printout t "Hi, I am here! " crlf))

Kod źródłowy składa się z dwóch reguł. Kod stanie się programem, jeżeli na liście faktów znajdą się wszystkie dane wymagane dla dopasowania do przesłanek; wtedy reguły zostaną umieszczone na tzw. agendzie. Agenda jest listą reguł o spełnionych warunkach, ale jeszcze nie wykonanych, działającą podobnie do stosu (reguła na wierzchołku stosu jest pierwszą w kolejce do wykonania). W naszym programie reguła R1 zostanie uaktywniona i wstawiona na agendę po spełnieniu zapisanych w jej definicji warunków, tj. wprowadzeniu faktów (initial-fact) i (fact1). Spełnienie tych dwóch warunków oznacza ich koniunkcję.

Uwaga. Faktem w języku CLIPS nazywa się pewną listę wartości, do której można się odwoływać pozycyjnie (są to tzw. fakty uporządkowane) lub przez nazwę (fakty nieuporządkowane). Fakty składają się z pól, które mogą zawierać wartości jednego z 8 tzw. typów prymitywnych (FLOAT, INTEGER, SYMBOL, STRING, EXTERNALADDRESS, FACTADDRESS, INSTANCENAME, INSTANCEADDRESS). Fakty uporządkowane zawierają pola nienazwane, których typ jest wyznaczany niejawnie przez wprowadzoną do pola wartość. Dla R2 warunkiem uaktywnienia i wstawienia na agendę jest wprowadzenie faktu (fact2 pole2); zauważmy, że wzorzec reguły R2 wymaga wprowadzenia faktu dwupolowego (pierwszym jest pole z wpisanym symbolem 'fact-2', a drugim pole z wpisanym symbolem 'pole2'; symbol jest sekwencją drukowalnych znaków ASCII). Działania w obu regułach są poleceniami wydruku na standardowe wyjście, co jest sygnalizowane literą 't' (terminal) po poleceniu 'printout'; sekwencja "crlf" oznacza przejście do nowego wiersza. Dla wymuszenia kolejności wykonania regułom nadano priorytety, nazywane tutaj 'salience'. Deklarowana wartość priorytetu powinna mieścić się w zakresie -10000 do 10000. Przy wielu regułach aktywnych jako pierwsza zostanie wykonana ta reguła, która ma najwyższy priorytet; jeżeli reguła nie ma zadeklarowanego priorytetu, otrzymuje domyślną wartość 'salience' równą zero. Reguły umieszczone na agendzie mogą być wykonane dopiero po wprowadzeniu faktów, które maszyna wnioskująca automatycznie dopasowuje do wzorców reguły. Fakty wprowadza się do pamięci roboczej na tzw. listę faktów poleceniem (assert), za wyjątkiem specjalnego faktu o nazwie initial-fact, który wprowadza się poleceniem (reset). Polecenie (reset) zeruje pamięć roboczą faktów, po czym wstawia (initial-fact) na listę faktów z identyfikatorem "f-0", gdzie "0" oznacza indeks faktu na liście. Gdyby w definicji reguły nie było żadnych warunków (wzorców), wówczas (initial-fact) zostanie użyty przez system jako warunek domyślny. Fakty usuwa się z pamięci roboczej poleceniem (retract). Zakładając, że pracujemy z wersją CLIPS 6.1 w trybie tekstowym, wymagane dla aktywizacji reguł R1 i R2 fakty możemy wprowadzić poleceniami z wiersza rozkazowego po symbolu zachęty CLIPS>:

CLIPS> (reset) ;wprowadzenie faktu (initial-fact)
    CLIPS> (assert (fact1))
    CLIPS> (assert (fact2 pole2))

Program uruchamiamy poleceniem (run); maszyna wnioskująca wykona najpierw działania przewidziane w regule R1, a następnie działania reguły R2. Po zakończeniu pracy możemy opuścić system CLIPS poleceniem (exit). W pokazanych wyżej wierszach rozkazowych tekst po średniku jest komentarzem nie podlegającym interpretacji.

Wzorce w regułach można uczynić bardziej giętkimi, wprowadzając do nich lokalne w obrębie danej reguły zmienne, których nazwy zaczynają się od znaku zapytania, jak w przykładzie poniżej:

;Po zaladowaniu R3, (run) lub (run 1)
    defrule R3 "R3"
    (fact-1 ?x); x - zmienna
    =>
      (printout t "The value of x is: " ?x crlf))

Do wzorca (fact-1 ?x) można dopasować każdy fakt, w którym wartość pierwszego pola jest symbolem 'fact-1', a wartość w drugim polu jest dowolna, np. (assert (fact-1 15.45)). Zwróćmy uwagę na komentarze w definicji reguły R1: tekst reguły możemy umieścić w pliku o domyślnym przedłużeniu nazwy '.CLP', np. 'R1.CLP', po czym załadować zawartość pliku do systemu CLIPS; polecenie (run) może mieć jako parametr liczbę reguł, po których program zostanie zatrzymany. Innym udogodnieniem jest możliwość definiowania faktów w plikach, a następnie ładowanie ich w miarę potrzeby. Np. dla reguły

(defrule R1
    ?dana <-(name ?fname ?age)
    =>
    (printout t  ?fname " is " ?age " years old" crlf)
    (retract ?dana))

możemy utworzyć konstrukcję deffacts o nazwie 'DF' i umieścić ją w tym samym lub w innym pliku:

(deffacts DF
        (name Kuba 30)
        (name Karol 13)
        (name Leon 77))

Po załadowaniu DF zdefiniowane w konstrukcji fakty można dodać do listy faktów poleceniem (reset). Zwróćmy przy okazji uwagę na operator '<-', który wiąże zmienną ?dana z adresem faktu zgodnego ze wzorcem (name ?fname ?age). Operację wiązania dodatkowej zmiennej z adresem faktu (-ów) wykonuje się zwykle po to, aby usuwać już niepotrzebne fakty w części operacyjnej reguły.

Wspomniane uprzednio fakty nieuporządkowane definiuje się w postaci konstrukcji deftemplate, podobnej do struktury rekordu w języku Pascal. Deftemplate jest listą nazwanych pól (slots), przy czym w definicji podaje się typy wartości danych, jakie można wpisać do danego pola. Przykładowy prosty program z konstrukcją deftemplate:

(deftemplate data
    (slot name
    (type STRING))
    (slot age
    (type NUMBER)))
    
    (defrule R1
    (data(name ?name) (age ?age))
    =>
    (printout t crlf ?name " is " ?age " years old" crlf))

Program uruchamia się, wpisując dane w odpowiednie pola szablonu 'data', np.:

(assert (data (name "John") (age 27)))
    (assert (data (name "Mary") (age 25)))

Konstrukcje proceduralne

CLIPS w zasadzie operuje na symbolicznych faktach; tym niemniej można w nim zapisywać konstrukcje, które służą do obliczeń matematycznych. Dla ilustracji możemy napisać regułę:

(defrule addition
       (numbers ?x ?y)
    =>
      (assert (answer (+ ?x ?y)))
      (bind ?answer (+ ?x ?y))
      (printout t "answer is " ?answer crlf)
    )

W regule addition występuje predefiniowana operacja dodawania ("+") zmiennych ?x i ?y w notacji przedrostkowej (w języku CLIPS wszystkie funkcje wywołuje się w notacji przedrostkowej). Jeżeli wprowadzimy fakt poleceniem (assert (numbers 3 4)), a następnie uruchomimy program poleceniem (run), to w części wykonawczej reguły zostanie wprowadzony fakt answer, w którym drugie pole będzie miało wartość wyrażenia (+ 3 4). Wartość ta zostaje związana bind) ze zmienną ?answer, której wartość (7) drukuje się poleceniem printout. Operacje, w których korzysta się z funkcji systemowych, np. "+", "*", nie wymagają definiowania specjalnych reguł. Przykładami mogą być polecenia:

(+ 3 4 5) ; wartość: 12
    (+ 3 (* 5 6) 2); wartość: 35
    (str-length "abcd"); wartość: 4

Oprócz wykorzystywania funkcji systemowych użytkownik może definiować swoje własne funkcje za pomocą następującej konstrukcji:

(deffunction nazwa-funkcji "opcjonalny komentarz"
    (?arg1 ?arg2 ... ?argM [$?argN]
    (działanie-1)
    (działanie-2)
        ...
    (działanie-K))

gdzie ?arg1 ?arg2 ... ?argM $?argN są argumentami funkcji, przy czym $?argN jest opcjonalnym argumentem o wielu polach. Tak zdefiniowana funkcja może albo zwracać pewną wartość, albo dawać pewien efekt uboczny (np. printout). Funkcja bezargumentowa drukująca napis "Hello, World!" na standardowym wyjściu może mieć postać:

(deffunction print-text ()
    (printout t "Hello, World!" crlf))

Funkcję wywołuje się z wiersza rozkazowego pisząc po prostu 'print-text'. W następnym przykładzie wywołanie funkcji print-arg z dowolnym argumentem spowoduje wydruk jej argumentu na standardowym wyjściu:

(deffunction print-arg (?a)
    (printout t ?a crlf))

Użytkownik ma także możliwość przeciążania nazw operatorów i funkcji systemowych za pomocą konstrukcji defgeneric. Konstrukcja taka składa się z zapowiedzi deklaracji oraz definicji jednej lub więcej tzw. metod, jak pokazano na poniższym przykładzie:

(defgeneric +)
    
    (defmethod +
       ((?a STRING)
        (?b STRING))
       (str-cat ?a ?b))

Tak zdefiniowaną funkcja "+" ma dwie metody: 1) niejawną, która jest funkcją systemową obsługującą dodawanie numeryczne i 2) jawną, zdefiniowaną wyżej dla konkatenacji łańcuchów. Tak więc funkcję "+" możemy wywoływać z argumentami typu STRING, np. (+"ab" "cd"), otrzymując wynik abcd, lub z argumentami numerycznymi, np. (+ 2 3), otrzymując wynik 5.

Konstrukcje obiektowe

Integralną częścią środowiska języka jest CLIPS Object Oriented Language (COOL). COOL wspiera pięć podstawowych cech języka obiektowego: abstrakcję, hermetyzację, dziedziczenie, polimorfizm i wiązanie dynamiczne. W jego konstrukcjach znajdujemy elementy kilku języków obiektowych: np. koncepcja hermetyzacji jest podobna jak w języku Smalltalk, zaś koncepcja dziedziczenia mnogiego została zapożyczona z języka CLOS (Common Lisp Object System). W COOL mamy 17 predefiniowanych klas systemowych: OBJECT, USER, INITIALOBJECT, PRIMITIVE, NUMBER, INTEGER, FLOAT, INSTANCE, INSTANCENAME, INSTANCEADDRESS, ADDRESS, FACTADDRESS, EXTERNALADDRESS, MULTIFIELD, LEXEME, SYMBOL i STRING. Wszystkie klasy (także klasy definiowane przez użytkownika) dziedziczą od predefiniowanej klasy OBJECT, jak pokazano na rysunku 6-1.

Rys. 6-1. Predefiniowane klasy języka CLIPS

Pokazane na rysunku klasy, za wyjątkiem klasy INITIAL OBJECT są klasami abstrakcyjnymi. Przy definiowaniu klas zaleca się, aby klasy użytkownika dziedziczyły (bezpośrednio lub pośrednio) od klasy abstrakcyjnej USER; wówczas CLIPS automatycznie wyposaży klasę użytkownika w operacje inicjowania, drukowania i usuwania obiektów. Użytkownik może definiować klasy z deskryptorem (role concrete) dla klas, które mają mieć wystąpienia, albo z deskryptorem (role abstract) dla klas abstrakcyjnych. Jeżeli wystąpienia klas (obiekty) mają być użyte w przesłankach reguł, w definicji klas należy umieścić deskryptor (pattern-match reactive); jeżeli nie, deskryptor ma postać (pattern-match non-reactive). Ponieważ klasy abstrakcyjne nie mogą mieć wystąpień, zatem nie mogą mieć deskryptora reactive. Jeżeli w definicji klasy nie umieszczono żadnego z powyższych deskryptorów, są one wyznaczane z hierarchii dziedziczenia. Najprostsza definicja klasy abstrakcyjnej ma postać:

(defclass nazwa-klasy (is-a superclasy_bezpośrednie)).

Odpowiednikiem tej definicji dla klasy konkretnej (takiej, która może mieć wystąpienia) będzie:

(defclass nazwa-klasy (is-a superclasy_bezpośrednie)
       (role concrete)
    )

CLIPS utrzymuje tzw. listę klas poprzedzających (class precedence list) dla każdej zdefiniowanej klasy. Sposób tworzenia takich list można prześledzić na kolejnych przykładach definicji klas abstrakcyjnych:

(defclass A (is-a USER))

Klasa A dziedziczy bezpośrednio od klasy USER. Lista klas poprzedzających dla A: A USER OBJECT.

(defclass B (is-a USER))

Klasa B dziedziczy bezpośrednio od klasy USER. Lista klas poprzedzających dla B: B USER OBJECT.

(defclass C (is-a A B))

Klasa C dziedziczy bezpośrednio od klas A i B. Lista klas poprzedzających dla C: C A B USER OBJECT. Zauważmy, że wyznaczony z hierarchii dziedziczenia deskryptor pattern-match ma wartość non-reactive, ponieważ wszystkie klasy z listy klas poprzedzających są abstrakcyjne.

Atrybuty obiektów są definiowane w klasie jako tzw. szczeliny nazwane (named slots), odpowiadające polom w innych językach obiektowych. W szczelinach mogą być przechowywane wartości typów prostych, takich jak np. INTEGER lub STRING. Szczegółowe własności szczelin mogą być opisywane przez tzw. fasety lub dyrektywy (facets). Lista dostępnych dyrektyw jest następująca: access, constraint-attributes, create-accessor, default, default-dynamic, override-message, pattern-match, propagation, source, storage, visibility. Zapis dyrektywy w definicji klasy jest poleceniem dla kompilatora lub środowiska wykonawczego. Dla różnych dyrektyw ma on postać:

  • Dla dyrektywy access, określającej prawo dostępu do szczeliny: (access read-write), lub (access read-only), lub (access initialize-only). Domyślnym trybem dostępu jest read-write.
  • Dla dyrektywy default, określającej statycznie wartość domyślną atrybutu: (default ?DERIVE), lub (default ?NONE), lub (default wyrażenie). Domyślną wartością jest ?DERIVE. Zapis (default ?DERIVE) oznacza, że wartością domyślną atrybutu będzie inicjalna wartość domyślna odpowiedniego typu, np. łańcuch zerowy "" dla szczeliny typu STRING. Natomiast użycie słowa kluczowego ?NONE dla wartości domyślnej oznacza, że wartość odpowiedniego typu musi być przypisana jawnie do szczeliny, gdy wykonywane jest polecenie assert.
  • Dla dyrektywy default-dynamic, określającej dynamicznie (tj. za każdym razem gdy tworzony jest nowy obiekt) wartość domyślną atrybutu: (default wyrażenie).
  • Dla dyrektywy pattern-match zapis jest taki sam, jak dla klasy; wartością domyślną jest reactive.
  • Dla dyrektywy propagation: (propagation inherit), lub (propagation non-inherit). Jeżeli szczelina definiowanej klasy jest opisana dyrektywą propagation, której wartością jest inherit, to szczelinę tę dziedziczą obiekty wszystkich podklas danej klasy. Opcja non-inherit oznacza, że tylko wystąpienia bezpośrednie danej klasy dziedziczą tę szczelinę. Jest to więc sposób na blokowanie dziedziczenia pewnych cech superklasy w jej podklasach.
  • Dla dyrektywy storage: (storage local), lub (storage shared). Wartość local dotyczy wystąpienia (obiektu), a wartość shared dotyczy klasy. W pierwszym przypadku szczelina odpowiada znanej z innych języków obiektowych zmiennej wystąpienia, zaś w drugim zmiennej klasy.
  • Dla dyrektywy visibility: (visibility private), lub (visibility public). Dla wartości private dostęp do opisywanej przez dyrektywę szczeliny mają tylko funkcje składowe klasy definiowanej; dla public - funkcje składowe superklas oraz podklas dziedziczących tę szczelinę.
  • Dla dyrektywy source: (source exclusive), lub (source composite). Dyrektywa source jest pomocniczą dyrektywą przy wyznaczaniu zbioru szczelin danej klasy i ich faset. Dla wartości exclusive, jeżeli szczelina jest dziedziczona od więcej niż jednej klasy, pierwszeństwo ma dyrektywa szczeliny zdefiniowana w najniżej położonej superklasie danej klasy; dla wartości composite, jeżeli w wymienionej superklasie fasety nie są jawnie zdefiniowane, szczelina otrzymuje dyrektywę od superklasy następnego, wyższego poziomu hierarchii dziedziczenia.
  • Dla dyrektywy create-accessor: (create-accessor ?NONE), lub (create-accessor read), lub (create-accessor write), lub (create-accessor read-write). Dyrektywa create-accessor z wartościami read, write, lub read-write jest dyrektywą dla systemu CLIPS, aby automatycznie utworzył odpowiednie funkcje składowe dla operacji czytania/zapisu szczeliny. Domyślnie żadne takie funkcje nie są tworzone.

Pokazana niżej definicja klasy GREETING zawiera jedną szczelinę z domyślną wartością będącą łańcuchem znaków "Hello, World".

(defclass GREETING (is-a USER)
            (role concrete)
        (slot hello (default "Hello, World!"))
    )

W języku CLIPS wystąpienia klasy tworzy się poleceniem make-instance. Nazwy obiektów są ujmowane w nawiasy prostokątne dla odróżnienia ich od symboli. Dla klasy GREETING możemy utworzyć obiekt [G1] poleceniem:

(make-instance [G1] of GREETING)

Ponieważ klasa GREETING dziedziczy od klasy USER, możemy wysłać (za pomocą predefiniowanej funkcji send) do obiektu [G1] komunikat print, który drukuje nazwy i wartości atrybutów

(send [G1] print)

otrzymując wydruk:

[G1] of GREETING
    (hello "Hello, World!")

Standardowy wydruk otrzymany za pomocą predefiniowanej funkcji print jest jej efektem ubocznym. W większości przypadków taka postać informacji o zawartości szczelin obiektu jest raczej niezadowalająca. Dlatego w klasach definiowanych przez użytkownika z reguły definiuje się funkcje składowe, nazywane w języku CLIPS message-handlers. Nie używa się nazwy "metoda", jak w innych językach obiektowych, ponieważ termin "method" został użyty przy definiowaniu funkcji przeciążonych (konstrukcja defmethod). Używane w języku CLIPS funkcje message-handlers (w tym także predefiniowane) mogą oznaczać różne implementacje tego samego komunikatu w różnych klasach. Dla naszej przykładowej klasy GREETING możemy zdefiniować funkcję składową print-hellos()

(defmessage-handler GREETING print-hellos()
        (printout t ?self:hello crlf))

Jeżeli definicję klasy GREETING umieszczono np. w pliku greeting.clp, to powyższą definicję możemy dołączyć do definicji klasy, bądź umieścić ją w osobnym pliku. Występująca w definicji zmienna ?self jest zmienną specjalną, w której CLIPS przechowuje aktywne wystąpienie klasy, tj. takie wystąpienie (obiekt), do którego wysyła się komunikat. Słowo ?self jest słowem zarezerwowanym; zmienna ?self nie może wystąpić w argumencie funkcji, ani nie może być wiązana z inną wartością. Zwróćmy jeszcze uwagę na notację: sekwencja znaków ?self:hello jest odnośnikiem (referencją) do szczeliny o nazwie hello (termin referencja w języku CLIPS oznacza nazwę lub adres, poprzez które uzyskuje się dostęp do zmiennej). Po zdefiniowaniu funkcji print-hellos() tryb postępowania będzie analogiczny, jak dla predefiniowanej funkcji print. Najpierw tworzymy obiekt klasy GREETING poleceniem make-instance, a następnie wysyłamy komunikat print-hellos do obiektu [G1]:

(send [P1] print-hellos)

Otrzymany wydruk

Hello, World!

jest samą zawartością szczeliny hello bez jej nazwy i otaczających nawiasów.

Przedstawione wyżej przykłady były konstrukcjami czysto obiektowymi: definiowaliśmy klasy, tworzyliśmy ich wystąpienia i przesyłaliśmy do tych wystąpień komunikaty. Jednak w systemach ekspertowych muszą występować reguły. Jeżeli w przesłance reguły będą występować wzorce obiektów, to definicje klas tych obiektów muszą zawierać deskryptor reactive, jak pokazano w następnym przykładzie.

(defclass A (is-a USER) (role concrete) (pattern-match reactive)
      (slot foo (default 5.7))
      (slot bar (default 2.3)))

Dla klasy A możemy zdefiniować funkcję print-all-slots():

(defmessage-handler A print-all-slots ()
      (printout t ?self:foo " " ?self:bar crlf))

Załóżmy, że zadana jest reguła match-A:

(defrule match-A
      (object (is-a A))
    =>
     (send [a] print-all-slots)
    )

Dla spełnienia warunków reguły należy utworzyć obiekt [a]:

(make-instance [a] of A)

Wykonanie reguły spowoduje wysłanie komunikatu print-all-slots do obiektu [a]. Ponieważ obiekt [a] zawiera implementację funkcji print-all-slots(), zatem "odpowie" na komunikat wydrukiem wartości domyślnych 5.7 i 2.3 ustawionych w szczelinach foo oraz bar.

Polimorfizm

We wszystkich przypadkach przesyłania komunikatów do obiektów korzystaliśmy z predefiniowanej funkcji send. Argumentami tej funkcji są w kolejności: obiekt, dla którego przeznaczony jest komunikat, a następnie nazwa komunikatu, po której mogą być podane (o ile to konieczne) argumenty przesyłane do funkcji składowej message-handler. Wartość zwracana przez funkcję send jest rezultatem komunikatu, tj. ostatniego wyrażenia, wartościowanego w ciele funkcji składowej. W funkcjach składowych korzystaliśmy z bezpośredniego dostępu do szczelin za pomocą zmiennej ?self. Słabością takiej konstrukcji jest fakt, że bezpośrednia referencja do szczeliny jest wiązana statycznie z odpowiednią szczeliną podczas ładowania programu; w rezultacie funkcja składowa, korzystająca z referencji o postaci ?self:nazwa-szczeliny jest stosowalna do szczeliny w klasie, dla której została zdefiniowana oraz tylko tych jej podklas, w których nie zredefiniowano tej szczeliny. Dla uniknięcia tego ograniczenia i uzyskania polimorfizmu można wykorzystać funkcje systemowe dynamic-get i dynamic-put; wówczas referencja do obiektu w wywołaniu funkcji send będzie wiązana z konkretnym obiektem (i odpowiednią szczeliną) dopiero w fazie wykonania programu. Przy takiej konstrukcji funkcji składowej będzie ona stosowalna zarówno do superklas, jak i podklas danej klasy. Ilustruje to poniższy przykład, w którym taką funkcją składową jest put-age klasy PERSON.

(defclass PERSON (is-a USER) (role abstract)
      (slot sex (access read-only)(storage shared))
      (slot age (type NUMBER)(visibility public)))
    
    (defmessage-handler PERSON put-age (?value)
      (dynamic-put age ?value))
    
    (defclass FEMALE (is-a PERSON)(role abstract)
      (slot sex (source composite)(default female)))
    
    (defclass MALE (is-a PERSON)(role abstract)
      (slot sex (source composite)(default male)))
    
    (defclass WOMAN (is-a FEMALE)(role concrete)
      (slot age (source composite)(default 21)
                (range 18.0 100.0)))
    
    (defclass MAN (is-a MALE)(role concrete)
      (slot age (source composite)(default 25)
                (range 18.0 100.0)))
    
    (defmessage-handler MAN put-age primary (?value)
    (printout t "** Starting to print **" crlf
    "Age of man= "  (bind ?self:age ?value) crlf
    "** Finished printing **" crlf))
    
    (defclass BOY (is-a MAN)(role concrete))
    
    (definstances PEOPLE
      (Man-1 of MAN (age 60))
      (Man-2 of MAN)
      (Woman-1 of WOMAN (age 18))
      (Woman-2 of WOMAN)
      (Boy-1 of BOY (age 10)))

Przykład demonstruje hierarchię klas, w której podano dwie definicje funkcji składowych, obie o nazwie put-age, a także konstrukcję definstances. W przykładzie szczelina age, zdefiniowana w klasie PERSON z dyrektywą (visibility public) ulegała zmianie w klasach WOMAN i MAN. Wskutek tego zredefiniowana w klasie MAN funkcja składowa put-age będzie stosowalna do klasy BOY (nie ma zmian w odziedziczonej od MAN szczelinie age), ale nie będzie stosowalna do superklas klasy MAN, ani też klas FEMALE i WOMAN. Natomiast put-age z klasy PERSON jest stosowalna do całej hierarchii zdefiniowanych wyżej klas, dzięki wywołaniu funkcji dynamic-put dla szczeliny age, którą zadeklarowano jako publiczną. Gdyby brakło dyrektywy (visibility public), funkcja put-age z klasy PERSON nie miałaby dostępu do szczeliny age w żadnej z podklas klasy PERSON. Konstrukcja definstances o nazwie PEOPLE jest analogiczna do deffacts; zdefiniowane w niej obiekty są ładowane do pamięci roboczej jednym poleceniem (reset), które ponadto tworzy i ładuje obiekt inicjalny klasy predefiniowanej INITIAL-OBJECT. Przy tworzeniu obiektów, które zmieniają wartości domyślne szczeliny age (klasy WOMAN i MAN), zostanie wywołana funkcja składowa put-age z klasy PERSON dla obiektu Woman-1, zaś funkcja put-age z klasy MAN będzie wywołana dla obiektów Man-1 oraz Boy-1. Powstanie zatem następująca lista obiektów:

[initial-object] of  INITIAL-OBJECT
    [Man-1]  of  MAN  (sex male)  (age 60)
    [Man-2]  of  MAN  (sex male)  (age 25)
    [Woman-1] of  WOMAN  (sex female)  (age 18)
    [Woman-2] of  WOMAN  (sex female)  (age 21)
    [Boy-1] of  BOY  (sex male)  (age 10)

Ewentualne zmiany wartości szczeliny age można uzyskać wysłaniem odpowiedniego komunikatu. Np. komunikat

(send [Boy-1] put-age 12)

zmieni wiek chłopca przez wywołanie funkcji składowej put-age z klasy MAN, zaś komunikat

(send [Woman-1] put-age 35)

zmieni wiek kobiety przez wywołanie funkcji składowej put-age z klasy PERSON.

Podsumowanie

CLIPS jest językiem hybrydowym, jako że łączy w sobie trzy paradydmaty: regułowy, funkcjonalny i obiektowy. Jest językiem specjalizowanym, przeznaczonym głównie do tworzenia systemów ekspertowych; w tym aspekcie cechy te stanowią o jego sile. Predefiniowane klasy dostarczają użytkownikowi szereg narzędzi dla operowania ich wystąpieniami, w tym także możliwością przesyłania do nich komunikatów i to zarówno dla typów prymitywnych (np. NUMBER, SYMBOL, STRING), jak i dla klas definiowanych przez użytkownika. Dostęp do obiektów klas definiowanych przez użytkownika odbywa się wyłącznie (jak w "czystych" językach obiektowych) za pomocą komunikatów. W mechanizmie dziedziczenia zwraca uwagę klarowna implementacja dziedziczenia mnogiego oraz szereg oryginalnych dyrektyw (faset) opisujących atrybuty. Warto w tym miejscu podkreślić użyteczność dyrektyw: propagation, pozwalającej blokować dziedziczenie opisywanego nią atrybutu; default, dzięki której wszystkie we wszystkich wystąpieniach danej klasy atrybuty mają te same wartości inicjalne; default-dynamic, która pozwala nadawać unikatową wartość domyślną każdemu tworzonemu obiektowi. Do słabości języka można zaliczyć mało czytelną składnię, która - przy braku specjalizowanego edytora kontrolującego liczbę nawiasów - wymaga od programisty bacznej uwagi. W aspekcie obiektowym za niedostatek należy uznać definiowanie klas i funkcji składowych w oddzielnych jednostkach syntaktycznych.

Dokumentacja, kody źródłowe i binarne języka CLIPS dostępne są bez opłat na wielu serwerach sieci Internet. Podstawowe adresy URL, pod którymi można znaleźć odpowiednie pliki, są następujące: http://www.ghgcorp.com/clips/ oraz http://www.ghg.net/clips/CLIPS.html; publicznie dostępne repozytorium napisanych w języku CLIPS systemów ekspertowych znajduje się pod adresem http://www.cs.cmu.edu/afs/cs/project/ai-repository/.

 


Aby dodawać komentarze musisz być zalogowany!


Kontakt

Jeśli chcesz się z nami skontaktować napisz na adres: info(at)binboy.org lub odwiedź nasz profil na Facebooku!

O Nas

Serwis binboy.org to kopalnia wiedzy dla wszystkich z branży IT, w szczególności dla programistów i webmasterów. To duży zbiór kursów programowania, tutoriali, darmowych ebooków, setki kodów źródłowych itp.

Bądź w kontakcie

Panel użytkownika

Zaloguj się do panelu użytkownika.
Nie masz konta? Zarejestruj się!
Zapomniałeś hasła?