A CLU programozási nyelv

Történet



A CLU története

Bevezető


Barbara Liskov: Histrory of CLU
Laboratory of Computer Science Massachusetts Institute of Technology
Cambridge, MA
1992, április
Kivonatos fordítás



A nyelvet a fenti egyetemen Barbara Liskov vezetésével fejlesztették a 70-es évek folyamán. A CLU volt az első megvalósított programozási nyelv, mely nyelvi eszközöket adott az adatabsztrakcióhoz, melyre a programozási módszertanok fejlődésével igény mutatkozott. Ezen kívül tartalmaz néhány, azóta több nyelvben is átvett lehetőséget, mint például a kivételkezelés, a programozható iterátorok és a paraméterezhető típusok.

A 70-es évek elején megoldásokat kerestek a programfejlesztések különböző problémáinak kezeléséhez. Ekkoriban jelentek meg az első munkák a strukturált programozásról (Dijkstra, Wirth), valamint a modularitásról (Randell, Parnas). Az adatabsztrakció - olyan fordítási egységek, melyek adatokat és a rajtuk végezhető műveleteket kínálják, de elrejtik a megvalósításhoz szükséges egyéb információkat - megfelelő eszköznek látszott a fenti módszerek alkalmazására. Ennek az ötletnek az alapján kezdődött a nyelv fejlesztése 1973-ban, s mivel ezeket a fordítási egységeket cluster-nek nevezték, ennek első három betűjéből kapta a nyelv a CLU nevet.

A nyelv eredetileg nem kifejezetten piaci terméknek indult, hanem inkább a programozási módszertanok fejlesztéséhez és oktatási célra szánták. Ennek ellenére készültek fordítók, de a nyelv nem igazán terjedt el. Az eredményeit több ismert, és elterjedtebb programozási nyelv kifejlesztésénél is alkalmazták (Ada, C++, ML, Modula 3, Trellis-Owl). A nyelv fejlesztését 1979-ben felfüggesztették, ill. egy új nyelv, az Argus megalkotásába fogtak.

A fejlesztéskor a szemantikus modellt a Lisp alapján készítették el, a szintakszis pedig Algol-szerű lett. Az adatabsztrakciós modellhez a legjobban a Simula 67 osztály fogalma hasonlított, de az akkori változat nem tartalmazott adatelrejtést, paraméterezhető típusokat, és a beépített típusokat a rendszer eltérően kezelte, mint a felhasználó által definiáltakat. Mivel az öröklődés nem pontosan fedte azt a célt, amit a CLU-nak szántak, ezért ezeket az eredményeket nem használták fel. (Ez az a pont, ahol a CLU lényegében eltér a ma ismert objektum-orientált programnyelvektől.)

A tervezési alapelvek

A nyelv kialakításánál a következő tervezési alapelveket vették alapul:

Eredmények, a nyelv áttekintése

Absztrakt adattípusok:

Az absztrakt adattípusokat cluster-ek valósítják meg. Egy cluster egy fordítási egység, és három fő részből áll: fejrész, reprezentáció definíciója, eljárások. A fejrész tartalmazza a típus nevét, a paramétereket, és a kívülről hívható eljárásokat, a reprezentáció és az eljárások kívülről rejtve vannak. Az eljárásokra kétféle módon lehet hivatkozni, mint a típus eljárása (Alphard), vagy mint az objektum eljárása (Simula, C++). A CLU nyelvben az első változatot részesítettek előnyben. A nyelv lehetővé teszi, hogy egyes speciális műveletekre hagyományos módon hivatkozzunk, pl. a+b, r.a, a<b (szintaktikus cukrok), de ezeket egy előfordító az eredeti típusműveletre fordítja. (A CLU2C fordító tartalmaz olyan szintaktikus cukor bővítéseket, melynek segítségével használhatjuk az objektum eljárása hivatkozási formát is.)
 

Szemantikus modell:

A CLU objektumokat kezel, melyeket egy közös adatterületen tárol. A változók csak azonosítják, ill. hivatkoznak az objektumokra. (Mint a pointerek, de a nyelv nem tartalmaz ilyen adattípust.). A változók tárolásának módját nem határozza meg a specifikáció, az lehet stack, heap, vagy valami más is. A működő rendszer feltételezi egy szemétgyűjtő (garbage collector) létezését, mely folyamatosan kitakarítja a már nem használt objektumokat a közös adatterületről. Egy x := exp értékadásra a kifejezés kiértékelődik, létrejön egy új objektum, mely ezt az értéket tartalmazza, és az x változó hivatkozását erre az objektumra állítja, x := y esetén mindkét változó ugyanarra az objektumra fog hivatkozni. Ez volt az alapmodell. Mivel az egyszerű adattípusok - int, real, stb. - esetén drasztikusan megnő a memória használata, és a teljesítmény csökken, ezért bevezették az immutable objektum fogalmát, míg a fent leírt objektumok mutable típusúak. Az immutable objektumokra a rá hivatkozó változó nem a hivatkozást tárolja, hanem magát az egész objektumot. Ekkor értékadáskor nem a hivatkozást kapja meg a változó, hanem a kapott érték másolatát. Az immutable objektumok sohasem lehetnek osztottak, azaz mindig pontosan egy változó hivatkozik rájuk.

  • Immutable típusok: null, bool, int, char, string
  • Mutable típusok: any
  • Mutable/immutable típus párok: array/sequence, record/struct, variant/oneof

  • Biztonság:

    A nyelv erősen típusos, minden típusellenőrzés fordítási időben történik, a típusparamétereket a megadott eljárások, az eljárás paramétereket a szintakszis alapján ellenőrzi. (Szemantikus ellenőrzés nincs.) Típusellenőrzés a beépített típusok és a cluster-ek alapján történik. Két típus nem lehet azonos, ha két különböző cluster-ben definiálták, még akkor sem, ha a két definíció megegyezik.

    Érték nélküli változóra való hivatkozás ellenőrzését nem építették be a nyelvbe, de a fordítónak a fordítási időben történő folyamat-ellenőrzést ajánlják a futásidejű ellenőrzés helyett. Változók deklarálásakor nem használ automatikus nil értékadást. Objektum létrehozásakor, explicite végrehajtódik az adott objektum elemeinek létrehozása is, így elkerülhető, hogy egy objektumban érték nélküli részobjektumok maradjanak. Például egy tömb létrehozásakor, egy elemeket nem tartalmazó tömb jön létre, vagy ha megadtunk literált, akkor azokat az értékeket veszi fel, de érték nélküli elemeket soha nem fog tartalmazni.
     

    Paraméteres polimorfizmus:

    A beépített és felhasználó által definiált típusok egységes kezelése érdekében vezették be a paraméteres típusokat, ugyanis a legtöbb nyelv tartalmaz ilyen beépített típusokat, pl. a tömböket. A paraméteres típusok nem típusok, hanem inkább típus generátorok, a típus akkor jön létre, amikor a hivatkozásnál megadjuk az aktuális paramétert is - ennek a kiértékelése fordítási időben történik meg, ezért csak fordítási időben kiértékelhető paramétereket adhatunk meg. (Kevés nyelv tartalmaz még ma is ilyen lehetőséget, pedig nagyon hatékony eszköz.)
     

    Uniformitás:

    Minden típusnál az eljárás hivatkozás mindig tartalmazza az adott típust is, melyhez az adott eljárás tartozik: t$p(...) - t a típus neve, p az eljárásé. Mivel ez a hivatkozás kényelmetlen a hagyományos műveleteknél (+,-,*,/,<,,stb.), ezért bevezették a 'szintaktikus cukor' fogalmát, mely lehetővé teszi aritmetikai kifejezések használatát. A fordítóprogram egy x + y kifejezésen egy előfordítást végez, és a következő alakra hozza: t$add(x,y), ahol t az első paraméter (itt x) típusa, majd a fordítást a 'cukortalanított' kifejezésen végzi el. A fenti módszerben a nyelv nem tesz különbséget a beépített és a felhasználó által definiált típusok között, ezért ezek az egyszerűsített szintaktikai formák bármely típusra használhatók - de pl. a fenti esetben a típusnak rendelkeznie kell egy két paraméterű add eljárással. Új 'cukorkák' megadására nincs mód.

  • Operátorok: ~ (not), -, **, // (mod), /, *, || (concat), +, -, <, , <=, =, & (and), | (or), cand (and then),

  • cor (or else)
  • Rekord típus: r.m <- rec$get_m(r) vagy rec$set_m(r, ...) attól függően, hogy értékadás jobb, vagy baloldalán helyezkedik el. Itt a get_m, set_m eljárásokat a fordító generálja minden rekord típus minden mezőjéhez.

  • (rec az r típusa)
  • Tömb: a[i] <- arr$fetch(a,i) vagy arr$store(a,i, ...) attól függően, hogy az értékadás jobb, vagy baloldalán helyezkedik el. (arr az a típusa)
  • A beépített típusokhoz léteznek literálok, ezeknek szintaktikus alakja alapján végzi a fordító a típusellenőrzést. A fejlesztők véleménye az, hogy nem sérül jelentős mértékben az uniformitás, ha a felhasználó által definiált típusoknak nincs literálja, ezért ezt a lehetőséget nem valósították meg. (Nem is igazán látszik, hogy meg lehetne-e ilyesmit valósítani.)
     

    Kivételkezelés:

    Robusztus, hibatűrő programok írásához, szükség van jó hibakezelő eszközökre. A kivételkezelés erre ad eszközt úgy, hogy az eredeti kód egyszerűsége, áttekinthetősége megmarad. Azt a módszert alkalmazzák, amikor egy eljárás többféleképpen fejeződhet be, egyrészt 'normális' módon, másrészt 'kivételes' módokon. A kivételek paraméterezhetők, de ellentétben más nyelvi megvalósításoktól, a le nem kezelt kivételek nem adódnak át automatikusan a hívó eljáráshoz, hanem egy általános failure nevű kivételt váltanak ki. Ez a kivétel minden típushoz automatikusan hozzáfordítódik. Az erős típusossághoz hozzátartozik, hogy az eljárás által kiváltható minden kivételt, a paraméterlistával együtt, meg kell adni az eljárások fejlécében. A kivételkezelő rutinok az eljáráshívás részei, a lekezelt kivétel után, a következő utasítással folytatódik a végrehajtás.
     

    Iterátorok:

    Több összetett típus is olyan, hogy különböző struktúrákban, de egyforma típusú elemeket tartalmaznak, melyeken esetenként végigmenve ugyanazt a műveletet kell elvégezni. Ebben az esetben az összetett típus minden elemén végiglépkedő részprogramját mindig meg kell írni, csak a ciklusmag változik. Kézenfekvő egy olyan részprogram írása, mely egy összetett struktúra elemeit sorban megadja. A gond általában az, hogy nincs erre nyelvi elem.

    A CLU nyelvben erre a célra az iterátorok szolgálnak. Az iterátorok külön modulban, vagy egy típus saját iterációjaként helyezkednek el, nagyon hasonlítanak a eljárásokra. A formai különbség annyi, hogy az iterátor return helyett yield utasítást tartalmaz. Egy yield utasításnál a futás felfüggesztődik, a paraméterben átadott érték (a fenti példában az aktuális elem) átadódik a hívó eljárásnak, és később, mikor visszakerül a vezérlés az iterációhoz, akkor az adott yield utasítás után folytatódik a végrehajtás. Az iteráció befejeződik, ha az eljárás végére ér.

    Hivatkozás egy iterációra a for utasítással történik: for x:int in array[int]$elements(a) do ... end x a ciklus lokális változója, az a egy array[int] típusú tömb, az elements pedig az array[int] típushoz írt iteráció, mely a yield utasításban a tömb elemeit adja sorban. Itt, amíg az elements iteráció ad értékeket, addig az x változó felveszi azokat, és a ciklustörzs lefut. Ha az elements iteráció véget ér, akkor a ciklus is véget ér. Ha a for ciklus megszakad, akkor az iteráció futása is befejeződik. Természetesen az iterációk az eredeti szándéktól eltérően is használhatók.
     

    Programszerkesztés:

    Az adatabsztrakció lehetővé teszi, hogy a modulokat egymástól függetlenül fordítsuk. A típusellenőrzéshez szükség van a modulok fejlécére, de az implementációt később is megadhatjuk. Egy tervezési folyamat közben a már megadott specifikációt nem szerencsés megváltoztatni - hiszen elképzelhető, hogy más, már elkészült modulok hivatkoznak rá. Másrészt előre összeállíthatók gyakran használt típusokból készletek, vagy akár egy teljes típus rendszer is. Végül nem készült ilyen könyvtár a CLU nyelvhez, a szerzők elkezdték ezt a munkát, de az eredményeket már az új Argus nyelvbe építették be.

    A nyelv nagyon jó eszközöket ad, mind a felülről-lefelé, mind az alulról-felfelé történő tervezéshez, valamint a párhuzamos fejlesztéshez is. (A párhuzamos futású programok írásához nem ad eszközöket.)