A Factor programozási nyelv

Kivételkezelés

A factor kivételkezelésről általában

A Factor nyelvben - mint oly sok minden más is, a kivételkezelés is csupán egy külső modulként létező dolog. Az egész rendszer az állapotfolytatások fölé felépített struktúra, mely a szokásos és néhány esetleg érdekesnek mondható kivételkezelési mechanizmust ad a kezünkbe.

Egyszerű kivételkezelési lehetőségek

A Factor nyelvben tetszőleges objektumot eldobhatunk kivételként, tehát gyakorlatilag nagyon szabad kezet kapunk kivételek deklarációjához, ami nem biztos, hogy mindig egészséges, gondoljunk arra, hogy dobhatunk például egy olyan kivételt, mely csupán a 42-es fixnum típusú számot fogja kiváltani (ami ugye nem túl informatív dolog). Kivételt a throw szóval válthatunk ki, mely a stack tetején lévő elemet dobja el kivételként!

Példa: kivételek dobása

42 throw ! Ez dob egy kivetelt, meghozza a 42 szamot "hiba tortent" throw ! Ez dob egy kivetelt, meghozza a "hiba tortent stringet"

Az előbb említett problémát elkerülendő, a nyelvben lehetőségünk van egy külön, a kivételek deklarációjához használható eszközt alkalmaznunk az ERROR: szó segítségével. Az ERROR: szót egy azonosító kell hogy kövesse, ez lesz a kivétel neve. Ha így definiáljuk a kivételünket, akkor nem csak egy típust deklarálunk, mely eldobható lesz (a kivétel neve lesz ennek a neve is), hanem egy szó is deklarálódik számunkra, melyet leírva automatikusan kiváltódik a kivételünk. A nyelvi konvenció az, hogy ahol csak lehet, inkább ERROR:-t használjunk mindenféle egyéb objektumok eldobása helyett!

Példa: ERROR: használata

ERROR: mondtam-hogy-csak-nemnegativat-engedek ; ! kivetel deklaracio : csak-nemnegativat-engedek ( n -- n ) dup 0 < ! Ha negativ a szam [ mondtam-hogy-csak-pozitivat-engedek ] ! akkor kivaltjuk a kivetelt [ ] ! egyebkent hagyjuk futni a programot if ! if... ; 5 csak-nemnegativat-engedek . ! kiirja, hogy 5 -5 csak-nemnegativat-engedek . ! exception valtodik ki ! igy tehat szurokent hasznalhato szavakat irhatunk

Persze a fentiek mit sem érnének, ha nem lehetne a kivételeket valahol elkapni. Kivételek kezelésére alapvetően kétféle módunk van: recover és cleanup.

A "recover ( ..a try: ( ..a -- ..b ) recovery: ( ..a error -- ..b ) -- ..b )" szó a hagyományos try-catch szemantikának felel meg. Elkapja a kivételt ha az a try kódidézetben fellép és végrehajtja a recover-ként megadott kódidézetet, a kivétel pedig nem terjed tovább.

A "cleanup ( try cleanup-always cleanup-error -- )" szó szemantikája az előzőtől abban tér el, hogy a try kódidézetben történő kivétel fellépése esetén, először a cleanup-always, majd a cleanup-error kódidézetek futnak le és a kivétel továbbterjed a hívóhoz. Emellett a cleanup-always kódidézet, mint azt a neve is sugallja, akkor is le fog futni, ha a try kódidézet futása során nem lépett fel kivétel. Gyakorlatilag a cleanup-always kicsit a más nyelvekben megtalálható finally blokkra hajaz.

: 100diver ( divend -- result ) [ 100 swap / ] ! Megprobaljuk a 100-at elosztani divend-el [ ! ha nem sikerul drop ! akkor nem erdekel mi volt a kivetel (toroljuk) "Nullával nem tudok osztani!" print ! kiirjuk, hogy nem tudunk nullaval osztani ] ! feltetelezven, hogy ez volt a problema recover ! nem dobjuk tovabb a kivetelt ; : mar-nem-csak-nemnegativat-engedek ( n -- n ) [ csak-nemnegativat-engedek ] ! alkalmazzuk a fenti szot, ami hibat ad neha [ ! ha kivetel lepett fel ( <0 ) mondtam-hogy-csak-pozitivat-engedek? ! akkor megnezzuk, hogy a mi kivetelunk volt-e [ ] ! es ha igen, akkor ugy teszunk, mint a pap ! szokott: "Mintha mise tortent volna!" [ "Nem vart hiba!" print ] ! Egyebkent pedig valami csunyasag volt if ! Ezert azt irjuk ki, hogy nem vart hiba! ] recover ! itt se dobjuk tovabb a kivetelt ;

Szofisztikált kivételkezelési lehetőségek

A fent látható, akár szokványosnak is mondható megoldásokon túl lehetőségünk van újrakezdésekkel ellátott és folytatható kivételek használatára is.

Egy újraindítható kivétel, ha kiterjed a programon kívülre, a felhasználótól kérdezi meg, hogy mit tegyen, gyakorlatilag ráterheli a kivétel lekezelését. Természetesen azonban nem kérhetjük meg a felhasználót, hogy írja újra a programot, amíg az áll a megfelelő helyen, viszont alternatívákat biztosíthatunk neki, amikből választhat. A program a választott alternatívához rendelt kód futásával folytatódik. A folytatható kivételek az előbbitől csak abban különböznek, hogy itt nem adhatunk meg különféle alternatívákat. A felhasználó csupán annyit választhat, tényleg akarja-e a veszélyes műveletet elvégezni vagy sem!

Példák szofisztikált kivételekre:

: restart-test ( -- ) "Oops!" { { "One" 1 } { "Two" 2 } } throw-restarts "You restarted: " write . ; restart-test ! Kiirja egy ablakban, hogy Oops! es felajalnja a One, Two, illetve abort ! lehetosegeket a folytatashoz... ! One eseten egy fog kiirodni a . hatasara (mert a lefuto kod a veremre teszi) ! Two valasztas eseten pedig 2 fog kiirodni : nemnegativnal-rakerdezek-milegyen ( n -- n ) [ csak-nemnegativat-engedek ] ! probalkozunk az eredeti muvelettel [ ! ha hiba tortent dup mondtam-hogy-csak-pozitivat-engedek? ! megnezzuk a negativsag miatt van-e [ ! ha igen, akkor drop "Nem-negativ szamot adtal meg, mi legyen?" ! megkerdezzuk mi legyen { { "Igazad van, vedd az abszoluterteket!" 0 } ! egyik lehetoseg { "Hagyjad, tudom mit csinalok!" 1 } ! masik lehetoseg } ! + mindig felajanlodik az abort is! throw-restarts ! a felhasznalo eldonti mit szeretne.. 0 = ! ha az elsot valasztotta, [ abs ] ! akkor abszoluterteket szamitunk, [ ] ! egyebkent hagyjuk a szamot ahogy van. if ] [ "Nem vart hiba" print rethrow ] ! ha valami mas hiba volt az baj.. if ! ilyenkor a kivetelt tovabbitjuk is! ] recover ! a kivetelt egyebkent nem dobjuk tovabb ; : negativnal-warning ( n -- n ) ! egyszerubb pelda [ csak-nemnegativat-engedek ] ! ellenorzi van-e hiba [ ! ha nincs feltesszuk, hogy negativ drop ! volt a szam (hiba torlese drop-al) "Warning: Negative value may cause problems!" throw-continue ] ! majd rakerdezunk folytassuk-e recover ! a muveletet.. ;

Az itt felsoroltak több esetben még tovább kombinálhatóak (pl. rethrow-restart), de az úgy kapott szavak szemantikája világos kell legyen a fentiek alapján...

Állapotfolytatások (a háttérben működő mechanizmusok)

Említettük, hogy a kivételkezelés csupán egy különálló library és nem is alapvető nyelvi elem. A kivételkezelés fent látható struktúrái, alacsanyabb szintű elemekből építkeznek, úgynevezett állapotfolytatásokból (continuitations).

Az állapotfolytatások a kiszámítás jövőjét jelképezik, felhasználásukkal/hívásukkal olyan hatást érhetünk el, mintha az állapotfolytatás lementésének a pillanatában meglévő maradékprogramot kezdenénk el futtatni. Az állapotfolytatás lementésére a callcc0, míg a folytatására a continue szavak állnak a rendelkezésünkre. Bár ezeknek van további több variációja is kényelmi okokból (pl. callcc1 és continue-with), ezeket itt most különösebben nem részletezzük, mert a szemléletük analóg-jellegű az itt leírtakkal.

A "callcc0 ( quot -- )" szó, egy kódidézetet vár a verem tetejéről input paraméterként. Mielőtt azonban ezt az idézetet végrehajtaná, lementi a programunk teljes állapotát (ez többek között az adatverem, a hívási verem, a kivételverem és egyéb vermek teljes mentésével jár) és meghívja a kódidézetet úgy, hogy a veremtetőn egy bonyolult adatstruktúra (állapotfolytatás) lesz a sok lementett verem tartalmával. Ez a művelet úgy is felfgható, hogy lementettük a programunknak azt a jövőjét/maradékprogramját, mely akkor állna elő, ha a paraméterként megadott kódidézetet nem futtatnánk le - mint azt mondtuk, a adatverem állapota is mentésre kerül, ugyanúgy, mint minden más dolog, ami a program állapotáért felelős.

A paraméterként megadott kódidézetnek a stack effect-je a következő alakú kell legyen: ( continuation -- ). Ez azt jelenti, hogy a callcc0-tól paraméterként kapott kontinuációnak az eltűntetéséért ő felel. A legegyszerűbb esetben semmire se használja fel az állapotot (drop, vagy azonnali continue), a második legegyszerűbb esetben pedig mondjuk kiirja a pont operátorral. Ezek azonban nem a kifejezetten érdekes esetek, mert akkor lesz értelme az egésznek, ha valamilyen műveletet végzünk és bizonyos esetekben kiadunk egy continue parancsot, amivel bizonyos értelemben meg-nem-történtté tehetjük a kódidézetben foglaltakat (kivéve például az outputra történő írást!). A continue szó tehát pont egy continuitation-t fogyaszt el a stackről és csak annyit csinál, hogy visszaállítja a mentésben leírt állapotot. A mentett állapotban természetesen mint azt mondtuk nem a jelen futási ág jövője van tárolva, hanem a "mi lenne ha a kódidézetet nem kéne végrehajtani" állapot (ugyebár nyilván végtelen ciklushoz vezetne a másik megközelítés).

Néhány példa, mely az állapotfolytatások megértéséhez közelebb hozhatja az olvasót:

! Alap peldak: ! ------------ [ drop ] callcc0 ! nem csinal semmi kulonoset, a kod folytatodik ahogy kell [ continue ] callcc0 ! hatasban ugyanazt eri el, mint az elozo, de itt azert, ! mert a lementett allapotot rogton vissza is allitjuk [ 1 2 3 4 5 6 7 8 9 + / * * + - * ] call 2drop ! Szemetet termel a veremre, melyet a 2drop dob el [ 1 2 3 4 5 6 7 8 9 + / * * + - * rot continue ] callcc0 ! Eltunik a keletkezett szemet a veremrol, mert az allapot ! a korabban mentett alternativ jovore all, ! ahol a szemet nincs ott! [ . ] callcc0 ! Ez a pelda prettyprinteli a program teljes allapotat! ! Ket erdekes pelda: ! ------------------ 4 ! Vermen a 4-es szam [ ! (*) Itt kezdodik az alternativ valosag kodidezete... swap ! a callcc0 miatt itt a veremteton nem a 4, hanem a contuniation ! all, amit egyelore elrejtunk, hogy dolgozhassunk 1 + ! a 4 kerult tehat a veremre, amit eggyel novelunk dup . ! megduplazzuk a kapott erteket (5), majd kiirjuk swap drop ! aztan visszaforgatjuk a vermet es eldobjuk a korabban mentett ! masik alternativ jovot (**). Nem hasznaljuk fel most... ] callcc0 . ! (**) itt mentjuk le, mi lenne ha a kodidezetet ! nem kene vegrehajtani es elkezdjuk vegrehajtani (*) ! a hivas utani call a vermen hagyott 5 erteket irja ki ! A futasi eredmeny tehat: 5 5 4 ! 4 a veremre [ ! (*) alternativ valosag swap ! a veremteton levo continuation elrejtese a szamitasokhoz 1 + ! a vermen levo 4-es ertek novelese 5-re dup . ! 5 kiirasa (a vermen marad a dup miatt itt is) swap continue ! continuation kiasasa a verem aljarol es continue(***) ! a continue hatasara a mentett masik alternativaba(**) kerulunk ] callcc0 . ! amikor eloszor jarunk itt mentjuk az allapotot (**) es indul ! alternativ valosagnak a (*)-tol kezdodo kod ! de amikor (***) hatasara visszatoltjuk a mentest, akkor ! a pont szo mar a korabbi 4-es erteket fogja latni a vermen ! es azt fogja kiirni... ! A futasi eredmeny tehat: 5 4 ! Egy "ertelmes" pelda: ! --------------------- : continuitation-abs ( x -- absx ) [ ! (*) alternativ valosag kezdete swap ! elrejtjuk a kontinuaciot dup 0 < not ! megnezzuk, hogy a verem tetejen levo szam nemnegativ-e [ drop continue ] ! ha igen, akkor visszatoltjuk az alternativ mentest (**) [ -1 * swap drop ] ! ha nem, akkor (-1)-el megszorozzuk az elemet es eldobjuk if ! az allapotfolytatast (tehat maradunk a mostani valosagba) ] callcc0 ! (**) itt jon letre az alternativ valosag mentese ; -42 continuitation-abs . ! 42 az eredmeny 42 continuitation-abs . ! 42 az eredmeny ! Nem meglepo modon az abszolutertek fuggvenyt ! sikerult mar megint sokadszorra implementalni...

Látszólag az utóbbi információk fényében a kivételkezelés megvalósítása kifejezetten hatékonytalannak tűnhet factorban, de a valóságban az optimalizáló fordító miatt nem feltétlen pont így néznek ki a dolgok. A szemantika ez, de hogy mi történik ennél lejjebb, ha már kódot fordítunk az Slava Pestov asztala....