Dr. Vermes Mátyás1
első változat: 2005. augusztus, utolsó módosítás: 2006. július
Ebben a kis pamfletben összegyűjtöttem a CCC és a Clipper közötti eltéréseket. Elsősorban a nyelvi különbségekre koncentráltam. Kevés szó esik a rendelkezésre álló könyvtárakról és ezzel kapcsolatban a nyelv lehetőségeiről. E tekintetben a UNIX-on és Windowson futó CCC nyilván óriási előnyben van.
Kezdem a rossz hírrel: A CCC nem alkalmas régi DOS-os Clipper programok változtatás nélküli lefordítására. A CCC általános célú programnyelv. Az eredeti Clipperből csak a jó részeket tartja meg, azokban viszont közel teljes a kompatibilitás. A CCC több területen kiterjeszti a Clippert (objektumok, szálak, névterek, kivételek). A kiterjesztés minden esetben úgy valósul meg, hogy a CCC speciális esetként tartalmazza a Clippper 5.x-et, miközben minimális mennyiségű szintaktikai újítást vezet be.
Manapság egyre kevesebben emlékeznek a Clipperre, érdemes ezért a CCC-t a Pythonhoz képest is pozícionálni. A CCC a Pythonhoz hasonlóan platformfüggetlen nyelv. A programozás hangulata, a leírandó kód mennyisége (saját tapasztalat alapján állítom ezt) nagyon hasonló. Mindkettővel pillanatok alatt meg lehet írni az egyszerű programokat. Nagy Python projektekről nincs közvetlen élményem, a CCC-ről azonban elmondhatom, hogy nagy és kritikus fontosságú projektekben is bevált.2 A Python bytekódra, a CCC (C++-on keresztül) natív kódra fordít. A Python nagy elterjedtsége folytán több kész modullal rendelkezik, viszont a CCC nyitottabb a C++ irányába, ezért könnyebb pótolni a hiányzó komponenseket.
A régi Clipperben a nyelv részének tekintették a dBase adatbáziskezeléssel kapcsolatos utasításokat. A CCC ezzel szemben általános célú programnyelv, amiben tetszés szerint írhatunk adatbáziskezelő szoftvert, ám ezeket nem a nyelv részének, hanem alkalmazásnak tekintjük. Az alap CCC nem tartalmaz hagyományos DBFNTX formátumú adatbáziskezelésre szolgáló eszközt, és nem is akarunk ilyen anakronisztikus dolgokkal vesződni. Az új fejlesztések középpontjában az SQL2 adatbázisinterfész áll, ami Oracle és Postgres adatbázisokhoz ad objektumorientált hozzáférést.
Amikor megismerkedtem a Clipperrel, az 5.01-es verzió volt éppen használatban. Ennek dokumentációjában azt találtam, hogy a public és private minősítésű változók elavult dBase örökség, használatuk nem javasolt, mivel ellentmondanak a korszerű programozási elveknek. Azóta sem tudok többet a public és private változókról, értelemszerűen nem kerültek be a CCC-be.
A régi Clipperben makrónak nevezték a forráskódot tartalmazó string változókat. Az ilyen forráskódot a Clipper röptében lefordította és végrehajtotta. A clipperes guruk kiterjedten használták a technikát menük megvalósítására, futás közben beolvasott lekérdezések végrehajtására. A makró azonban mindig a Clipper mocsarasabb területei közé tartozott. Sosem tudták egyértelműen szabványosítani, mi az, amire a makrófordító képes, és mi az, amire nem. A CCC-ben körülményes volna megvalósítani a makrókat, ezért jobbnak láttam az egészet elhagyni.3
CCC-ben a változókat kötelezően deklarálni kell (local vagy static). A régi Clipperben ez fordítási opció volt.
A Clipper ismerte a procedure utasítást, ami azonban semmiféle önálló jelentőséggel nem bírt. A CCC-be nem vettük át, helyette NIL-t visszaadó function-t kell írni. Hasonlóképp, nem megengedett az üres return, hanem a return-nel mindig meg kell adni egy visszatérési értéket, ami alkalmasint lehet NIL. Egy function-t nem kötelező return-nel befejezni, ha hiányzik, az a NIL visszatéréssel egyenértékű.
A régi Clipper a linkernek elsőként megadott object modul első függvényében kezdte a program végrehajtását. A CCC programban mindig kell legyen main függvény, a modulok sorrendjétől függetlenül mindig ott indul a végrehajtás.
A CCC-ben a kulcsszavakat mindig teljes hosszukban kell kiírni, pl. nem írhatunk function helyett func-ot.
A régi Clipperben kétféle értékadás létezett:
A régi Clipper megengedte a kódblokkok egymásba ágyazását, hogy azután az esetek egy részében hibás kódot fordítson. Az egymásba skatulyázott kódblokkok kellemetlenül bonyolítanák a fordítót, emellett feleslegesek is: A belső kódblokk helyére egy változót írunk, aminek értéke a kérdéses kódblokk.
A logikai kifejezések kiszámításakor, amint ismertté válik a végeredmény, a CCC abbahagyja a további tagok kiértékelését. Ez a Clipperben fordítási opció volt.
A CCC pad (padl, padr) függvényei nem fogadják el (blankekkel kiegészítendő) argumentumként a NIL értéket. A régi Clipper ilyen esetben üres stringet adott, ami véleményem szerint megtévesztő, azaz hibás.
A régi Clipperben a local a[10] deklaráció egy tízelemű arrayt (tömböt) hozott létre. A CCC-ben ehelyett azt írjuk: local a:=array(10), ami a Clipperben is ismert volt, CCC-ben és Clipperben ugyanazt jelenti, ráadásul még logikus is.
A régi Clipper elfogadta az a[1,2] kifejezést a kétdimenziós array egy elemének címzéseként. A CCC-ben helyette ezt kell írni: a[1][2], ami megintcsak ugyanazt jelenti a CCC-ben és Clipperben, ráadásul logikus. A dolog logikája: [] egy operátor, amit a tömbökre jobbról alkalmazunk, eredménye pedig a tömbelem.
Korszerű programnyelv nem nélkülözheti a névtereket. A forrásmodulok legelején állhat az opcionális namespace utasítás, például:
namespace aa.bb.ccaminek hatására a modulban definiált összes függvény az aa.bb.cc (többszintű) névtérbe kerül. A namespace utasítás nélkül, egyedileg is névtérbe helyezhetünk függvénydefiníciókat a következő módon:
function aa.bb.cc.f() ...A kétféle (globális és egyedi) minősítés (névtérbe helyezés) egyszerre is jelen lehet, ebben az esetben a hatásuk összegződik. A modulon belül definiált függvények meghívásakor nincs szükség teljes minősítésre.
A modul függvényeire (pl. f-re) kívülről a mínősített névvel, esetünkban az aa.bb.cc.f() formával hivatkozhatunk. A külső hivatkozások megkönnyítésére szolgál a using utasítás. A using utasítások a modul elején, közvetlenül az esetleges namespace után állhatnak. A using olyan rövidítést vezet be, amivel elkerülhető a teljesen minősített függvénynevek túl sokszori kiírása.
alternatív using utasítások | hivatkozás f-re |
using aa.bb.cc=alias | alias.f() |
using aa.bb=x | x.cc.f() |
using aa.bb.cc f g | f(), g() |
using aa.bb cc.f | cc.f() |
A globális (gyökér) névteret (kezdő) pont jelöli. Ha pl. a using aa.bb.cc f utasítás után aa.bb.cc.f helyett a globális névtérben definiált f-et akarjuk meghívni, akkor ezt írjuk: .f().
CCC/Clipperben a hagyományos függvénydefiníció function fname(a,b,c,d) alakú. A függvényt akárhány paraméterrel meg lehet hívni. Ha a függvény hívása (a példában szereplő) négynél több paraméterrel történik, akkor a négy felettieket a rendszer kipucolja (elvesznek), ha négynél kevesebbel, akkor azok az argumentum változók, amikre nem jutott érték, NIL-re lesznek beállítva.
Előre nem ismert számú paraméter átvételére valók a
function fname(*) function fname(a,b,c,*)formák. A '*' az utolsó helyen állhat, és jelzi, hogy a függvény akárhány paramétert átvesz. A második példában az argumentumok száma legalább három, ezekre név szerint is lehet hivatkozni, és NIL-re inicializálódnak, ha a függvényhíváskor nincs megadva elég paraméter.
Az alábbi kifejezésekben:
funcall(*) obj:method(*) {*}a '*' karakter helyére behelyettesítődik a függvény összes argumentum változója. Nézzük az alábbi kódot:
function fname(a,b,*) ? {*} //kiírja az összes argumentumot tartalmazó arrayt ? {*}[1] //kiírja a-t ? len({*}) //ennyi argumentum van (a-t és b-t is beleértve) ? ff(*) //továbbadja az összes paramétert (a-t és b-t is) ? o:meth(*) //továbbadja az összes paramétert (a-t és b-t is)A *-helyettesítés komplikáltabb formái is megengedettek, például {1,2,*,*} legális kifejezés, tehát '*' minden pozícióban és többször is használható. Függvény- és metódushívásban a *-helyettesítés megőrzi a refeket, azaz refes paraméterek refesen adódnak tovább.
Ugyanez a logika működik kódblokkban is azzal a különbséggel, hogy blokkon belül '*' a blokkargumentumokat helyettesíti. A {|*|fname(*)} blokk pl. minden paraméter továbbaddásával meghívja fname-et. Megengedett minden komplikáltabb eset is, pl. {|p1,p2,*|fname(*),p1+p2} minden paraméterrel meghívja fname-et, és visszaadja az első két paraméter összegét.
Külön szólni kell arról az esetről, amikor a blokkban egyetlen függvényhívás van, és a blokk minden paraméterét változtatás nélkül tovább akarjuk adni a függvénynek.
{|*|fname(*)}Az ilyen blokkok nem hoznak létre külön függvényhívási szintet a CCC stacken (nincs paraméter átpakkolás), legfontosabb alkalmazásuk az optimalizált metódushívás. Néhány további alkalmazás.
Konstruktor, ami minden paramétert átad az inicializálónak:
function clnameNew(*) return objectNew(clnameClass()):initialize(*)
Függvény meghívása arrayben megadott paraméterekkel:
evalarray({|*|fname(*)},{p1,p2,p3,...})
A Clipper 5.x-ben négy előre bedrótozott osztály volt, újak létrehozására nem volt lehetőség. A CCC-ben természetesen megvan a négy régi osztály, ám ami fontosabb, az alkalmazások tetszés szerint definiálhatnak új osztályokat. A CCC-ben nem szívesen vezetünk be új szintaktikát, ezért sokáig osztályok készítéséhez sem volt speciális szintaktika, hanem függvényhívási API szolgált a célra. Újabban az osztályokat a class utasítással definiáljuk, amihez kicsit kevesebbet kell írni.
static clid_template:=templateRegister() static function templateRegister() local clid:=classRegister("template",{objectClass()}) classMethod(clid,"initialize",{|this|templateIni(this)}) classAttrib(clid,"cargo") return clid function templateClass() return clid_template function templateNew() local clid:=templateClass() return objectNew(clid):initialize() function templateIni(this) objectIni(this) return this
A fenti példában a template osztály az object osztályból örököl, egy új metódusa (initialize) és egy új attribútuma (cargo) van. A metódusokat kódblokkal adjuk meg. Nincs new operátor, hanem az
o:=templateNew()függvényhívással jutunk új objektumpéldányhoz. Az egyszerű API-ból ,,magától" adódik egy sor olyan tulajdonság, amit a C++-ban, Jávában speciális kulcsszavakkal fejeznek ki. Például, ha nem definiáljuk a templateNew függvényt, akkor az osztály absztrakt lesz, azaz nem lehet példányosítani. Ha a templateClass függvényt static-nak definiáljuk, akkor az osztályból nem lehet örökölni, azaz Jáva terminológiával final.
Az objektumokat használó programnak nem kell számontartania, hogy az objektumműveletek belsőleg attribútumként vagy metódusként vannak-e implementálva. Ez annak következménye, hogy a fordító ugyanazt a kódot fordítja az o:initialize vagy o:initialize() bemenetre. Tehát az üres zárójelpár léte/nemléte nem utal arra, hogy attribútumról vagy metódusról van-e szó. Hasonlóképpen, ugyanaz a kód keletkezik az o:initialize:=x vagy o:initialize(x) bemenetből. Ez biztosítja, hogy az osztály implementációjában szabadon lehessen attribútumot metódusra, vagy metódust attribútumra cserélni. A Jávában elterjedt ún. set-get metódusoknak ezért kevesebb jelentősége van.
Az osztályok többszörös öröklődéssel örökölnek. A Pythonhoz hasonlóan minden objektum tag nyilvános. Mindig az objektum dinamikus típusa szerinti metódusok hívódnak meg, kivéve, ha az alábbi (kivételesen új) szintaktikával mást írunk elő:
o:(object)initializeA fenti explicit típuskényszerítés (metódus-cast) eredményeképpen az o objektum típusától függetlenül az object osztály initialize metódusa fog meghívódni.
Néhány új kulcsszó (class, attrib, method) árán kicsit csökkenthető az osztályok készítéséhez szükséges kód terjedelme. Az alábbi szintaktikát a fordító visszavezeti a függvényhívási API-ra, tehát az ott elmondottak most is érvényesek.
class derived(base1,base2,...) attrib a1 attrib a2 method m1 codeblock method m2 method m3 ...A class definíció függvények helyén állhat. A class a következő class-ig vagy function-ig tart. A baseclass-okat zárójelek között felsorolva kell megadni. Mindig van legalább egy baseclass. Az osztálydefiníció névtérbe helyezhető. A class definícióból automatikusan keletkezik a derivedClass és derivedNew függvény. A class függvény a szokásos módon az osztályazonosítót adja. A new (konstruktor) függvény létrehozza a megfelelő osztályú objektumot, és meghívja rá az initialize metódust. A konstruktor minden paramétert továbbad az inicializálónak.
A class törzsében csak attribútum és metódus deklarációk állhatnak. Megengedett, hogy a class törzse üres legyen.
Az attrib teljesen egyszerű: Lesz egy új (vagy felüldefiniált) attribútum az osztályban a megadott névvel.
A method kulcsszó és metódusnév után egy tetszőleges kódblokk írható, ebben az esetben a metódus implementációja maga a kódblokk. A metódus deklarációnak van egy alternatív formája: Ha a deklaráció csak a nevet tartalmazza, az olyan, mintha kiírtuk volna a következő (optimalizáltan forduló) kódblokkot: {|*|derived.m2(*)}. Tehát ilyenkor a metódust úgy kell implementálni, hogy megírjuk a derived.m2 (közönséges) függvényt. Mint látjuk, a metódusok az osztálynévből származó névtérbe kerülnek (automatikus prefixelés).
A CCC objektumrendszer speciális esetként tartalmazza a Clipper 5.x lehetőségeit. Ha a régebbi függvényhívási API-t használjuk és nem alkalmazunk metódus-castot, akkor a régi fordító nem talál szintaktikaliag ismeretlen elemet a programunkban. Így szintaktikai újítások nélkül is teljes értékű, általánosan használható objektumrendszerünk van. Sajnos bizonyos hátrányok is közösek: Mivel a Clipperben, Pythonban, CCC-ben csak futásidőben kapnak típust a változók, azért csak futásidőben derülhet ki, ha elírás folytán nemlétező metódusnévre hivatkozunk.4 Az objektumokról szóló leírás teljes változata itt olvasható: Objektumok használata a CCC-ben.
A POSIX thread API-t (pthread könyvtárat) használjuk szálak kezelésére. Minden szál saját lokális stackkel rendelkezik a függvényargumentumok és local változók számára. A program statikus objektumai közösek. A futtatórendszer gondoskodik a belsőleg definiált statikus objektumok konkurens használatának szinkronizálásáról. Az alkalmazások a saját statikus változóikat saját hatáskörben kell szinkronizálják. Bármelyik szálban beindulhat a szemétgyűjtés. Tudni kell, hogy nem minden CCC könyvtár szálbiztos.5
A DOS-os Clipper esetében nem lehetett szó többszálúságról. Tudomásom szerint a Python és Ruby úgy valósítja meg a többszálúságot, hogy a valójában egyszálú interpreter váltogatja az interpretált kódrészt. A CCC fordítás végeredménye natív kód, tehát a CCC-ben natív többszálúság van.
A kivételkezelést Jáva mintára kiterjesztettük:
function ff(x) local e begin /*sequence*/ ? "HOPP-1" break(x) //kivételt dob ? "HOPP-2" recover e <specerror> //elkapja specerror leszármazottait ? "rec1", e:classname recover e <error> //elkapja error leszármazottait ? "rec2", e:classname recover e <c> //elkapja a stringeket ? "rec3", upper(e) recover /*using*/ e //bármit elkap (régi szintaktika) ? "rec4", e recover //ez is bármit elkapna (felesleges) ? "ide nem jöhet" finally ? "lefut a begin-recover elhagyásakor" end /*sequence*/
Az újdonságok Clipper egyszintű kivételkezeléséhez képest:
A típuskifejezéssel bővített recover akkor kapja el a kivételt, ha a kivétel típusa megfelel a típuskifejezésnek. Az <error> típusú recover az error osztály leszármazottait kapja el. Könnyen látható, hogy az új begin-recover speciális esetként tartalmazza a régit: ha kiírjuk a sequence és using zajszavakat, ha csak egy típuskifejezés nélküli (régi módon mindent elkapó) recover-t használunk, és nem írunk finally záradékot.
A CCC kivételkezelése mindent tud, amit a Jáva, sőt, a kivétel tetszőleges CCC típus lehet. A példában külön recover ág kapja el a <c> (string) típusú kivételeket. A kivételekről szóló leírás teljes változata megtalálható itt: Struktúrált kivételek a CCC-ben.
Példa hosszú stringre:
local query_ab:=<<query>> select konto.a.id as id_a, name, datum, flag, konto.a.value as val_a, konto.b.id as id_b, konto.b.value as val_b from konto.a full join konto.b on konto.a.id=konto.b.id order by konto.a.id -- nulls first -- csak Oracle <<query>>A query_ab változó egy SQL parancs szövegét tartalmazza. A szövegben bármi előfordulhat, kivéve a <<query>> részstringet, mert az a szöveg végét jelzi. Persze a query helyett más szimbólumot is használhatunk, a lényeg, hogy a <<symbol>> részstring ne szerepeljen a közrefogott szövegben.
A 64-bites rendszerek megjelenésével szükségessé vált egy olyan adattípus, ami 64-bites mennyiségek tárolására képes. Egy P típusú változóval CCC (Clipper) szinten nem sokat tudunk kezdeni, lényegében csak az értékadás és egyenlőségvizsgálat működik vele. A típus értelme, hogy a C kiterjesztésekben előadódó handlereket, pointereket tárolni tudjuk a CCC programban. Korábban (32-biten) gyakran az N típust használtuk ugyanerre a célra.
Szigorúan véve a megjelenítés nem része a nyelvnek, mégis érdemes pár mondatot írni róla:
A karakteres megjelenítés ugyanazt tudja, mint a Clipper, megtoldva egy spéci terminál protokollal. A terminálos üzemmód lehetővé teszi, hogy a CCC program futtatása és megjelenítése különböző (akár földrajzilag távoleső) gépeken történjen. Természetesen a UNIX-on futó program szót ért a Windowson futó terminállal, és fordítva.
Az új programjainkban a megjelenítést a Jáva terminálra bízzuk, teljesen szétválasztva az alkalmazási logikát és a megjelenítést. A Jáva terminál egy 300K-s jar fájl, ami bárhol fut, ahol a JRE installálva van. Működési elve leginkább egy szerver oldali widgetekkel dolgozó X szerverre hasonlít. A Jáva terminállal igényes GUI-t lehet készíteni ügyviteli programokhoz. Rendkívüli előnye, hogy ugyanaz a program használható lokálisan, intranetes és internetes környezetben.
A CCC-GTK csatoló könyvtár szintén alkalmas a legigényesebb GUI-k készítésére. A ccc3_glade modul lehetővé teszi, hogy a népszerű glade-del készített GUI-kat működtessük CCC-ből. Ezzel egy grafikus GUI szerkesztő is helyet kapott CCC fegyvertárában.
A Clipperben és a CCC2-ben nincs unicode támogatás. A CCC2 bedrótozottan Latin/CWI karakterkészlettel működik, a stringeket és a (tetszőleges tartalmú) bytesorozatokat nem különbözteti meg. Ezért koreai, arab, héber, orosz, stb. nyelvű programok készítésére nem alkalmas.
A CCC3-ban viszont megjelent a unicode támogatás. A Jávához hasonlóan külön típus szolgál a bináris adatok (bytesorozat) és a karakter stringek tárolására. Az új binary string típus átveszi a régi stringek szerepét, amikor azokat bináris adatok tárolására használtuk. A karakter string értelmezése változott: az új (CCC3) rendszerben unicode karakterek sorozatát tartalmazza. A CCC3 programokban a karakterliterálokat UTF-8 kódolással kell beírni. Az új/megváltozott típusok miatt nincs teljes kompatibilitás, de viszonylag kis munkával lehet portolni a programokat CCC3-ra, ami ímmár támogatja az internacionalizált programok készítését, legalábbis, ami a karakterállandók különböző nyelvi változatainak csereberéjét illeti. A függvény- és változónevekben továbbra sem lehet ékezetes (vagy cirill) betűket használni. További infó a témáról az Újdonságok a CCC3-ban és Unicode használat a CCC3-ban dokumentumokban.
1ComFirm BT.
2Hogy mi a kritikus szoftver? Ha pl. a szoftver működésképtelensége miatt a bank nem tudja kiszolgálni az ügyfeleit, az kritikus.
3A bytekódra fordító programoknál (mint a Python, Jáva, régi Clipper) egyszerűbb a helyzet, a CCC azonban C++-ra, arról pedig natív kódra fordít.
4Pythonban még rosszabb a helyzet, ui. az eltévesztett nevű attribútum automatikusan létrejön, tehát még később derül fény a hibára.
5Csak a ccc2 és ccc2_ui_ könyvtárakban kitűzött cél a szálbiztonság. A szálbiztonság legnehezebben teljesíthető kritériuma: Minden olyan pillanatban, amikor egy másik szálból szemétgyűjtés indulhat, a vermeken kell legyen minden élő változó, de nem lehet ott semmi más, pl. keletkezőben vagy megszűnőben levő változók. Ugyanez szükséges a korrekt szignálkezeléshez is. Persze ez nem az alkalmazások, hanem a futtatórendszer felelőssége.