A LISP programozási nyelv

Objektumorientált programozás

Osztályok definiálása

Osztályokat a DEFCLASS függvénnyel lehet definiálni:

(DEFCLASS osztály-név (szülő-osztály-neve*) (slot-leírás*) osztály-opció*)

A szülő-osztály neve, slot-leírás és osztály opciók közül bármelyik elhagyható.

A slot az attribútumok és metódusok összefoglaló neve. A slot-leírások (slot-név slot-opció*) formájúak, ahol minden opció egy kulcsszó, melyet név, kifejezés vagy egyéb követ. A legfontosabb slot-opciók:

:ACCESSOR definiál egy függvényt ami írja és olvassa is a slot-ot
:INITFORM alapértelmezett érték megadása
:INITARG az osztály létrehozásakor a kezdeti érték felüldefiniálható
:READER definiál egy függvényt ami kiolvassa a slot-ot
:WRITER definiál egy függvényt ami írja a slot-ot
:TYPE Slot típusa adható meg vele
:DOCUMENTATION slot dokumentációja

A legfontosabb osztály-opciók:

:DEFAULT-INITARGS az ősosztályból örökölt initform érték felüldefiniálása
:DOCUMENTATION osztály dokumentációja

A DEFSTRUCT hasonló a DEFCLASS-hoz. A szintaxis kissé más, és a programozónak nagyobb kontrollja van a funkciók elnevezése felett. Példaként nézzük a DEFSTRUCT következő alkalmazását:

(DEFSTRUCT person (name 'bill) (age 10))

A DEFSTRUCT automatikusan definiál slotokat: kezdőértékeket számítókat, PERSON-NAME jellegűeket a slot-értékek beállításához és kiolvasásához, és egy MAKE-PERSON nevűt, mely az inicializást végzi a megadott argumentumokkal (initargs), például:

(make-person :name 'george :age 12)

Ehhez hasonló működés létrehozása DEFCLASS kulcsszóval:

(defclass person () ((name :accessor person-name :initform 'bill :initarg :name) ((age :accessor person-age :initform 10 :initarg :age)))

A DEFCLASS alkalmazásával a programozó határozhatja meg, mit hogy hívjanak. Például, nem kell a kiegészítőt PERSON-NAME -nek hívni, lehet egyszerűen csak NAME-nek.

A DESCRIBE OSZTÁLY függvény alkalmazásával információhoz juthatunk az osztály felépítéséről és további paramétereiről.

Nem kötelező az összes opció beállítása minden slot számára. Esetleg nem akarunk egy slotot inicializálni a MAKE-INSTANCE (l. később) hívásakor. Ebben az esetben, nem kell :INITARG-ot írni. Avagy lehet, hogy nincs értelme az alapértelmezett értéknek, mert például az értékeket mindig a leszármazott osztályoknál akarjuk beírni. Ekkor hagyjuk el az :INITFORM-ot.

A CLOS-ban az osztályok is objektumok, melyekre a létrehozás, megszüntetés, manipuláció stb. műveletei vannak definiálva.

A struktúráknak és az osztályoknak egyaránt vannak mezői és a mezőinek értékei. Osztályok esetében a mezőket azonban a Lisp terminológia szerint slotoknak nevezzük.

Ha egy struktúrát definiálunk a Lisp-ben, ahhoz több funkció automatikusan generálódik (pl. példánylétrehozó, típuslekérdező függvény stb.), osztályt alkalmazva azonban több dolgot kell a programozónak magának megadnia.

Az osztályok definiálása a következő minta szerint történik:

(defclass <osztály neve> <szülőosztályok listája> (<slot-név 1> :accessor <slot-hozzáférő eljárás 1> :initform <kezdőérték-számító form 1> :initarg <argumentum jelölő 1> . (<slot-név n> :accessor <slot-hozzáférő eljárás n> :initform <kezdőérték-számító form n> :initarg <argumentum jelölő n>)))

Példa:

(defclass article () ((title :accessor article-title :initarg :title) (author :accessor article-author :initarg :author)))

Legyen a computer-article az article osztály leszármazottja:

(defclass computer-article (article) ())

Fontos különbség az osztályok és a struktúrák között, hogy az osztályoknál a MAKE-INSTANCE kulcsszóval és az osztály nevének megadásával ehet új példányt létrehozni, és nem egy speciális nevű konstruktor alkalmazásával. A minta a következő:

(make-instance '<osztálynév>)

Osztályok metódusai vagy metódusok osztályai

A LISP és a CLOS funkcionális nyelv, tehát a matematikai függvényfogalomra épít. Ez nyomot hagy az objektumelvű részeken is. Az objektumelvű imperatív nyelvekkel szemben (pl. Smalltalk, C++, Java), a CLOS a multimetódus koncepciót követi. A metódusok nem egy konkrét osztályhoz tartoznak, hanem azokhoz, melyek hívását metódus argumentumai lehetővé teszik. Melyik hajtódik végre az azonos metódusok közül? Az, melynek argumentumai illeszthetők az "aktuális paraméterekre", és az illeszthetők közül a legspecifikusabb argumentumlistával rendelkezik. (Aciklikus osztályhierarchia esetén balról jobbra haladva egyértelmű metódus precedencia sorrendet kapunk, ciklikus esetben is van kiszámítási szabály, az már kissé bonyolultabb.)

Osztályok példányai

Osztály példánya a MAKE-INSTANCE kulcsszóval hozható létre. Ez hasonló a DEFSTRUCT-hoz tarozó MAKE-x függvényekhez, de a példányosítandó osztályt itt argumentumként lehet megadni.

(MAKE-INSTANCE class {initarg value}*)

Az osztály objektum alkalmazása helyett, a nevét is meg lehet adni. Például:

(make-instance 'person :age 100)

Ennek a person objektumnak 100 lesz az age "mezőjében", a neve BILL lesz, mert ez az alapértelmezett (az osztály definícióját l. fent)

Sokszor a MAKE-INSTANCE közvetlen meghívása helyett célszerű saját konstruktort definiálni, mert így elrejthetők az implementációs részletek és nem kell mindenre kulcsszó paramétert használni.

(defun make-person (name age) (make-instance 'person :name name :age age))

Így a név és a kor kulcsszóparaméterből pozícionális paraméterré változott.

A kiegészítő (accessor) függvények a slot-értékek olvasására és írására alkalmazhatók.

<cl> (setq p1 (make-instance 'person :name 'jill :age 100)) #<person @ #x7bf826> <cl> (person-name p1) jill <cl> (person-age p1) 100 <cl> (setf (person-age p1) 101) 101 <cl> (person-age p1) 101

DEFCLASS használata esetén, a példányokat #<.> formában írja ki az értelmező, nem #s(person :name jill :age 100) jelölésmódot használva. Ezt azonban meg lehet változtatni a PRINT-OBJECT általános függvény használatával.

A slotok a nevükön keresztül is elérhetők a (SLOT-VALUE példány slot-név) kifejezésen keresztül.

<cl> (slot-value p1 'name) jill <cl> (setf (slot-value p1 'name) 'jillian) jillian <cl> (person-name p1) jillian

Sok hasznos dolgot megtudhatunk egy példányról a DESCRIBE meghívásával:

<cl> (describe p1) #<person @ #x7bf826> is an instance of class #<clos:standard-class person @ #x7ad8ae>: The following slots have :INSTANCE allocation: age 101 name jillian

A slot-opciók öröklődése

Az eddigi osztályoknak nem volt szülőosztálya. Ezért volt "()" a "defclass person" után. Valójában ez azt jelenti, hogy a STANDARD-OBJECT nevű osztály a szülője.

Ahol van szülőosztály, a leszármazott definiálhat olyan slotot, amit már a szülő osztály definiált. Ekkor a két információt össze kell egyeztetni. A következő slot opcióknál vagy uniózás vagy felüldefiniálás történik:

:ACCESSOR definiál egy függvényt ami írja és olvassa is a slot-ot
:INITFORM alapértelemzett érték megadása
:INITARG az osztály létrehozásakor a kezdeti érték felüldefiniálható
:READER definiál egy függvényt ami kiolvassa a slot-ot
:WRITER definiál egy függvényt ami írja a slot-ot
:TYPE Slot típusa adható meg vele
:DOCUMENTATION slot dokumentációja

Végülis ezt várja az ember. A leszármazott meg tudja változtatni az alapértelmezett kezdőértéket az :INITFORM felüldefiniálásával, és inicializáló argumentumokat (initarg) és kiegészítőket (accessor) adhat hozzá. Mindazonáltal, a kiegészítők (accessor) uniója az általános függvények működésmódjának következménye. Ha egy általános függvény alkalmazható a C osztály példányára, akkor alkalmazható a C leszármazottjainak példányára is.

(A kiegészítő (accessor) függvények általános függvények.)

Példa az öröklésre:

<cl> (defclass teacher (person) ((subject :accessor teacher-subject :initarg :subject))) #<clos:standard-class teacher @ #x7cf796> #<cl> (defclass maths-teacher (teacher) ((subject :initform "Mathematics"))) #<clos:standard-class maths-teacher @ #x7d94be> <cl> (setq p2 (make-instance 'maths-teacher :name 'john :age 34)) #<maths-teacher @ #x7dcc66> <cl> (describe p2) #<maths-teacher @ #x7dcc66>; is an instance of class #<clos:standard-class maths-teacher @ #x7d94be>: The following slots have :INSTANCE allocation: age 34 name john subject "Mathematics"

A #<...> jelölésnek általában következő a formája:

#<az-objektum-osztálya ... további információk .>

Tehát a maths-teacher osztály példányai #<MATHS-TEACHER ...> formátumban íródnak ki. A fenti osztályok jelölése megmutatja, hogy a STANDARD-CLASS példányai. A DEFCLASS a standard osztályokat definiálja, a DEFSTRUCT a struktúra osztályokat.

Objektumorientált programozás generikus függvényekkel

A LISP és a CLOS funkcionális nyelv, tehát a matematikai függvényfogalomra épít. Ez nyomot hagy az objektumorientált részeken is. Az imperatív objektumorientált nyelvekkel szemben (pl. Smalltalk, C++, Java), a CLOS a generikus függvények módszerét alkalmazza. Ennek lényege, hogy a metódusok nem egy konkrét osztályhoz tartoznak, hanem azokhoz, melyek hívását a metódus argumentumai lehetővé teszik.

A generikus függvények elvéhez hasonló példákat a mindennapi életben is megfigyelhetünk, hiszen bármilyen tevékenység végre hajtása erősen függ attól, hogy milyen tárgyon hajtjuk végre:

Ugyanezt az elvet alkalmazhatjuk a programozás esetében is. Tegyük fel, hogy síkbeli alakzatok rajzolásához a következő struktúratípusokat definiáljuk:

(defstruct triangle (base 0) (altitude 0)) (defstruct rectangle (width 0) (height 0)) (defstruct circle (radius 0))

Ha ezekhez területszámító eljárást akarunk írni, azt első közelítésben megtehetjük a következő módon:

(defun area (figure) (cond ((triangle-p figure) ; a cond a case Lisp-beli megfelelője (* 1/2 ; a triangle-p figure az vizsgálja, hogy (triangle-base figure) ; a figure háromszög típusú-e. (triangle-altitude figure))) ((rectangle-p figure) (* (rectangle-width figure) (rectangle-height figure))) ((circle-p figure) (* pi (expt (circle-radius figure) 2)))))

Bár ez a megoldás tökéletesen működik, gondjaink lesznek vele, amint egyre több síkbeli alakzat területét akarjuk kiszámolni. Ha sok alakzatunk van, a feltételes elágazás több oldalon keresztül fog folytatódni, egyre áttekinthetetlenebb lesz, és így például az is előfordulhat, hogy valamit újra felveszünk az elágazásba, pedig azt már egy korábbi feltételnél lekezeltük. A többoldalas elágazást megpróbálhatjuk új eljárások definiálásával kivédeni:

(defun triangle-area (figure) (* 1/2 (triangle-base figure) (triangle.altitude figure))) (defun rectangle-area (figure) (* (rectangle-width figure) (rectangle-height figure))) (defun circle-area (figure) (* pi (expt (circle-radius figure) 2)))

De mivel ebben az esetben mindig emlékeznünk kéne az eljárások nevére, ezért célszerű újból definiálni az általános AREA függvényt:

(defun area (figure) (cond ((triangle.p figure) (triangle-area figure)) ((rectangle-p figure) (rectangle-area figure)) ((circle.p figure) (circle-area figure))))

Így az AREA függvény kezelhető méretű darabokra van osztva, és maga az area függvény meghívja az adott alakzathoz éppen aktuális területszámító függvényt.

Amire igazán szükségünk lenne, az egy olyan automatikus mechanizmus, mely meg tudná találni az alakzat típusának megfelelő AREA függvényt. Habár ezt meg lehet oldani manuálisan is — mint tettük ezt a fenti példában —, de egy automatikus megoldás jóval kényelmesebb lenne.

Szerencsére lehetőségünk van a definíciók kisebb darabokra bontására, melyek a nekik megfelelő paramétertípus esetén hajtódnak végre. A darabokat a CLOS-ban metódusoknak, az azonos nevű metódusok gyűjteményét pedig generikus függvénynek hívjuk.

A metódusokat az eljárásokhoz hasonló módon definiáljuk, két dolgot kivéve: először is a DEFMETHOD kulcsszót használjuk a DEFUN helyett, másodszor a paraméterlistában megadjuk, hogy a metódus milyen adattípusra vonatkozik:

(defmethod area ((figure triangle)) ; a háromszögekhez tartozó metódus (* 1/2 (triangle-base figure) (triangle-altitude figure))) (defmethod area ((figure rectangle)) ; a négyszögekhez tartozó metódus (* (rectangle-width figure) (rectangle-height figure))) (defmethod area ((figure circle)) ; a körökhöz tartozó metódus (* pi (excpt (circle-radius figure) 2)))

Megjegyezzük, hogy mindegyik AREA metódusnak ugyanaz a neve. A három metódus együttesen alkotja az AREA generikus függvényt.

Felhívjuk a figyelmet arra, hogy a hagyományos függvénydefinícióhoz (l. defun-os példa) képest megváltozott a paraméter: olyan kifejezést kell megadni, mely a paraméter nevén kívül azt is meghatározza, hogy az adott metódust milyen típus esetén lehet alkalmazni. A (figure triangle) a TRIANGLE adattípust használja, ezt paraméterspecializációnak nevezzük.

A programozásban két megközelítést különböztethetünk meg. Az egyik, az adatvezérelt perspektíva azon az elven alapul, hogy az argumentumok típusainak kombinációja határozza meg, hogy melyik eljárást hajtsuk végre. Az adatvezérelt perspektívából nézve az AREA metódusok egy családja, és az AREA argumentuma határozza meg, hogy ezek közül pontosan melyik metódust használjuk.

A másik megközelítés, az objektumorientált perspektíva arra épül, hogy az adattípusokat attribútumok és eljárások csomagjaként képzelhetjük el. Az objektumorientált perspektívából, a síkbeli alakzatok típusához (pl. TRIANGLE, RECTANGLE, és CIRCLE) tartoznak azok a metódusok, melyek összességükben az AREA és egyéb generikus függvényeket alkotják.

A területszámító AREA számára az objektumorientált perspektíva tűnik természetesebbnek, mert az AREA metódusoknak csak egy paramétere van. Több paraméter esetében gyakran az adatvezérelt perspektíva a gyümölcsözőbb megközelítési forma.

Megjegyezzük, hogy a generikus függvények és metódusok által lehetővé tett programozási formát is objektumorientált programozásnak szokták nevezni.

Többszörös öröklődés

A generikus függvények hívása esetében fontos tudnunk, hogy az azonos nevű metódusdefiníciók közül melyik fog végrehajtódni. Ez a generikus függvény meghívásakor alkalmazott aktuális paraméterek típusától függ. Az aktuális paraméterekre illeszkedő metódusok halmazát, az adott esetben használható metódusoknak nevezzük. Ezek közül az fog végrehajtódni, melyet a legspecifikusabbnak tekinthetünk. Ennek meghatározásához a paraméterek osztályainak öröklődési viszonyait kell figyelembe vennünk. Az a metódus fog kiválasztódni, melynek paraméterspecializációja a legkorábban fordul elő az ún. osztály-precedencialistában. Bár ezen lista előállítása egy bonyolultabb algoritmus szerint történik, két alapvető szabályt mindenképpen fontos ismernünk.

Egyszeres öröklődés esetében egyszerűen meghatározható az osztályok precedenciájának sorrendje: minden osztályt specifikusabbnak tekintünk a szülőjénél (1. szabály). Tehát a precedencialistát úgy állítjuk elő, hogy az öröklődési hierarchiában alulról fölfelé haladunk.

A többszörös öröklődésnél a precedencia lista előállítása már bonyolultabb kérdéssé válik. Tekintsük a következő osztálydefiníciót:

(defclass A (B C) .) ; tehát az A osztály a B-nek és C-nek egyaránt leszármazottja

Ekkor egyértelmű, hogy az A osztály specifikusabb a B-nél és a C-nél is, de mi van abban az esetben, ha valamilyen metódust vagy kezdőérték-számító formot a B és C osztály már egyaránt definiált? Melyik az érvényes? A CLOS-ban ekkor az a szabály, hogy az előbb felsorolt szülőosztály specifikusabb a később felsoroltnál (2. szabály). Tehát ekkor a precedencia listát úgy kapjuk meg, hogy az öröklődési gráfban mindig alulról fölfelé haladunk, és ha elágazáshoz jutunk, akkor a különböző ágakat balról-jobbra egymás után járjuk be.

A két előbb említett szabály azonban nem mindig elegendő az egyértelmű sorrend meghatározásához (kört tartalmazó öröklődési gráf esetén). A CLOS nyelv tervezői ezért definiáltak egy algoritmust, mely ciklikus öröklődés esetében is feloldja a kétértelműségeket. Ennek alapján minden CLOS-implementációnak ugyanazt a precedencia-sorrendet kell előállítani az ősosztályok között. Mivel az algoritmus nem triviális és bonyolultabb öröklődés esetén nehezen követhető, ezért a programozás során nem célszerű erre építeni. Ha fontos a szülőosztályok sorrendje, lehetőségünk van arra, hogy azt explicit módon fogalmazzuk meg az osztály definiálásakor.