A Clojure programozási nyelv

Speciális formák, vezérlési szerkezetek

Speciális formák, vezérlési szerkezetek

def, let és set! - a definíció és értékadás 3 alakja

Clojure-ben a Clojure "változók" (ún. Vars, a továbbiakban: Var) értéke (ellentétben a Java változókéval), csak úgy változhat, ha a hozzájuk tartozó változókötés megváltozik. A változókötések szálanként különbözőek is lehetnek, így akár (különböző szálakon) egy időpillanatban különböző értékeket is jelölhet ugyanaz a Var. Tehát Var-ok esetében nem az értékek változnak, hanem a változókötések, amiket úgy képzelhetünk el leginkább, hogy (szálanként) egy-egy veremben helyezkednek el. Ha begépeljük a következőt:

	user=> (def x 4)   ; definiálunk egy változókötést, ami a 4-re mutat
	#'user/x
	user=> x           ; a Var nevének megadása már elég ahhoz, hogy megpróbálja feloldani a referenciát (implicit dereferencia)
	4

Az adott szál vermének tetején tehát minden időpillanatban a Var (adott szálon) aktuális értékére mutató referencia áll, és ha a Var-nak nincs az adott szálban lokális értéke, akkor a dereferencia során ellenőrzésre kerül, hogy van-e a Var-hoz ún. root binding (alapkötés, ami az összes szálra nézve közös, és egyik szálra nézve sem lokális), és ha van, akkor a root binding szerinti értéket adjuk vissza. A fenti példában a def speciális formával pont ezt az alapkötést állítottuk be, és innentől kezdve az összes szál x-et 4-nek látja, kivéve ha nem ad neki más, adott szálra nézve lokális értéket. A root binding egyébként megváltoztatható az alter-var-root függvénnyel, de ezt értelemszerűen nagy elővigyázatossággal kell használni. A fent említett vermek kezelő függvényeit is megtalálhatjuk a clojure.core-ban: a push-thread-bindings és a pop-thread-bindings függvények közvetlenül a Var objektumok megfelelő műveleteit hívják. (Ezek mind alacsony szintű függvények, nincs szükség a használatukra) A már látott def mindezekkel szemben viszont nem függvény, hanem speciális forma, ahogy következőkben bemutatásra kerülő let és a set! is az.

A let egy olyan szerkezet, ami lehetővé teszi egy lexikális hatókörön belüli lokális változók (locals) inicializálását. A lokális változók nem az előbb bemutatott Var-ok, de egy lexikális scope-on belül elfedhetik a Var-ok értékeit, így azokhoz vagy a névtér kiírásával, vagy a var objektum explicit dereferentálásával kérhetünk le:

	user=> (let [x 55] [x user/x (deref (var x)) (deref #'x)])  ; a #' reader makró egyenértékű a var speciális formával, az előbbi az utóbbira alakul át
	
	[55 4 4 4]   ; az x var értéke 4 (fenti példában állítottuk be)

A let-tel beállított értékek csak egyszer kerülhetnek beállításra, többet nem változtathatók! Legvégül pedig, a set! speciális forma Var-ok változókötéseinek megváltoztatására használható - egy megkötés van: set!-tel nem állíthatunk át root binding-okat, csakis adott szálra nézve lokális Var értékeket, valamint - természetesen - Java változókat.

A fenti három speciális formán kívül léteznek más szerkezetek is a nyelvben: ilyenek például a binding vagy a with-bindings makró, ezek azonban implementációs szinten mind a fent említett módszereket (beleértve a push/pop-thread-bindings függvényeket is) használják.

Szekvencia: do

Clojure-ben a do speciális forma felel meg a szekvenciának: sorban kiértékeli a paraméterként kapott kifejezéseket, visszatérési értéke az utolsó kiértékelt kifejezés. Ha nincs ilyen (mert nem adtunk meg törzset a do-nak), akkor a do kiértékelése nil-t eredményez.

Elágazás, vagyis: az if forma

Clojure-ben feltételeket az if speciális formával írhatunk. A feltételkifejezés függvényében az igaz, vagy a hamis ág kerül kiértékelésre. Ha nem adtunk meg hamis ágat, és a feltételkifejezés hamis igazságértékű (tehát false vagy nil), akkor a feltételkifejezés nil-t ad vissza.

    user=> (if (= 1 2) :igen :nem)
    :nem
    user=> (if (= 1 2) :igen)
    nil

Ciklusok Clojure-ben: loop

Ciklusokat a loop speciális formával írhatunk:

    user=> (loop [x 0 y 0]
             (if (not (= x 5))
                (if (not (= y 5))
                   (do (print "X ") 
                       (recur x (inc y)))
                   (do (println) (recur (inc x) 0 )))))
    X X X X X 
    X X X X X 
    X X X X X 
    X X X X X 
    X X X X X 
    nil
    

A loop hasonlóan működik, mint a már ismertetett let, annyival tud többet, hogy rekurziós pontként szolgál a benne szereplő recur speciális formák számára. Tehát általánosan elmondható, hogy a Clojure ciklus szerkezete (a loop speciális forma) valójában nem más, mint egy kényelmes szerkezet "rekurzív ciklusok" írására. Hogy mit csinál a recur forma, arról lásd a következő bekezdést.

Rekurzió hatékonyan: recur

Funkcionális programozást támogató nyelvekben megkerülhetetlen a rekurzió fogalma: azt már láttuk, hogy Clojure-ben nincsenek valódi ciklusok (csak loop speciális forma) - "számítástudományi" szemmel nézve ez nem igazán jelent gondot, hiszen minden, ami kiszámítható (például elöl vagy hátultesztelős) ciklusok segítségével, az kiszámítható kizárólag rekurzív függvények használatával is - az igazi gond a hatékonysággal van: a rekurzió ugyanis az iterációk számával lineáris veremterületet igényel, ami könnyen vezet veremtúlcsorduláshoz:

    (defn lnko [a b]
          (if (< a b) (lnko a (- b a))
          (if (> a b) (lnko (- a b) b)
                       a)))

    user=> (lnko 1132134 11241214124)
    
    java.lang.StackOverflowError (NO_SOURCE_FILE:0)

Ezekre az esetekre találták ki az úgynevezett farokrekurzió optimalizációt (tail-recursion optimization), aminek az ötlete abban rejlik, hogy amennyiben a rekurzív hívás visszatérési értéke a teljes függvény visszatérési értéke, akkor fölösleges megtartani a veremben az aktuális változókötéseket csak azért, hogy a rekurzív hívás lefutása után visszatérjünk ide, és a hívás visszatérési értékét változtatás nélkül visszaadjuk mint saját visszatérési értékünket. Ehelyett a rekurzív hívások teljes vezérlésátadást jelentenek: a rekurzív hívás előtt "kitisztítjuk magunk után a hívási vermet", így a rekurzív hívás közvetlenül a hívónak fogja visszaadni az értékét. Persze ez az optimalizáció csak akkor működik, ha a rekurzív hívások farok pozícióban (tail position) helyezkednek el: speciálisan, Clojure-ben és a Lisp nyelvekben egy függvénykifejezés részkifejezése farok pozícióban helyezkedik el, ha a részkifejezés lehet a teljes függvény visszatérési értéke. A fenti példában a két lnko hívás, valamint a 4. sorban lévő "a" farokpozícióban helyezkednek el.

Ha a fenti példában alkalmazható a farokrekurzió optimalizáció, miért kapunk mégis veremtúlcsordulást? A válasz egyszerű: Clojure-ben a farokrekurzió optimalizált hívásokat a recur speciális formával kell írni:

    (defn lnko [a b]
          (if (< a b) (recur a (- b a))
          (if (> a b) (recur (- a b) b)
                       a)))

    user=> (lnko 1132134 11241214124)

    2

Amennyiben nem farokpozícióban használjuk a recur-t, hibát kapunk, hiszen ilyenkor nem alkalmazható a fenti optimalizáció:

    user=> (defn fuggveny [n]
              (* n (recur (dec n))))
    java.lang.UnsupportedOperationException: Can only recur from tail position (NO_SOURCE_FILE:55)

Tehát Clojure-ben érdemes mindig inkább farokrekurziót használó algoritmusokat írni a recur használatával: sajnos a recur speciális forma léte abban gyökerezik, hogy a Java platform nem támogatja az általános farokrekurzió optimalizációt (general TCO), így nem használható hatékonyan a Scheme programozási nyelvből és más Lisp nyelvekből ismert "trükk", a continuation-passing style sem.

Kölcsönös rekurzió

A fent említett recur forma nem használható kölcsönös rekurziót használó esetekben:

    (declare bar)    ; a bar-t előre deklarálni kell, vagy pedig a letfn függvényt lehetne még használni

    (defn foo [n] 
      (if (pos? n) 
          (bar (dec n)) 
          :done-foo)) 

    (defn bar [n]
      (if (pos? n)
          (foo (dec n)) 
          :done-bar)) 

     user=> (foo 1000000) 
     java.lang.StackOverflowError (NO_SOURCE_FILE:0)

A nyelv készítője ezt úgy oldotta meg, hogy definiált egy trampoline nevű függvényt:

    (declare bar)
	
    (defn foo [n] 
      (if (pos? n) 
          #(bar (dec n))   ; anonymous függvényt adunk vissza
          :done-foo)) 

    (defn bar [n]
      (if (pos? n)
          #(foo (dec n))   ; anonymous függvényt adunk vissza 
          :done-bar))

    user=> (trampoline foo 1000000)   ; trampoline hívás, lehetne így is: (trampoline #(foo 1000000)) 
     
    :done-foo

A trampoline tehát úgy működik, hogy meghívja a paramétereként kapott függvényt a megadott paraméterekkel, és ha az így meghívott függvény egy másik függvényt ad vissza, akkor ezt a függvényt is meghívjuk, egészen addig, amíg nem függvényt kapunk vissza.

Idézőjelezés

Lisp nyelvekben az idézőjelezésnek (quoting) nevezett technikát leggyakrabban makrók írásakor használjuk. Clojure-ben, a Lisp nyelvektől eltérően kétféle "idézőjel" is szerepel:

A Quote nem csinál mást, mint visszaadja a kiértékeletlen kifejezést. Nem hív meg függvényt, nem bontja ki a makrót, de a reader makrók elvégzik a dolgukat:

    user=> '(trampoline #(foo 1000000)) 

    (trampoline (fn* [] (foo 1000000)))     ; a #(...) részt a megfelelő reader makró "kibontja" egy függvénydefícióvá, de ki nem értékeljük

    user=> (quote (trampoline #(foo 1000000)))    ; a ' makró karaktert a megfelelő reader makró "kibontja" a quote speciális formává

    (trampoline (fn* [] (foo 1000000)))

A Syntax quote ezzel szemben másként működik: abban a kifejezésben, amire "ráeresztjük" a Syntax quote-ot, a Syntax quote minden részkifejezésre rekurzívan alkalmazásra kerül. Szimbólumok esetében a szimbólum teljesen feloldott neve (névtér + azonosító) kerül be a Syntax quote eredménykifejezésébe, kivéve, ha a szimbólum nevét kettőskereszt (#) jel követi: ez utóbbi a reader auto gensym funkcionalitása miatt van - a kettőskereszt előtt álló kifejezés egy új, egyedi azonosítót generál, ami megkönnyíti a makrókban például a segédváltozók vagy segédfüggvények deklarálását:

    user=> `(a b c d# d#)
    
    (user/a user/b user/c d__129__auto__ d__129__auto__)   
	
    ; a syntax quote feloldotta az azonosítókat (mivel az user névtérben vagyunk, oda) - a d szimbólumnak pedig egyedi nevet generált

    user=> (ns nevter)   ; névteret váltunk
    nil

    nevter=> `(a b c d# d#)
    (nevter/a nevter/b nevter/c d__149__auto__ d__149__auto__)

    ; a syntax quote tehát mindig "a névtérnek megfelelő" eredményt ad

Nem mindig jó, hogy a syntax quote rekurzívan végigmegy az egész kifejezésfán. Főleg makrók írásakor lehetne hasznos, ha a kifejezés egy részkifejezését mégiscsak kiértékelhetnénk a syntax quote-on belül. Erre szolgál az unquote, aminek a jele a ~ (tilde) karakter:

    user=> (def x 42)
    #'user/x

    user=> (let [x 88, y 33]
              `[x y ~x ~y ~(deref #'x)])    ; #'-vel visszakapjuk az x-szel jelölt Var objektumot, amire a dereferencia függvény a "globális" x értékét adja
    [user/x user/y 88 33 42]

Látható, hogy az unqoute függvényekre, makrókra, valójában tetszőleges részkifejezésre (vektorokra, listákra, stb.) is hívható, ami egy syntax quote hatókörén belül helyezkedik el. Ha úgy helyezünk el egy unquote-ot, ami nincs egy syntax quote hatókörén belül, hibát kapunk:

    user=> ~12
    java.lang.IllegalStateException: Var clojure.core/unquote is unbound. (NO_SOURCE_FILE:0)

Az unquote egy speciális fajtája az unquote splicing, amit a ~@ karaktersorozattal jelezhetünk a readernek:

    user=> (let [lista '(1 2 3 4)
                 vektor [1 2 3 4]
                 map    {1 2 3 4}
                 set   #{1 2 3 4}]
            `[ "A lista elemei: " ~@lista
               "A vektor elemei: " ~@vektor
               "A map elemei: " ~@map
               "A set elemei: " ~@set ] )
               
    ["A lista elemei: " 1 2 3 4 "A vektor elemei: " 1 2 3 4 "A map elemei: " [1 2] [3 4] "A set elemei: " 1 2 3 4]

Az unquote splicing tehát "kicsomagolja" az unquote eredményét.

Függvénydefiníció: az fn forma

Függvényeket az fn speciális formával készíthetünk:

    user=> (fn [x] (* x x))   ; négyzetre emelős függvény
	
    #<user$eval200$fn__201 user$eval200$fn__201@8d5aad>

Látható, hogy a függvényobjektum önmagában nem sokat ér, amíg meg nem hívjuk:

    user=> ((fn [x] (* x x))  4)
	
    16

A függvénynek lehet neve is, és az értéke minden további nélkül eltárolható egy Var-ban vagy egy lokális változóban:

    user=> (ns user
          (:use [clojure.contrib.math :only (sqrt) ]))
    nil

    user=> (def negyzet (fn [x] (* x x)))
    
    #'user/negyzet

    user=> (let [gyok (fn [x] (clojure.contrib.math/sqrt x))]
             (gyok (negyzet 4)))

    4

A függvények túlterheltek is lehetnek, és mindegyik függvénynek lehet legfeljebb egy darab olyan túlterhelt változata is, ami tetszőleges számú paramétert fogad (& jel):

    user=> (def fgv (fn ([x] x)
                    ([x y] y)
                    ([x y & z] z)))    ; változó paraméterlista: z-nek nem kötelező értéket adni, de ha van, akkor egy listába fogja össze a kapott paramétereket
    #'user/fgv

    user=> (fgv 2)
    2
    user=> (fgv 2 4)        ; többértelmű: ha a ([x y] y) túlterhelés nem szerepelt volna, ([x y & z] z)-t hívná
    4
    user=> (fgv 2 4 1 5)    ; z értéke most (1 5)
    (1 5)

Szükségünk lehet egy függvényen belül hivatkozni a függvényre. Ezt megtehetjük úgy is, hogy egy névhez kötjük a függvényt, de a függvény nevét a definícióján belül is megadhatjuk úgy, hogy csak a függvényen belülről látszódjon, ehhez írjuk a függvény nevét a fn szimbólum után.

A makró karakterekről szóló részben volt szó a névtelen függvények rövid írásmódjáról. Azaz, a #(...) forma beolvasáskor automatikusan a megfelelő (fn (...)) hívássá egészítődik ki, ilyen módon még rövidebb névtelen függvényeket írhatunk. A függvény paraméterére ekkor a % jellel hivatkozhatunk, a második paraméterre a %2 jellel, a harmadikra %3, stb.

    ;; a #(+ % 2) ugyan az, mint a (fn [x] (+ x 2)), csak rövidebb
    user=> (map #(+ % 2) [1 2 3])
    (3 4 5)
    
    user=> (map #(/ (+ %1 %2) 2) [1 2 3] [3 4 5])
    (2 3 4)
    

A nyelv bővíthetősége

Az ebben a fejezetben bemutatott vezérlési- és egyéb szerkezetek, a Clojure nyelv speciális formáiból (special forms) kerültek ki. A speciális formák primitíveknek tekinthetők, hiszen minden Clojure függvény és makró leírható speciális formák segítségével. Ez nem azt jelenti, hogy minden esetben a speciális formák használata a legtermészetesebb egy adott probléma megoldására. Függvények definiálására például léteznek jóval kényelmesebb módok is az fn-nél. Egy ilyen lehetőség a defn függvény, amivel akár dokumentációs stringet is megadhatunk. Az előző rész példájában látott fgv függvény definíciójával lényegében ekvivalens tehát:

    user=> (defn fgv
              "Példa függvény"
              ([x] x)
              ([x y] y)
              ([x y & z] z))

Hasonlóan, ciklusok írása helyett gyakran érdemesebb a for vagy a doseq makrókat használni, if-fel történő elágazások helyett a case makrót, stb. A nyelv rugalmassága miatt a fenti vezérlési szerkezetek tetszőlegesen bővíthetők saját makrók, függvények írásával is.

Makrók definiálása a defmacro makró hívásával történik. A makró lényegében olyan függvény, ami a kódot, mint adatszerkezetet kapja meg paraméterül, ezen végezhet transzformációkat, és végül kódot ad vissza eredményül is. Egy makró azonban nem használható minden esetben függvényként, például csak meghívni tudjuk, de nem kezelhetjük értékként, mint például egy függvényt.

Ahhoz, hogy egy makróban könnyen előállíthassuk a kódot reprezentáló adatszerkezetet, az úgynevezett syntax quote-ot alkalmazhatjuk. Ez azt jelenti, hogy az idézőjelezéshez hasonlóan a ` (backtick) karakter után írt adatszerkezet nem hajtódik végre, viszont egy ilyen módon kiidézőjelezett szakaszon belül a ~ (tilde) karakter után írt kód igen. Ezzel a módszerrel könnyen előállíthatunk olyan, nagy méretű adatszerkezeteket, amiknek mindig csak egy mélyen beágyazott, kicsi része változik meg. Jellemzően egy makróhívás ilyen kódot eredményez.

Egy másik fontos kérdés a makrókkal kapcsolatban az, hogy hogyan kerüljük el, hogy egy makró által előállított kódban véletlenül olyan azonosítókat alkalmazzunk, amelyek a makró hívásának helyén már léteznek, és ezáltal azt eltakarjuk, vagy véletlenül módosítjuk. A probléma megoldására a Clojure nyelv az úgynevezett higénikus makrók rendszerét alkalmazza. Ha egy szimbólum neve # jellel végződik, akkor a reader oda egy minden esetben egyedi szimbólum nevet helyettesít be. Ehhez a gensym függvényt használja, ami minden híváskor egyedi értéket ad vissza.