A Schemeprogramozá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 SICPArchiválva2005. 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(*23))
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:
(defined5)(definez'(12))
É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(fx)(+xd))
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((d1))(fd))
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):
(definev1)
Itt az v változó értéke 1, amely egész típusú, de a
(set!v'(12))
é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:
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(mohoxy)x)(moho1(/10))
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(xy)(+xy1))
Ez az eljárás összeadja a két paraméterét és még egyet ad hozzá, például a
((lambda(xy)(+xy1))23)
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-peldaxy)((if(>x0)+*)xy))
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:
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.