A LISP programozási nyelv

Kivételek kezelése

A háromszereplős kivételkezelés

A hagyományos imperatív programozási nyelvekhez képest a Lisp tágabb lehetőségeket nyújt a kivételek kezelésére. Hagyományosan egy kivétel kezelésének két szereplője van: valaki a callstack tetején eldobja a kivételt, és egy másik kódrészlet, a callstackben lejjebb, elkapja azt. A módszer nyilvánvaló hátránya, hogy az elkapásnak elég közel kell lennie a kiváltás helyéhez ahhoz, hogy értelmesen kezelni tudjuk, ugyanakkor kellően magas szinten is kell állnia ahhoz, hogy jó döntést tudjon hozni a kezelés mikéntjéről.

Lispben ezzel szemben három szereplős a történet:

Például képzeljünk el egy programkönyvtárat, amellyel valamilyen adott formátumban lévő file-okat olvasunk be. Előfordulhat olyan eset, hogy a file nem a formátumának megfelelő: ekkor valahol a file-feldolgozóban keletkezik kivétel.

Ekkor feljebb, a file-beolvasás legfelső szintjén a helyzetnek egy nyilvánvaló kezelési módja, hogy helyette egy másik file-t nyitunk meg. Ehhez azonban valahonnan kell kerítenünk egy újabb file-nevet. Mármost lehet, hogy ezt a file-kezelő kódrészletet sok modulból hívjuk, és minden modulnak más lehet a stratégiája arra vonatkozóan, hogy mik az alternatív file-elérési utak. Egyes modulok pedig szeretnének ilyenkor inkább hibajelzéssel leállni, például mert a hiányzó file nem helyettesíthető. Ezért aztán innen egy olyan újrakezdési lehetőséget küldünk tovább, hogy egy file-név ismeretében megpróbálkozhatunk egy másik file beolvasásával.

Nyilvánvalóan a beolvasót használó modulok belsejében találjuk meg a döntéshozó kódot: az egyik hívó például ilyen esetben úgy dönt, hogy a FILE.IN file olvasási hibája esetén próbálkozzunk a FILE.IN.BAK file-lal, a másik pedig nem próbálkozik újrakezdésekkel, inkább kiír egy hibaüzenetet a felhasználónak.

Lássuk, milyen nyelvi elemekkel támogatja a Lisp az ilyen szerkezetű alkalmazások létrehozását!

Kivételek

Kivételeket a DEFINE-CONDITION makróval definiálhatunk. Egy kivétel valójában egy speciális osztály; ennek megfelelően a DEFINE-CONDITION szintaxisa is hasonló a DEFCLASS makróéhoz:

(DEFINE-CONDITION név (ősosztály*) (slot-leírás*) osztály-opciók)

Ha nem adunk meg ősosztályt, akkor a CONDITION lesz az, ez az általános nemlokális visszatérések ősosztályát jelenti. Ha kifejezetten hibajelenség jelzésére használjuk a kivételt, akkor az ERROR osztályból származtassunk.

(SIGNAL kivétel kivétel-paraméterek) (WARN kivétel kivétel-paraméterek) (ERROR kivétel kivétel-paraméterek)

A kivétel kiváltására a legáltalánosabb lehetőség a SIGNAL függvény. Ez a függvény NIL értekkel tér vissza, ha senki sem kapta el a kivételt, vagyis a futás folytatódik az eldobás helyét követően (vagyis ez nem igazán felel meg a hagyományos throw fogalomnak). Ehhez hasonló a WARN függvény is, amely alapesetben, vagyis ha nem kezelték le a kivételt, a kimenetre naplózza a hibát és folytatja a végrehajtást.

A hagyományos kivétel-dobásnak a ERROR függvény felel meg Lispben: ha nem kezeli senki a kivételt, akkor a futtatókörnyezettől függően vagy a debuggerbe jutunk, vagy egyszerűen leáll az alkalmazás.

A fentiek alapján már el tudjuk kezdeni a bevezetőben vázolt file-feldolgozó alkalmazás vázát:

(DEFINE-CONDITION file-format-error (ERROR) ((filename :initarg filename :reader format-error-filename) (error-message :initarg :error-message :reader format-error-message))) (DEFUN process-file (filename) ; Megnyitja a file-t, és soronként feldolgozza (WITH-OPEN-FILE (s filename) (LET ((result (LIST)) (line-no 1)) (FOR-EACH-LINE (line s) (SETF result (CONS (PROCESS-LINE filename line line-no) result)) (INCF line-no) result))) (DEFUN process-line (filename line line-no) ; Ha a sor helyes formátumú, akkor visszaad belőle egy objektumot (WHEN (MALFORMED-LINE-P line) (ERROR 'file-format-error :filename filename :error-message (FORMAT nil "Error in line ~D" line-no))) (OBJECT-FROM-LINE line))

Kivételkezelők

A kivételkezelők a program egy részét úgy hajtják végre, hogy ha azokban meghatározott típusú kivételek lépnek fel, akkor a helyzetet különböző újrakezdésekkel tudják kezelni. Ehhez a RESTART-CASE makrót használjuk:

(RESTART-CASE kifejezés (megoldás-1 (paraméterek-1) kifejezés-1) (megoldás-2 (paraméterek-2) kifejezés-2) ...)

Ez a makró megpróbálja kiszámítani kifejezés értékét, és ha közben nem történik kivétel, akkor ezzel az értékkel tér vissza. Ha viszont kivétel történik, akkor felajánlja a különböző megoldásokat, és ha ezek közül egy döntéshozó kód valamelyiket kiválasztja, akkor az annak megfelelő kifejezés értékével tér vissza.

Az eddigi példánkat tovább folytatva megváltoztathatjuk a PROCESS-FILE függvény definícióját: ha hiba lép fel a sorok feldolgozása során, akkor vagy üres listával térünk vissza, vagy megpróbálkozhatunk egy másik file-lal.

(DEFUN process-file (filename) (RESTART-CASE (WITH-OPEN-FILE (s filename) (LET ((result (LIST)) (line-no 1)) (FOR-EACH-LINE (line s) (SETF result (CONS (PROCESS-LINE line line-no) result)) (INCF line-no) result))) (return-empty-list () (LIST)) (retry-other-file (new-filename) (PROCESS-FILE new-filename))))

Döntéshozók

A felkínált újrakezdéseket a INVOKE-RESTART függvénnyel hívhatjuk meg. Az egyes kivétel-típusokhoz pedig a HANDLER-BIND makróval rendelhetünk kezelő-függvényeket.

(HANDLER-BIND ((kivétel-1 kezelő-1) (kivétel-2 kezelő-2) ...) kifejezés) (INVOKE-RESTART újrakezdés paraméterek)

A HANDLER-BIND makróban használt kezelőknek olyan formátumban kell lenniük, hogy paraméterként megkaphassák a tényleges kivétel-példányt. A legegyszerűbb erre a célra névtelen függvényeket használnunk, mint azt az alábbi példában is láthatjuk, amely a file-beolvasó kirakósjátékunk utolsó darabja:

(DEFUN my-process-file-1 (filename) (HANDLER-BIND ((file-format-error #'(LAMBDA (exception) (INVOKE-RESTART 'return-empty-list)))) (PROCESS-FILE filename))) (DEFUN my-process-file-2 (filename) (HANDLER-BIND ((file-format-error #'(LAMBDA (condition) (LET ((backup-filename (BACKUP-FILENAME (FORMAT-ERROR-FILENAME condition)))) (WHEN backup-filename (INVOKE-RESTART 'retry-other-file backup-filename)))))) (PROCESS-FILE filename)))

Látható, hogy az első függvény hiba esetén üres listával tér vissza, a második pedig, ha létezik biztonsági másolat, akkor megpróbálja azt beolvasni, egyébként pedig nem kezeli a hibát (vagyis hagyja leállni az alkalmazást).

Hagyományos, eldobás-elkapás jellegű kivételkezelés

Természetesen a fent leírt rendszert nem kötelező teljes egészében kihasználnunk Lispben sem: a HANDLER-CASE makró segítségével megvalósíthatjuk a hagyományos, kétszereplős kivételkezelést is. Ennek szintaxisa:

(HANDLER-CASE kifejezés (kivétel-1 (változó-1) kezelő-1) (kivétel-2 (változó-2) kezelő-2) ...)

Ennek a viselkedése megegyezik a hagyományos imperatív nyelvekben látott kivételkezeléssel: a HANDLER-BIND kiszámítja a kifejezés értékét, és ha közben nem történik kivétel, akkor visszaadja ezt az értéket; ha viszont kivétel történik, akkor a kiváltott kivétel típusa alapján megkeresi a megfelelő kezelőt, és azt futtatja le (a megfelelő változó értéke lesz a konkrét kivétel-példány). Például az alábbi kifejezés visszatérési értéke 42 lesz:

(HANDLER-CASE (/ 41 0) (DIVISION-BY-ZERO-ERROR (c) (1+ (CAR (ARITHMETIC-ERROR-OPERANDS c)))))