Scheme

Scheme

Paradigmatöbbelvű: funktionális, procedurális, meta
Jellemző kiterjesztés.scm, .ss
Megjelent1975
TervezőGuy L. Steele, Gerald Jay Sussman
Utolsó kiadásR6RS (ratified standard) (2007)
Típusosságerősen típusos, dinamikus kötés
DialektusokT
Megvalósításoksok, lásd még: Category:Scheme implementations
Hatással volt ráLisp, ALGOL, MDL
Befolyásolt nyelvekCommon Lisp, Dylan, EuLisp, Haskell, Hop, JavaScript, Kernel, Lua, R, Ruby, Clojure , Racket
Weboldal

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 (SRFI = Scheme Request for Implementation) – ma már a legtöbb Scheme implementáció tartalmaz több-kevesebb SRFI-megvalósítást.

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 Archiválva 2005. március 2-i dátummal a Wayback Machine-ben (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.

A Scheme nyelv jellemzői

Az alábbi leírás nem teljes, leginkább az eredeti Lisptől való eltéréseket hangsúlyozza.

A Scheme-ben is a Lispben megszokott teljesen zárójelezett prefix kifejezésforma használatos, például az 1 + (2 * 3) (matematikai, vagy más nyelvekben szokásos) kifejezést a

(+ 1 (* 2 3))

formában írhatjuk le.

Az adatok között megjelenik a #t és #f logikai érték, amely az igaz és a 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 például 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á, például 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.