A Clojure programozási nyelv

Az Objektum-orientált programozás alternatívái

A Clojure nyelv a funkcionális paradigma mellett támogatja az objektumorientált programozást is. A nyelv komoly erőssége, hogy ez a része jól együttműködik a java típusrendszerével. Lehetséges ugyanis új típusokat létrehozni, amikhez a megfelelő java típusok is létrejönnek, és ezek később akár újra felhasználhatóak is lesznek.

Protokollok

Az öröklődés a nyelvben protokollok segítségével történik. Egy protokoll függvények gyűjteménye, amikhez a programozó különböző implementációkat biztosíthat a protokoll megvalósításakor. Ezek működése nagyon hasonló a javaban megszokott interface működéséhez, ám fontos különbség, hogy egy típushoz a definíció után, menet közben is rendelhetünk protokollokat. Hasonlóan az interface-ekhez, egy objektum több protokollt is megvalósíthat, ezt a (satisfies?) hívásával ellenőrizhetjük.

Protokoll definiálásakor megadjuk a protokoll nevét, egy opcionális docstring-et, és a függvények szignatúráit opcionális további docstring-ekkel. Egy függvény szignatúrája tartalmazza a függvény nevét és a lehetséges paramétereinek a felsorolását. Az felsorolás első eleme mindig a this lesz, azaz az aktuális objektum referenciája. Ha több aritást is szeretnénk engedni, akkor több felsorolást is meg kell adnunk.

A defprotocol hívása automatikusan létrehoz egy java interface-t is a megfelelő névtéren belül a megfelelő metódusokkal. Ekkkor az interface megvalósításai automatikusan együtt fognak működni a protokoll függvényeivel is.

Egy protokoll definiálása tehát a következőképpen történhet. A példában létrehozuk a PrettyPrint protokollt és a hozzá tartozó pr függvényt.

(defprotocol PrettyPrint
	(pr [t] "visszaad egy jól olvasható string reprezentációt"))

Most még nem tudjuk semmire sem használni a függvényünket, hiszen semmilyen típus nem nyújt hozzá implementációt. Több módon is összekapcsolhatunk protokollt és konkrét típust.

Az extend-type segítségével egy típushoz több protokollt kapcsolhatunk. Az extend-protocol segítségével egy protokolhoz több típust is kapcsolhatunk. A példában az utóbbit használjuk.

(extend-protocol PrettyPrint
	Integer
	(pr [i] (str "egesz szam: " i))
	String
	(pr [s] (str "string: " s))
	Object
	(pr [o] (str "ismeretlen objektum: " o)))

Rekordok

Egy programban sokszor szükség van kulcs-érték párokat tartalmazó adatszerkezetekre. Ekkor használhatjuk a beépített map típust, de definiálhatunk saját típusokat is. Ez utóbbinak nagy előnye, hogy nagyobb a kifejezőereje, gyorsabb programot eredményez, és a létrehozott típust később bővíthetjük.

A defrecord makró egy új rekord típust hoz létre. Ehhez meg kell adnunk a típus nevét, kulcsainak a felsorolását. Megadhatjuk továbbá opcionálisan a megvalósított protokollok és interface-ek felsorolását és a konkrét megvalósításokat. A rendszer ekkor az aktuális névtérnek megfelelő csomagban egy új osztályt hoz létre.

Az új rekord típushoz létrejön továbbá két konstruktor függvény is. A ->Típusnév formájú a kulcsokhoz tartozó értékekek várja paraméterként, a map->Típusnév pedig a kulcs-érték párok map-jéből készíti el a rekord egy példányát.

A multimetódusok polimorfizmusa

A multimetódusok olyan függvények, amelyekhez a paramétertől függően később is különböző implementációkat adhatunk. A multimetódus definiálásakor megadunk egy úgynevezett dispatch függvényt. A definíció a (defmulti) hívásával történik. A metódusok hozzáadása a (defmethod) hívásával történik, ekkor megadjuk, hogy a hozzáadott függvénytörzs a dispatch függvény milyen visszatérési érték esetén hívódjon meg.

Lehetséges alapértelmezett implementációt is megadni, ami akkor fut le, ha a dispatch függvény visszatérési értéke egyik hozzáadott metódusnak sem felel meg. Ekkor a (defmulti) hívásakor a :default kulcsszó után megadunk egy alapértelmezett értéket, és az ehhez az értékhez tartozó metódus törzse fog végrehajtódni. Ez az érték alapértelmezetten a :default kulcsszó.

A (get-method multifn dv) hívásával megkaphajuk, hogy egy adott multimetódus egy adott paraméterre milyen implementáció fog végrehajtódni. A (methods mulifn) függvényhívás visszaad egy map-et, amiben a megadott dispatch értékekhez a különböző implementációk vannak rendelve.

Gyakran a paraméter típusa alapján akarunk különböző implementációkat biztosítani.

(defmulti terulet class)
(defmethod terulet Circle [x] (* (:r x) (:r x) 3.1415))
(defmethod terulet Rectangle [x] (* (:width x) (:height x)))

A multimetódus konkrét dispatch értékéhez tartozó implementációját a clojure.core/remove-method segítségével, az összes implementációt pedig a clojure.core/remove-all-methodshívásával távolíthatjuk el.

Egy objektum gyakran több protokollt és interfacet is megvalósíthat, ekkor néha nem egyértelmű, hogy a multimetódus mely implementációja hívódjon meg. Ezt a helyzetet oldja fel a (prefers-method).

Objektumok, osztályok generálása

Adott típus egy példányát a megfelelő konstruktor hívásával hozhatjuk létre. Láthattuk, hogy rekordok esetén ez map->Típusnév és ->Típusnév alakú. Ha ilyen konstruktor nem áll rendelkezésre, akkor használhatjuk a new formát is. Ez a parancs a típus nevét és a paraméterek felsorolását várja.

A következő példában létrehozzok a három alapszínt.

	(def piros (new java.awt.Color 255 0 0))
	(def kek   (new java.awt.Color 0 0 255))
	(def zold  (new java.awt.Color 0 255 0))
	

Erre egy másik lehetőség a "." (pont) nyelvtan használata. Ekkor a típus neve után tett pont karakter a típus konstruktorfüggvényét jelöli. Például (new java.awt.Color 1 2 3) helyett írhatunk (java.awt.Color. 1 2 3) formát is.

Objektumok tagváltozóinak elérésére is hasonló szintaxis használható. A (. x valtozoNev) forma visszaadja az x objektum megfelelő nevű tagváltozójának értékét. Ezt rövideben úgy is írhatjuk, hogy (.változóNév x), de itt nem szabad szóközt hagyni a pont és a név között. A tagfüggvények hívása hasonlóan történik, ekkor itt a lista végére még odaírjuk hívás paramétereit is.

A bean függvény visszaad egy olyan hash-map-et, amely kulcsai a paraméterül kapott objektum tagváltozóinak nevei, az értékei pedig a változók értékei lesznek. Érdemes tudni, hogy a Clojure alapértelmezetten a Java Reflection API-ját használja a tagváltozók eléréséhez, ami lassú lehet, ezért, ha sokszor van szükségünk ilyen hívásokra, hasznos lehet először az objektumról készíteni egy ilyen lenyomatot, és annak az értékeit lekérdezni.

proxy

A (proxy) makró létrehoz egy olyan típusú objektumot, ami az adott őstípusokat valósítja meg. Ezek lehetnek interface-ek és protokollok, és legfeljebb egy nem final java osztály. A proxy makró hívásában meg kell adnunk az ősosztály nevét (ez alapértelmezetten Object), az ősosztály konstruktor hívásakor a paraméterben átadott értékeket, és az őstípusok listáját és a hozzájuk tartozó metódusok implementációit. A proxy hívás a java virtuális gép proxy mechanizmusát használja ki.

A proxy egy érdekes alternatívája a (reify) makró. Ez nagyon hasonló az előzőhöz, de több különbség is van. Először is, a reify nem támogat konkrét osztályokat, csak interface-eket és protokollokat. Továbbá a létrehozott objektum metódusai valódi metódusok lesznek, nem pedig egy map alapján hivatkozik a megfelelő függvényekre. Így a metódusok későbbi cseréje sem lehetséges. Mindezekért cserében a reify alkalmazása proxy helyett sokkal gyorsabb programot eredményez.

gen-class

A Clojure fordító által biztosított AOT (Ahead-Of-Time) fordítás lényege, hogy a clojure kód egy részét egy java osztályokba fordíthatjuk. Az osztály metódusai a megadott függvények lesznek. Ennek egyik előnye, hogy a létrehozott java osztály a clojure környezeten kívül is jól használható más java kókkal együtt. Másik fontos előnye, hogy az előre lefordított kód sokkal gyorsabban tud futni, mint a menet közben fordított. Most a névtér fordítását tekintjük át.

Ha névtérből akarunk osztályt fordítani, azt legkönnyebben az (ns) deklarációban elhelyezett (:gen-class) direktívával tehetjük meg. Ebben az esetben az aktuális névtér függvényei lesznek a létrehozott osztály metódusai. A direktívában megadható konstruktor, vagy az örökölendő típusok listája is. A :prefix paraméter megadja, hogy milyen előtaggal rendelkező függvények kerüljenek az osztályba, ez alapértelmezetten a "-" jel.