Scheme
A Wikipédiából, a szabad enciklopédiából.
A Scheme programozási nyelv a Lisp nyelvcsalád egyik tagja, illetve a Lisp egyik nyelvjárása (dialektusa). A Scheme tervezői arra törekedtek, hogy a Lispből minden fölösleges tulajdonságot kigyomláljanak, és egy egyszerű, kevés szabállyal leírható, de erőteljes nyelvet hozzanak létre. Például a Scheme-ben egyetlen egységes névtérben találhatók a függvények és a változók. A nyelv precíz leírása és egyszerűsége miatt az oktatásban jól használható; alkalmazását valós problémák megoldására viszont némileg akadályozza, hogy kevés a szabványos eljárás, nincsenek kiterjedt szabványos könyvtárak. Ezt a hiányosságot azonban az utóbbi években igyekeznek pótolni (SFRI = Scheme Request for Implementation) – ma már a legtöbb Scheme implementáció tartalmaz több-kevesebb SRFI-megvalósítást.
[szerkesztés] A nyelv nagyon rövid története
A Scheme nyelv első leírását Gerald Jay Sussman és Guy Lewis Steele Jr. alkotta meg 1975-ben. A nyelvet azóta a javított jelentések (Revised Reports) írják le, melyekből az első 1978, a legutóbbi, az R5RS 1998-ban jelent meg. Az IEEE is szabványosította a nyelvet (IEEE Std 1178-1990). A legújabb szabvány, azaz az R6RS létrehozása éppen folyamatban van. A nyelvet 1981-ben kezdték oktatásra használni az MIT-n, a Yale-en és az Indiana University-n. 1984-ben jelent meg a SICP (Structure and Interpretation of Computer Programs) első kiadása. Ez egy meghatározó jelentőségű számítástechnikai tankönyv amely a Scheme nyelvet használja.
[szerkesztés] A Scheme nyelv jellemzői
(Egyelőre nem teljes a leírás, főleg az egyedi jelleget, illetve más Lisp nyelvjárásoktól való eltérést hangsúlyozzuk. Javasoljuk, hogy a tisztelt olvasó tanulmányozza a Lisp bejegyzést is.)
A Scheme-ben is a Lispben megszokott teljesen zárójelezett prefix kifejezésforma használatos, pl. az
1 + (2 * 3)
(matematikai, vagy más nyelvekben szokásos) kifejezést az
(+ 1 (* 2 3))
formában írhatjuk le.
Az adatok között megjelenik a #t és #f logikai érték, amely az igaz és hamis értéket jelöli. Fontos, hogy az üres lista (nil) nem számít hamis értéknek, mint más Lispeknél.
Változó létrehozása:
(define d 5)
(define z '(1 2))
Értékadás:
(set! z #t)
Általában a mellékhatással rendelkező eljárások neve felkiáltójellel végződik.
Eljárások létrehozása:
(define (f x) (+ x d))
A Scheme-ben a Lisp más nyelvjárásaiban szokásos függvény elnevezés helyett az eljárás szót használják.
A Scheme-ben a Lisp történetében szokatlan módon a lexikális hatókör szabálya érvényesül (mint az Algolban vagy A Pascalban. Ez azt jelenti, hogy az eljárások hívásakor a definíciójukkor érvényes környezetben keresik a változó értékét, nem pedig a hívási környezetben. (A környezet nem más, mint változó-érték kötések halmaza.) A lexikális hatókörre mintapélda (az előző példákat is figyelembe véve):
(let ((d 1)) (f d))
Ez a kifejezés a 6 értéket adja vissza, mivel az f eljárás definíciós környezetében a d változó értéke 5. Ha a dinamikus hatókör lenne érvényben (mint például a Common Lispben a globális változók esetén) akkor a let kifejezés értéke 2 lenne.
A Scheme-ben az adatok típusa nem a változókhoz, hanem az értékekhez van kötve, azaz a Scheme dinamikus típusokat használó nyelv (mint a többi Lisp):
(define v 1)
Itt az v változó értéke 1, amely egész típusú, de a
(set! v '(1 2))
értékadással már az (1 2) lista lesz az értéke.
A Scheme-ben létrehozott objektumok (értékek) élettartama nem korlátozott, tehát ha egy eljárás lokális változóját valahogy el tudjuk érni, akkor annak értéke megmarad a hívás után is:
(define (osszeado z) (lambda (x) (+ z x)))
(define plusz2 (osszeado 2))
Az osszeado eljárással egy tetszőleges számmal növelő eljárást tudunk létrehozni. Annak ellenére, hogy a program futása – a kifejezések kiértékelése – közben újabb és újabb tárterületeket használ el ez általában nem jelenti azt, hogy gyorsan elfogy a rendszerünk összes tárkapacitása: ha egy érték többé már nem elérhető a rendszer az általa elfoglalt helyet újrahasznosítja. Ez a folyamat a szemétgyűjtés (ilyet más nyelvek is használnak, mint pl. a Perl, Python vagy a Java).
Az eljárások hívásánál a paraméterek átadása mindig érték szerint történik (call-by-value) (mint általában a Lispben, a C-ben, Pascalban, de például nem úgy, mint a Haskellben, ahol ún. szükség szerinti átadásról beszélünk (call-by-need)). Ezt a paraméterátadási stratégiát mohó kiértékelésnek is nevezzük, ahol a paraméterként megadott kifejezések az eljárás hívása előtt kiértékelődnek – ellentétben a lusta kiértékeléssel, ahol a paraméterként átadott kifejezés csak akkor értékelődik ki, amikor szükség van erre az értékre. Példa:
(define (moho x y) x)
(moho 1 (/ 1 0))
A moho függvény hívásakor hibajelzést kapunk, holott az y paraméter értékét nem használjuk a kifejezésünkben – de az érték szerinti paraméterátadás szabálya szerint a ki kell értékelni a (/ 1 0) kifejezés értékét a moho hívása előtt, és ez hibajelzéshez vezet.
A Scheme nyelvben az eljárások is első osztályú polgárai a nyelvnek, azaz eljárást át lehet adni paraméterként, használható visszatérési értékként (mint azt az osszeado eljárásnál láttuk), adatstruktúra része lehet. Névtelen függvényeket a lambda szimbólummal definiálhatunk:
(lambda (x y) (+ x y 1))
Ez az eljárás összeadja a két paraméterét és még egyet ad hozzá, pl. a
((lambda (x y) (+ x y 1)) 2 3)
kifejezés a 6 értéket adja vissza.
Más Lisp dialektusokkal ellentétben az eljárás- (vagy függvény)hívás első tagja egy tetszőleges kifejezés, amely értéke eljárás kell, hogy legyen:
(define (buta-pelda x y) ((if (> x 0) + *) x y))
Ez az eljárás összeadja két paraméterét ha x nagyobb mint 0, egyébként összeszorozza őket. (A Common Lisppel ellentétben itt nincs szükség a function speciális formára, vagy a funcall függvényre, mivel egy névtérben laknak az eljárás-értékek az egyéb adatértékekkel.)
A Scheme nyelv talán egyik legérdekesebb fogalma a folytatás (continuation). A folytatás egy adott számítás menetének jövőjét jelenti, azaz egy program kifejezése kiértékelésének egy adott pillanatától számított további működését. A folytatás a más nyelvekben megtalálható goto, exit, return, catch névvel illetett utasítások, illetve mechanizmusok általánosítása. A folytatás megvalósítására a call-with-current-continuation (szokásos rövid neve: call/cc) eljárás szolgál.
A call-with-current-continuation eljárást egy egyparaméteres eljárásparaméterrel (általában lambda-kifejezéssel) kell meghívni. Az átadott eljárásnak a paramétere egy szökési eljárás, amellyel az átadott eljárásból kiléphetünk.
Ebben a példában a call-with-current-continuation eljárás egy egyszerű használatát mutatjuk be:
(call-with-current-continuation (lambda (exit) (for-each (lambda (x) (if (negative? x) (exit x))) '(54 0 37 -3 245 19)) #t))
Ezen kifejezés értéke a lista első negatív eleme lesz, azaz -3. (A for-each eljárás az első paraméterként adott eljárást hívja meg sorban a második paraméterében adott lista minden elemére.) Itt egyszerűen egy strukturált kilépésként használtuk a szökési eljárást. Mivel ez a folytatás egyenrangú adatnak számít a Scheme-ben, ezért visszatérési értékként használhatjuk, eltárolhatjuk, esetleg később többször is meghívhatjuk mint bármelyik más függvényt: ezzel többféle, a szokástól eltérő vezérlési struktúrát hozhatunk létre.