A Nice programozási nyelv

Típusok, típuskonstrukciók

Java vs. Nice

A NICE programozási nyelv a JAVA-hoz hasonlóan erősen típusos nyelv, azaz minden kifejezés és változó típusa egyértelműen meghatározható már fordítási időben. A típusok meghatározzák, hogy az adott változó milyen értékeket vehet fel, illetve milyen kifejezéseket képezhet, milyen műveletek végezhetőek rajta. A NICE, a JAVA-hoz hasonlóan 2 fő típusosztályt különböztet meg, az úgynevezett primitív típusokat, illetve a referenciatípusokat, amelyek az összetett típusok (osztályok) objektumaira való hivatkozást segítik.

A NICE még további megszorításokat is tesz ezen típusokra, mégpedig aszerint, hogy az illető típus felveheti-e a null értéket, vagy sem. Az ilyen megkülönbözetett típusokat “opcionális típusnak” (optional type) hívjuk. Ezeket a nyelv szintaktikusan megkülönbözteti egymástól és a köztük való konverzió csak olyan módon lehetséges, hogy előbb meg kell vizsgálni hogy az aktuális változó értéke null-e vagy sem és ha nem az, akkor konvertálható olyan típussá, amely nem tartalmazhatja a null értéket. Ez a megkülönböztetés a legtöbb hibás (nem inicializált) mutatóhasználat esetén már fordításidejű hibát fog okozni, illetve rákényszeríti a programozót a helyes esetkezelésre.

Egy másik fontos különbség a JAVA típusrendszeréhez képest, hogy a NICE nem engedi meg az explicit típuskonverziókat az osztályokon (class cast), hanem az instanceof operátor segítségével elágaztathatjuk a programunkat, és azon ágon, ahol a fordítóprogram tudja, hogy lehetséges a konverzió, ott azt automatikusan engedélyezi. (Ennek a kezelési szerkezete hasonló a null változók konverziójához, mindkettőre lesz példa a későbbiekben.)

Mindkét fenti módszernél figyelembe kell venni azt is, hogy azok csak lokális változókra (lokális referencia) működnek, hiszen az osztályok tagjai esetén előfordulhat, hogy a vizsgált érték menetközben megváltozik (például egy többszálú program esetén egy másik szál megváltoztathatja az adott objektum értékét). Emiatt az osztály tagjain elvégzett ilyen konverzióhoz előbb egy lokális másolat készítése szükséges.

A NICE ezenkívül rendelkezik a típusparaméterezés lehetőségével is (hasonló módon a C++, illetve a JAVA5.0-hoz), azonban itt további megkötések is megadhatóak a típusparamétereket illetően, például hogy melyik ősosztályból származó objektumok fogadhatóak csak el, illetve megadható olyan reláció is a típusparaméterek között, mint pl. bármely U és T típus megfelel, ameddig U altípusa T-nek.

Ezeken kívül lehetőség van még un. absztrakt interfészek (abstract interface) létrehozására is. Ezek hasonlóak a normális interfacek (a JAVA interfészei, amelyek használhatóak NICE-ban is) működéséhez abban, hogy itt is megadhatóak olyan függvények, eljárások az interface tagjaként, amelyeket később egy osztály megvalósítva az interfészt kifejthet, azonban két fontos különbség is van. Az első az, hogy egy absztrakt interfész esetén lehetőség van arra, hogy azt egy adott már létező osztály valósítsa meg később, anélkül, hogy annak akár a forráskódja elérhető lenne (és módosítani kéne). A másik különbség, hogy egy absztrakt interfész nem egy tényleges típust definiál, sokkal inkább egy jelölést, amely a későbbiekben alkalmazható lesz más típusok kiterjesztéséhez. Ennek segítségével aztán készíthetünk pl. olyan függvényt, amely ezzel az absztakt interfésszel van típusparaméterezve és alkalmazhatóvá válik minden olyan típus esetén, amely megvalósítja az interfészben megadott funkciókat. Ennek segítségével pl. egyszerűen megvalósítható egy “loggolás” művelet, hiszen elegendő egy absztrakt interfészt megadni, egy log metódusra, és aztán ezt a metódust megvalósítani minden loggolható típusban (akár már létezőben is, később kiterjesztve vele az osztályt multi-metódusok használatával, nem szükséges annak eredeti kódját változtatni).

A továbbiakban rövid példák találhatóak az említett kiterjesztések használatára, illetve azok szintaxisára:

Elemi típusok konverziója

Az elemi numerikus típusok a csökkenő ábrázolási tartomány szerinti sorrendben: double, float, long, int, short, byte. A “kisebbtől” a “nagyobb” felé teljesen automatikus a konverzió. Az ellenkező irányban explicit konverzió szükséges, amelynek alakja type(e), ahol a type a céltípus neve, az e pedig a konvertálandó kifejezés. Ez ekvivalens a JAVA-ban használatos “(type) e” kifejezés használatával.

int a; short b; a = b; // ez rendben van, a konverzió automatikus b = short(a); // ebben az esetben az explicit konverziót kell használni

Opcionális típusok

A NICE referencia típusai alapértelmezetten nem tartalmazhatják a null értéket, ezért ha olyan típust szeretnénk megadni, amely felveheti a null értéket, azt a típusban külön jelölni kell: Pl. ha a ‘String’ típusnak szeretnénk megengedni a null értéket, akkor a deklarált változónk elejére a ‘?’ jelet kell illeszteni, azaz a null-al kiegészített típus neve a ‘?String’ lesz. Azokban az esetekben, amikor nem lehet tudni, hogy az eredeti típus megengedi-e a null-t (például egy típusparaméterezett esetben), használható a ‘!’ prefix is, amely tetszőleges típus azon verzióját jelenti, amely nem engedi meg a null-t. (Például ha a ’T’ típus a ’String’, akkor ’!T’ is a ’String’, ha ’T’ a ’?String’, akkor ’!T’ a ’String’.)

Példa opcionális típus használatára:

void foo(String arg) { ... } void bar( ?String arg ){ if( arg != null ){ foo(arg); // itt lehetséges a konverzió, arg nem null } foo(arg); // itt arg lehet null, ezért ez a sor fordításidejű hibát fog adni }

Class casting

Mivel a klasszikus típuskonverzió nem létezik NICE-ban osztályokon, ezért hasonló hatást csak fenti példához hasonlóan tudunk elérni. Például, ha A osztály és B gyermeke A-nak, akkor A-ból B-be való konverziót csak az alábbi módon érhetünk el:

void foo( B arg ); void bar( A arg ){ if( arg instanceof B ){ foo(arg); // itt tehát lehetséges a konverzió, így nincs hiba } foo(arg); // itt nem feltétlenül lehetséges a konverzió, fordításidejű hiba }

Absztrakt interfészek

Az absztrakt interfészek használatával már meglévő osztályainkat bővíthetjük új, közös funkcionalitással. Ennek előnyeit legjobban az alábbi példán keresztül lehet szemléltetni, amely a naplózás műveletével bővíti ki a már meglévő osztályokat:

//A naplózás műveletét megvalósító absztrakt interfész abstract interface LogEntry { String toLogString(); int severity(); } //A hiba súlyosságát jelző konstans értékek definiálása let int DEBUG = 1; let int INFO = 2; let int ERROR = 3; // Parametrikus naplózás művelet, amely használható minden olyan // osztály objektumára, amelyik megvalósítja a LogEntry interfészt void log(E entry, int severity = -1) { if (severity < 0) severity = entry.severity(); } // A “String” osztály, mint DEBUG szintű naplószöveg jelenjen meg class java.lang.String implements LogEntry; toLogString(String s) = s; severity(String s) = DEBUG; // A kivételek (Throwable) egy veremtérképet és egy üzenetet // jelenítsenek meg a naplóban class nice.lang.Throwable implements LogEntry; toLogString(Throwable t) { let writer = new StringWriter(); let printWriter = new PrintWriter(writer); printWriter.println("ERROR: " + t.getClass().getName() + ": " + t.getMessage()); t.printStackTrace(printWriter); printWriter.close(); return writer.toString(); } severity(Throwable t) = ERROR; // a http kérelmek is kerüljenek INFO szinten naplózásra: class javax.servlet.http.HttpServletRequest implements LogEntry; toLogString(HttpServletRequest r) = "Request from: " + r.getRemoteHost(); severity(HttpServletRequest r) = INFO;

Rendezett n-esek (tuples)

A NICE nyelv lehetővé teszi az adatok rendezett csoportokba (n-esekbe) rendezését, amelyek segítségével bizonyos műveletek könnyebben és átláthatóbban írhatóak le, logikailag és szintaktikusan összefűzhetnek több adattípusból álló elemeket is, lehetővé téve egyfajta „anonim struktúrák” használatát például a paraméterátadásoknál, amikor egynél több paramétert szeretnénk visszaadni egy függvény visszatérési értékeként. Ezenkívül érdekességképp meg lehet említeni, hogy ez a struktúra lehetővé teszi pl. két változó értékének felcserélését is, ideiglenes változó bevezetése nélkül.

Változók értékének felcserélése:

int x,y; (x,y) = (y,x);

Egy függvény visszatérési értéke is lehet rendezett n-es, így több visszatérési érték átadása sem lesz nehézkes.

// Egy pont koordinátáinak visszaadása n-esek segítségével: (int, int) getPointCoords( int no ){ return (x[no], y[no]); }

A tuple-k megadása sima zárójelek között, vesszővel elválasztott típusnevek felsorolásával történik, és inicializálásuk is ilyen módon érhető el.
Pl: (int, int) x = (3,4);
A paraméterben kapott tuple értéke is felbontható lokális változókra, ahogy a következő példában is látható. Ez a funkcionális nyelvek mintaillesztéséhez hasonlítható technika.

void f( (int, int) tup ) { ( int x, int y ) = tup; // ekkor x,y az eredeti pár elemeit fogja tartalmazni }