230 likes | 351 Views
Logtalk. Objektum orientált kiterjesztés Prologhoz Bánsághi Zoltán bz341@hszk.bme.hu. Logtalk. Történet Smalltalk + logical = Logtalk Szerző: Paulo Moura 1994-95-ben Bonyolult (logikai) programok OO támogatásához készült 1998-99: 2.x, aktuális verzió: 2.15.4
E N D
Logtalk Objektum orientált kiterjesztés Prologhoz Bánsághi Zoltán bz341@hszk.bme.hu
Logtalk Történet • Smalltalk + logical = Logtalk • Szerző: Paulo Moura 1994-95-ben • Bonyolult (logikai) programok OO támogatásához készült • 1998-99: 2.x, aktuális verzió: 2.15.4 • Nyílt rendszer: www.logtalk.org • Teljes egészében Prologban íródott: előfordító és runtime predikátumok segítségével működik Mit próbál a Logtalk? • Objektumok kezelése (egységbe zárás, öröklődés) • Prototípus alapú és osztály alapú objektumhierarchiák együttes támogatása • Eseményvezérelt programozás • Automatikus dokumentáció generálás • ISO Prolog kompatibilis • A modul szervezésnek (ISO) alternatívája az OO szemlélet
Objektumok :- object(list). :- public(length/2).% láthatóság/mód deklarációk :- mode(length(?list, ?integer), zero_or_more). % megoldások száma length(List, Length) :- length(List, 0, Length). % belülről elérhető length/3 length([], Length, Length). length([_| Tail], Acc, Length) :- % length/3 kívülről nem látszik! Acc2 is Acc + 1, length(Tail, Acc2, Length). :- end_object. • Objektumok: predikátumok összessége, ezek felelnek meg a metódusoknak, a tényállítások az attribútumok • Az objektumaink segítségével névterekre partícionálhatjuk a programunkat • Objektum azonosító: atom ill. struktúra (generikus objektumokhoz) • Láthatóság deklaráció: C++ szerű, így valósul meg az egységbe zárás • A láthatóságot kötelező deklarálni, különben csak belülről érhető el a predikátum • Mód deklaráció: ISO szerint, de CSAK dokumentálás céljára (egyelőre)
Objektumok alkalmazása(azaz objektumok predikátumainak hívása) • Logtalk rendszer betöltése: | ?- consult('gnu.config'), %gazda rendszer függő predikátum deklarációk consult('logtalk.pl'). %független rész (fordító, runtime) yes • Objektum fordítása, betöltése (.lgt kiterjesztésű forrásból): | ?- logtalk_load([list]). yes • Üzenetküldés: | ?- list::length([1, 2, 3], L). L = 3 yes • Prototípus szerű objektumok: nem kellett külön példányosítani, a definícióval létre is jött valamilyen példány • Obj::Pred alakú infix operátor küld üzenetet, ez felel meg a metódus meghívásának • Nincs delegáció külön (mint a SICStus Objects-ben), üzeneteket delegálni csak az öröklési fában lehet • Pontosabban: ha egy szülő objektum metódusát implicit módon egy gyermeké(jé)n keresztül érjük el, akkor automatikusan delegálódik a gyermektől az üzenet (a szülőhöz) • :: Pred prefix operátor üzenetet küld az aktuálisan futó objektumnak, ez azért lehet fontos, mert nem feltétlenül egyezik meg avval amelyik kódjában szerepel ez a hívás
Broadcast üzenetküldés • Lehetőség van egy üzenetet több objektumnak, illetve egy objektumnak több üzenetet is elküldeni • Ehhez a címzett illetve az üzenet helyére a megfelelő konjunktív vagy diszjunktív kifejezést kell írni: Obj::(Pred1, Pred2, ...)ekvivalense: Obj::Pred1, Obj::Pred2, ... Obj::(Pred1; Pred2; ...)ekvivalense: Obj::Pred1; Obj::Pred2; ... (Obj1, Obj2, ...)::Pred)ekvivalense: Obj1::Pred, Obj2::Pred, ... (Obj1; Obj2; ...)::Pred)ekvivalense: Obj1::Pred; Obj2::Pred; ...
Futási kontextus, üzenetküldés • A rendszer követi az aktuális futási környezetet (kontextust), ami egy objektum • Ez a kontextus azt mondja meg, hogy melyik objektum viszonylatában kell értelmezni a predikátum definíciókat • Nézzük hogy egy Obj::Pred alakú üzenetküldés során mi történik: • Láthatóság ellenőrzése: az aktuális futási kontextusból hívható-e az adott metódus? • Futási környezet megváltoztatása a címzett objektumra • Az üzenet predikátum nevének feloldása, azaz a kód megkeresése az öröklési fában kiindulva az aktuális kontextusból (kötött sorrendben, így fedik el egymást az azonos nevű metódusok) • Az üzenet kódjának végrehajtása (tehát ha nem az aktuális kontextusban találtuk meg a kódot, akkor sem változik meg a környezet, azaz delegálunk) • Visszatérés sikerrel vagy meghiúsulással, a kontextus visszaállítódik az adott üzenetküldés előttire • Ha a ::Pred operátort alkalmazzuk akkor az épp olyan mintha explicit az aktuális környezetnek küldtünk volna üzenetet • self(S) visszaadja a kontextust • Ha egy predikátum hívás elé nem rakunk operátort, akkor az adott forrásban található hívódik meg (a források objektumoknak, az objektumok pedig névtereknek felelnek meg)
square.lgt: :- object(square). :- public(side/1). :- public(area/1). :- public(side_def_diff/1). side(1). % default oldalhossz az örökléshez side_def_diff(A) :- % az aktuális oldal eltérése a defaulttól ::side(S1), side(S2), A is S1-S2. area(A) :- ::side(S), A is S*S. :- end_object. mysquare.lgt: :- object(mysquare, extends(square)). % beillesztés az öröklési fába side(2). % elfedi az alapértelmezettet :- end_object. Futtatás: | ?- mysquare::side_def_diff(D). D = 1 yes Látható a különbség side és ::side hívások között: az előbbi a „bevasalt” értéket jelenti, míg az utóbbi a megfelelő leszármazott objektum predikátumát, hiszen ha kód örökítés útján került a végrehajtás side_def_diff-re, a kontextus akkor is a megfelelő gyerek marad Prototípusos öröklődés
Öröklés extrák • Többszörös öröklődésre van lehetőség, de névütközés esetén nem garantál semmit rendszer (tehát nem definiált a szülők prioritása, mint SICStus-ban) • Az öröklési fában mélységi módon keresünk, és az első megfelelő funktor nyer • Nincs beépített lehetőség az öröklés szűkítésére, azaz arra, hogy a szülő bizonyos metódusait ne örökölje a gyermek (de kézzel definiálhatunk meghiúsuló, vagy hibát dobó metódusokat, eltakarva az örökölt metódusokat) • Viszont van lehetőség láthatóság-korlátozott öröklésre, azaz az öröklött metódusok láthatóságát korlátozzuk :- object(Object, % ami Parent-ben public az itt protected lesz extends(protected::Parent)). ... :- end_object.
Teljesen rejtett predikátumok :- object(list). :- public(length/2). %:- private(length/3). length(List, Length) :- ::length(List, 0, Length). % így NEM érhető el length/3!! length([], Length, Length). length([_| Tail], Acc, Length) :- % length/3 kívülről nem látszik! Acc2 is Acc + 1, length(Tail, Acc2, Length). :- end_object. • Ha a fenti módon próbáljuk meg hívni length/3-at (azaz a Logtalk örökléses hivatkozásfeloldó rendszerén keresztül), akkor nem fog sikerülni, mert nem deklaráltuk, így nem regisztrálódott a rendszerbe a predikátum • Az ilyen predikátumokat csak közvetlenül a forrásból használhatjuk, mert láthatóságuk híján nem vesznek részt a névfeloldásban az üzenetküldéskor • Bármelyik piros jelet elhagyva működő kódot kapunk • Ha viszont alkalmaznánk a private deklarációt, de a hívást explicit self-nek szóló hívásként írnánk, akkor se lenne látható length/3 (biztos hatékonysági okokból)
Protokollok • A predikátum direktívákat (láthatóság, mód, stb…) külön forrásfájlba helyezhetjük, amiket az objektumok forrásai beillesztenek (importálnak), ezeket hívjuk protokolloknak • Protokollok is örökölhetnek egymástól (prototípusos szerű öröklés) • Egy objektum több protokollt is használhat • Hatékonyabb újrafelhasználás • Interfész-implementáció, definíció-deklaráció szétválasztás • Ugyan abban a névtérben helyezkednek el mint az objektumok, és külön fordítási egységet képeznek • Ebből következik, hogy az objektumok fordítását is befolyásoló predikátum direktívákat mindig ki kell írni, annak ellenére, hogy protokollokat használunk (ilyenek a dinamizmusra és metapredikátumokra vonatkozó direktívák) • Protokoll beillesztésnél is lehet korlátozni a láthatóságot
Protokollok példa :- protocol(listp). % protokoll definíció :- public(append/3). :- mode(append(?list, ?list, ?list), zero_or_more). :- public(length/2). :- mode(length(?list, ?integer), zero_or_more). :- public(member/2). :- mode(member(?term, ?list), zero_or_more). :- end_protocol. :- object(list, implements(listp)). % protokoll beillesztése ... :- end_object. :- object(difflist, % valamilyen másik lista implements(listp). ... :- end_object.
Kategóriák • Kód újrafelhasználás újabb eszköze: predikátumok egy halmazát nem kell feltétlenül egy objektumokba foglalni, létrehozhatunk kategóriát is • Egy kategóriát bármely objektum importálhat, és akkor elérheti a kategória predikátumait, mintha benne lennének definiálva • Fontos részüket képezik a predikátum direktívák, mert csak így lesz elérhető kívülről is a kód, de természetesen használhatnak protokollokat • Smalltalk-ban és Objective-C-ben van hasonló (állítólag) • Kategóriák használatával nagyobb szabadságot élvezhetünk az öröklési hierarchia megtervezésekor, mert azonos kódon osztozó objektumoknak nem feltétlenül kell egyazon öröklési fában lenniük • Közös névtérben vannak az objektumokkal, és önállóan fordítódnak • Kategória beillesztésnél is lehet korlátozni a láthatóságot
Kategóriák példa :- category(attributes, % kategória definíció implements(attributes_p)). % protokoll beillesztése :- dynamic(attribute_/2). % ezt ki kell írni set_attribute(Attribute, Value):- ::assertz(attribute_(Attribute, Value)). get_attribute(Attribute, Value):- ::attribute_(Attribute, Value). del_attributes(Attribute, Value):- ::retractall(attribute_(Attribute, Value)). :- end_category. :- object(person, imports(attributes)). % kategória importálása ... :- end_object. • Futtatás: | ?- person::set_attribute(name, paulo), person::set_attribute(gender, male). yes | ?- person::get_attribute(Attribute, Value). Attribute = name, Value = paulo ; Attribute = gender, Value = male ; no
Generikus objektumok • Objektum azonosító lehet struktúra is, aminek behelyettesítetlen változóival paraméterezhetjük az objektumot minden alkalommal amikor üzenetet küldünk neki • Ennek segítségével készíthetünk általános algoritmusokat, vagy a paramétereket tekinthetjük valami féle attribútumoknak • A paraméterekre nem tudunk a nevük alapján hivatkozni, csak a sorrendjük alapján a következő beépített predikátumokkal: • parameter(+N,?Val) segítségével adott sorszámú paraméter értékét illesztjük, de N nem lehet változó (behelyettesített se!!) • this(?Struct) használatával Struct illesztődik az objektum azonosítójára (amiben benne vannak a paraméterek is) • Az objektumokat a funktor/aritás azonosítja, tehát azonos paraméterszámú, és azonos nevű, de különböző objektumokat nem definiálhatunk (öröklődéssel se) • Üzenetküldéskor a címzett illesztődik a megfelelő funktorú objektumra, és ha ez nem sikeres akkor hibát kapunk • Ilyen módon az üzenetküldés klózában behelyettesítetlen változók is értéket kaphatnak
Parameter/2 (ellen)példa :- object(hanyadik(_,’bla’,_)). % paramétereket felesleges elnevezni :- public(param/2). % parameter-t nem lehet kívülről hívni %param(N,V) :- parameter(N,V). % megpróbáljuk „kivezetni”, de nem megy param(N,V) :- this(S), arg(N,S,V). % ez a megoldás %:- public(parameter/2). % deklarálni még lehet %parameter(N,V) :- param(N,V). % de a beépített eljárást elfedni már nem %:- public(param1/1). %egy(1). %param1(V) :- egy(N),parameter(N,V). % már ez se megy: nem fordul le :- end_object. • Futtatás: | ?- hanyadik(11,12,13)::param(2,A).% masodik argumentum „bla” kéne legyen uncaught exception: error(existence_error(object,hanyadik(11,12,13)), hanyadik(11,12,13)::param(2,_32),user) | ?- hanyadik(11,B,13)::param(3,A). A = 13 B = bla yes
Generikus objektumok(öröklődéssel) :- object(osszeg(_,_)). % paramétereket fölösleges elnevezni :- public(ertek/1). %ertek(E) :- this(osszeg(A,B)), E = A+B.% nem jó, mert leköti a funktort ertek(E) :- parameter(1,A),parameter(2,B), E = A+B. % általánosan jó :- end_object. :- object(osszeg(A,B,_), % itt már el kell nevezni a paramétereket!! extends(osszeg(A,B))). % pontosan milyen paraméterű szülője van ertek(E) :- ^^ertek(E1), parameter(3,Q), E = E1*Q. :- public(nemnorm_ertek/1). nemnorm_ertek(E) :- ^^ertek(E). :- end_object. • ^^Pred operátor: mint a ::Pred operátor, de a névfeloldást a szülőknél kezdi, így lehetőség van felüldefiniáláskor az eredeti predikátumot is felhasználni • Ha nem sorszám alapján érjük el a paramétereket (hanem a név-struktúra alapján), akkor a leszármazottakra nem fog illeszkedni
Generikus objektumok(másik példa) :- object(ellipszis(_X,_Y,_R1,_R2), % itt nincs szerepe a neveknek extends(...)). ... % ellipszis osztály metódusai :- end_object. :- object(kor(X,Y,R), extends( ellipszis(X,Y,R,R) ). % összekötjük a változókat % ide mást nem is kell írni, működni fog minden körre is! :- end_object. • Ez a módszer nem általánosítható: a szülő és a gyermek osztály paraméterei között nem valósíthatunk meg tetszőleges kapcsolatot • Ennek oka, hogy nem történik semmilyen predikátum hívás a szülőre való hivatkozáskor (akkor fordulunk a szülőhöz ha nincs definiálva egy metódus), hanem csak felépül(t) a név-struktúrája • Áthidalhatnánk ezt a problémát úgy, hogy a gyermek paramétereiből tetszőleges módon előállítjuk a szülőiét (logikailag összekapcsoljuk őket), és minden származtatandó metódus törzsében explicit módon továbbküldjük a hívást a (megfelelően felparaméterezett) szülőnek • Sajnos ilyenkor megváltozik a futtatási kontextus, ezért a gyermek által felüldefiniált predikátumokat elfedi a szülőé (explicit küldés helyett explicit delegációt kéne alkalmazni)
Eseményvezérelt programozás • Esemény: minden üzenetküldés (csak Obj::Pred alakú) generál két eseményt: mikor elküldődik az üzenet (before esemény) és mikor visszakerül a végrehajtás a küldőhöz (after esemény); ez az üzenet típusa • Egy esemény tehát evvel a négyessel írható le: (Event, Object, Message, Sender) azaz (Típus, Címzett, Üzenet, Küldő) • Monitor: az az objektum amelyik vár adott eseményre, és ha az bekövetkezik, akkor meghívódik a megfelelő metódusa • Az esemény kezelését a before(?Object, ?Message, ?Sender) ill. az after(?Object, ?Message, ?Sender) predikátumok végzik, az esemény típusától függően • Bármely objektum lehet monitor, ha implementálja a fenti metódusok legalább egyikét • Eseménykezelő regisztrációja: define_events(@Event, @Object, @Message, @Sender, +Monitor) alakú hívás után minden eseménynél ami illeszkedik az első négy argumentumra, meghívódik a Monitor objektum eseménykezelője, és átadódik a középső három argumentum • A before eseménykezelő meghiúsulása az egész üzenetküldést meghiúsítja, after eseménykezelő meghiúsulása visszalépést eredményez az üzenet végrehajtásában (az üzenet következő megoldása jön) • Így kényszereket vezethetünk be, szűrhetjük a megoldásokat, vagy csak nyomonkövethetjük a végrehajtást
Egy eseményvezérelt példa( main.lgt ) :- object(main). n(1). n(2). n(3).% több megoldást szolgáltató predikátum :- public(run/1). run(X) :- this(This), % eseményvezérlők törlése, majd beállítása abolish_events(_Event,_Object,_Message,_Sender,mod_monitor(_)), % külön monitor a before és after eseményeknek define_events(before, _, _, This, mod_monitor(2)), define_events( after, _, _, This, mod_monitor(3)), % innentől jön a „program” n(N1),% választási pont az üzenetküldés előtt % monitorozandó üzenetküldés: This::eredmeny(N1,X).% csak ::/2 üzenetküldés generál eseményeket :- public(eredmeny/2).% ha nem public lenne, nem hívhatnánk ::/2-vel eredmeny(Be, Ki) :-% maga az üzenet n(N2),% választási pont az üzenetben Ki is Be + N2 * 10. :- end_object.
Egy eseményvezérelt példa( mod_monitor1.lgt ) :- object(mod_monitor(_), % a paraméter határozza meg mit „enged át” implements(event_handlersp)). % before/after deklarációi % illesztünk az üzenetre, hogy a belsejébe lássunk % az üzenet nevét nem kéne bevasalni, csak az argumentumai sorszámát before(_, eredmeny(Be,_), Sender) :- parameter(1,M), 0 is Be mod M,!, writeq(Sender), write(' jot kuld: '), writeq(Be),nl. % ide csak akkor jutunk, meghiúsulás kell: before(_, eredmeny(Be,_), Sender) :- writeq(Sender),write(' rosszat kuld: '),writeq(Be),nl, fail. % az üzenet elküldésekor még szabad változó behelyettesített értékét % is el tudjuk kapni! after( _, eredmeny(_,Ki), _) :- parameter(1,M), 0 is Ki mod M,!. after( Object, eredmeny(_,Ki), _) :- writeq(Object),write(' rosszat ad vissza: '),writeq(Ki),nl, fail. :- end_object.
Egy eseményvezérelt példa( futtatás ) | ?- main::run(X). main rosszat kuld: 1% nem jut el a végrehajtás eredmény/2-be main jot kuld: 2% elkezd maga az üzenet predikátum is végrehajtódni main rosszat ad vissza: 12% belső választási pontok X = 22 ?% egyetlen megoldás ami átmegy a monitor szűrésén main rosszat ad vissza: 32 main rosszat kuld: 3 no • Küldő és fogadó objektum ugyan az, ezért az nem különül el • Monitorokat egyszerű létrehozni, ki-be kapcsolni, változtatni akár dinamikusan is • A logikai paradigmának köszönhetően kifinomult vizsgálódási feltételeket könnyedén fogalmazhatunk meg • Csak a public üzeneteket látjuk, mert csak az explicit küldés generál eseményt
Dinamizmus • Lehetőség van arra, hogy bármilyen Logtalk elemet dinamikusan, futási időben hozzunk létre • A szokásos predikátum-adatbázis kezelő eljárások (assert, retract) használhatóak az objektumok metódusainak manipulálására ha azokat dinamikusnak deklaráltuk • Létrehozhatunk futási időben objektumokat (protokollokat, kategóriákat is teljesen analóg módon) a create_object(Identifier, Relations, Directives, Clauses) beépített eljárás segítségével: create_object(foo, [extends(bar)], [public(foo/1)], [foo(1), foo(2)]). ekvivalens avval mintha a következő kódot töltöttük volna be: :- object(foo, extends(bar)). :- dynamic. %nyilván dinamikus lesz :- public(foo/1). foo(1). foo(2). :- end_object. • Fordítási időben létrehozott objektumot akkor van értelme dinamikusnak deklarálni, ha ki akarjuk törölni a futás során ( abolish_object(Object) )
Dinamikus lekérdezések • Futási időben le is kérdezhetjük a létezők különböző tulajdonságait • Ezek közül talán a legfontosabb az objektumhieararchiára vonatkozó kérdés: extends_object(?Object, ?Parent [, ?Scope]) alakú hívással például lekérdezhetjük egy objektum szüleit • Hasonlóan kategóriák, protokollok relációit is kérdezhetjük, de vigyázni kell, mert például a implements_protocol(?Object, ?Protocol [, ?Scope]) predikátum teljesülhet kategóriára és objektumra (az első argumentum helyén), hisz ezek mind implementálhatnak protokollokat • Az ilyen esetekben használhatjuk például a current_object(?Object) predikátumot, ami akkor teljesül, ha létezik az argumentum objektum • Egy objektum dinamizmusát az object_property(?Object, ?Property) eljárás segítségével tudhatjuk meg, a megoldás lehet: static, dynamic, vagy built-in • Egy objektum predikátumait mint megoldásokat felsorolja a következő hívás: current_predicate(?Functor/Arity) , de a láthatóságok figyelembe vételével (ha kívülről hívjuk, csak a public lesz látható) • Egy predikátum tulajdonságait így nyerhetjük ki: predicate_property(+Predicate, ?Property) ,ekkor információt kapunk a láthatóságról (public, protected, private), a dinamizmusról (mint az objektumnál), valamint arról, hogy hol definiáltatott illetve deklaráltatott a predikátum