A LISP programozási nyelv

A Lisp nyelv szintaxisa

A Lisp kifejezésorientált nyelv. Ez annyit tesz, hogy a legtöbb nyelvvel ellentétben a Lispben nincs különbség „kifejezések” és „parancsok” között, minden adatot és utasítást kifejezések formájában írunk le. Amikor az értelmező kiértékel egy kifejezést, eredményként egy értéket (vagy értékek listáját) kapunk, amely aztán beágyazható újabb kifejezésekbe, vagy akár kifejezésként maga is kiértékelhető.

McCarthy 1958-as cikke két kifejezéstípust különböztetett meg: Az S-kifejezéseket (S-expression, sexp), vagyis szimbolikus kifejezéseket, és az M-kifejezéseket, vagyis meta kifejezéseket, amelyek S-kifejezéseket alakítanak át újabb S-kifejezésekké. Ez utóbbi típus azonban nem talált támogatókra, és a legtöbb mai Lisp változat S-kifejezéseket használ mind az adatok, mind a kód manipulálásához.

Sokan kritizálták a zárójelek túltengését az S-kifejezésekben (ezt tükrözi a Lisp mozaikszó ironikus kifejtése, a „Lots of Irritating Superfluous Parentheses”, azaz „rengeteg irritáló és felesleges zárójel” is), de az igazság az, hogy az egyszerű, rendkívül szabályos szintaktika adja a Lisp legfőbb erejét: a lehetőséget, hogy a kódot adatként kezeljük.

Az, hogy mindent kifejezésekkel írunk le, nagy rugalmasságot biztosít a nyelvnek. Mivel a Lispben magukat a függvényeket is listákként definiáljuk, akként is tudjuk kezelni őket, és nem okoz technikai nehézséget programot generáló programok írása (ezt hívjuk metaprogramozásnak). Számos Lisp nyelvjárás ki is aknázza ezt a tulajdonságot, és lehetővé teszi makrók készítését, amelyek tkp. függvényeket generáló, paraméterezhető függvények.

A Lisp-ben egy listát zárójelekkel határolunk, az elemeket szóközzel választjuk el egymástól:

(1 2 "abc")

Ezen lista elemei az 1, 2, és az "abc" értékek. Az értékek típusa implicite adott (azaz nem kell – nem is lehet – külön megadni a típusukat): az első kettő egész, a harmadik pedig szöveges (ún. karakterlánc, string). Az üres listát a () kódsorozattal vagy a nil kulcsszóval írhatjuk le.

A kifejezéseket szintén lista alakban írjuk, mégpedig prefix jelölést használva. A lista legelső eleme egy művelet (angolul form), azaz egy függvény, operátor, makró stb. neve. A lista további elemei adják a művelet argumentumait. A list függvény például visszaadja az argumentumaiból alkotott listát, tehát a

(list 1 2 "abc")

kifejezés az (1 2 "abc") értékké egyszerűsödik. Függvény- vagy operátor-művelet esetén az argumentumok a művelet elvégzése előtt rekurzíve kiértékelődnek, értékük behelyettesítődik az eredeti kifejezésbe, amely csak ezután értékelődik ki, például a

(list 1 2 (list 3 4))

kifejezés értéke (1 2 (3 4)). (Az elemi, tovább már nem egyszerűsíthető értékek, mint az 1, 2, és az "abc" mindig önmagukat adják eredményül.) Vegyük észre, hogy a lista harmadik eleme egy másik lista, a listák tehát egymásba ágyazhatóak.

A számtani műveletek hasonlóan működnek (itt szokatlan lehet a prefix jelölés):

(+ 1 2 3 4)

értéke 10.

Bizonyos rendhagyó műveletek (special form-ok) vezérlési szerkezetként szolgálnak. Az if művelet például három argumentumot vár. Ha az első argumentum nem az üres listát (azaz nem a nil értéket) adja eredményül, akkor a második, ellenkező esetben a harmadik argumentumát értékeli ki. Így tehát a

(if nil (list 1 2 "abc") (list 3 4 "def"))

eredménye (3 4 "def"). Vegyük észre, hogy itt az argumentumok nem értékelődnek ki a művelet elvégzése előtt, mivel nem függvényhívásról vagy operátor alkalmazásáról van szó. Jelen esetben az sem okozna tehát gondot, ha az akkor ágon hibát okozó kifejezés állna.

Egy másik rendhagyó művelet, a defun, függvények definiálására szolgál. Argumentumai rendre a definiálni kívánt függvény neve, a névleges argumentumainak listája, valamint a függvény törzsét alkotó kifejezés (vagy ezek listája), amelynek értéke (kifejezéslista esetén az utolsóé) egyúttal a függvény eredménye is:

(defun f (a b) (list 1 a 3 b))

függvénydefiníció után a (f 2 4) kifejezés értéke (1 2 3 4) lesz.

Párok és listák

A Lisp nyelvben a listák egyszeresen láncolt listák. A lista celláit cons celláknak (sokszor egyszerűen pároknak) nevezzük. Egy cons cella két mutatóból áll, amelyeket car-nak és cdr-nek hívunk. Ezen elnevezések eredete az első Lisp megvalósításig nyúlik vissza, az akkori IBM számítógépeken ugyanis így nevezték a Lisp értelmező által használt hivatkozásfeloldó műveleteket (amelyek elérték a mutatók átal megcímzett memóriaterületet). A cons cellákból tetszőleges adatszerkezet építhető, a leggyakoribb azonban a szabályos lista, amelynek car mutatója a lista első elemére, cdr mutatója pedig a farkára, tehát kivétel nélkül vagy a nil szimbólumra vagy egy másik, egy elemmel rövidebb szabályos listára mutat.

Mint látjuk, a listák nem atomi értékek, hanem cons cellák láncolt sorozatai. Egy listára hivatkozó változó nem más, mint a lista első cons cellájára hivatkozó mutató. Egy lista bejárása a cdr függvény sorozatos meghívásával lehetséges, amellyel tehát a listát alkotó cons cellákat látogatjuk sorra.

Egy zárójelezett S-kifejezés egy ilyen láncolt listát definiál. Ugyanazon lista ábrázolására S-kifejezésként számos lehetőség kínálkozik. Egy cons cellát felírhatunk az ún. pontozott pár jelöléssel (a . b) alakban, ahol a a car, míg b a cdr mutató. Ezt az alakot hívjuk kanonikus alaknak. Egy ebben az alakban felírt lista a (1 . (2 . (3 . (4 . nil)))) formát ölti. Ennek az egyszerűsített írásmódja a (1 2 3 4) forma. A két alak keverhető is, azaz ugyanezt a listát felírhatjuk (1 2 . (3 4)) vagy (1 2 3 4 . nil) alakban is. A pontozott pár alak arra is lehetőséget ad, hogy nem szabályos listát alkotó cons cellákat hozzunk létre: (1 2 . 3): itt az első cella cdr mezője ugyan a második cellára mutat, a másodiké viszont a 3 értékre. Párokat létrehozhatunk a cons beépített függvény segítségével is, amely két argumentumot vár, rendre a car és a cdr mutatók értékét.

A párok sokrétű felhasználhatóságának köszönhetően elterjedt, de téves vélekedés, hogy a Lisp-ben nincs is más adatstruktúra. Valójában azonban a legtöbb Lisp megvalósítás tartalmaz más struktúrákat is, mint például a vektorok (vagy tömbök), hash táblák stb.

Fontosabb lista műveletek

Listák létrehozása a list kulcsszóval történhet. Itt fontos megjegyezni hogy a listák elemeinek típusa implicit adott, nem lehet külön megadni. Például:
(list 1 2 "abc") eredménye, egy 3 elemű lista amiből az első 2 elem integer a 3. elem string típusú.

Elem befűzése a lista elejére a (cons elem lista) kulcsszóval történhet. Itt az elem lehet atom és lista típusú is.

Listák építésére nagyon hasznos lehet az (append args) függvény is. Ez a megadott argumentumokból listát épít. Fontos, hogy az argumentumok csak listák lehetnek.

Listák egyes elemeire a sorszámuknak megfelelő angol (first, second ...) kulcsszavakkal is hivatkozhatunk. A kulcsszavak száma fordító függő. A Common Lispben jelenleg 10-ig engedélyezett.
(first lista) eredménye a lista első eleme.

A lista n-edik elemét az (nth n lista) függvénnyel érhetjük el.

A lista n-edik elemétől a visszamaradó részt az (nthcdr n lista) függvénnyel érhetjük el.

Megosztott struktúra

A Lisp listák, lévén hogy egyszerű láncolt listák, tartalmazhatnak közös, megosztott szakaszokat. Nevezetesen, két (vagy több) listának lehet ugyanaz a farka. Például az alábbi kód hatására

(setq a (list 1 2 3)) (setq b (cons 0 (cdr a)))

az a és b változók rendre az (1 2 3) és a (0 2 3) értéket veszik fel. A (2 3) szakasz közös, ha ez valamilyen módon megváltoztatnánk, mindkét lista értéke megváltozna. Ez a megosztás nagyon kedvező hatással van a teljesítményre és a programok memóriaigényére, ugyanakkor könnyen válhat hibák forrásává, ha olyan függvényeket használunk, amelyek destruktív módon manipulálják a listákat.

A tisztán funkcionális programozás hívei éppen ezért elkerülik a destruktív függvények használatát. A Scheme-ben, amely a Lisp funkcionális jellegére nagy hangsúlyt fektet, a destruktív függvények neve egy-egy figyelemfelkeltő felkiáltójelben végződik, mint például set-car!, amely felülírja egy pár első tagját. A Common Lisp nyelvjárásban a desktruktív függvények jóval gyakoribbak, sőt, a nyelv definiál egy rendhagyó műveletet (a setf-et) is, amely desktruktív függvények létrehozását és használatát hivatott megkönnyíteni.

Önazonos kifejezések és az idézés

Amikor egy kifejezést kiértékelünk, egy másik, többnyire egyszerűbb kifejezést, értéket kapunk. A (+ 2 3) kifejezés például 5-té egyszerűsödik. Bizonyos kifejezések azonban saját magukká értékelődnek ki. Ilyenek a már említett egészek és a füzérek, de ilyen a nil kulcsszó is.

A Lisp-ben lehetőségünk van arra is, hogy egy értéket megvédjünk a kiértékeléstől. Erre szolgál az idézés, azaz a quote rendhagyó művelet, amelyet röviden egy felülvesszővel ( ’ ) is jelezhetünk. Ha például megpróbáljuk kiértékelni a kif kifejezést, akkor vagy az ilyen nevű változó értékét kapjuk, vagy hibát, ha nincs ilyen változó. Ha magát a kif szimbólumot akarjuk felírni, akkor idézetbe kell tennünk: (quote kif) vagy egyszerűen ’kif. Ezzel már könnyen tudunk a nil-hez hasonló önazonos neveket definiálni:

(setq kif ’kif)

Ezután akármilyen mélységben értékeljük ki a kif kifejezést, mindig önmagát fogjuk kapni. (Tehát kif értéke kif, aminek értéke megint csak kif stb.)

A makrókat lehetővé tevő nyelvjárások ennél bonyolultabb idézőjeleket is definiálnak, mint például a részleges idézést (backquote vagy quasiquote, jele a hátravessző, `). Ennek hatása majdnem azonos a hagyományos idézéssel, azt kivéve, hogy egy vesszővel lehetőség van visszaváltani idézett módból kiértékelendő módba egy-egy részkifejezés erejéig, ezzel kényelmesen változókat ágyazhatunk egy nagyobb idézett kifejezésbe.

Az önazonos és az idézett kifejezések alkotják a Lispben a konstansokat. (Bár ez az elnevezés nem pontos, mert a gyakorlatban lehetőség van ezek értékének megváltoztatására is, amely komoly félreértésekre és zavarokra adhat okot. Elképzelhető például, hogy a ’kif kifejezés egy előfordulása – tkp. egy hivatkozás – kiértékeléskor nem kif, hanem más értéket szolgáltat. Ez mindenképp kerülendő.)

A programkód listaszerkezete

Az eddigi példákban szereplő karaktersorozatok szigorúan véve nem Lisp programok, csak azok írott reprezentációi. Amikor beírjuk őket a Lisp értelmezőbe, az elemző (a Lisp esetében a read függvény) azonnal átalakítja őket láncolt listákká és fa struktúrákká. A Lisp makrók ezeken a struktúrákon, nem pedig az írott reprezentáción manipulálnak. A legtöbb más nyelvnél az elemző kimenete szigorúan belső, csak a fordító- vagy értelmezőprogram által elérhető adat. A C-ben ugyan vannak makrók, ám ezek a programszöveg előfeldolgozása (preprocesszálása) során aktiválódnak, mielőtt az elemző megkezdené a munkáját.

Egyszerű Lisp megvalósításokban az értelmező közvetlenül ezt a belső formát dolgozza fel a program futtatásához, azonban a legtöbb létező implementáció a hatékonyság érdekében futtatás előtt egy alacsonyabb szintű byte-kódra (adott esetben gépi kódra) fordítja a függvényeket.

Kiértékelés és a OKK (REPL)-ciklus

A Lisp rendszereket gyakran egy interaktív parancssoron keresztül vezéreljük, amelyet adott esetben kiegészíthet egy integrált fejlesztői környezet (IDE). A felhasználó vagy közvetlenül a parancssorba írja be a kifejezéseket, vagy utasítja a környezetet, hogy tegye meg ezt helyette (mondjuk egy állományból). A Lisp értelmező először beolvassa az adott kifejezést, ezután kiértékeli azt, végül kiírja az eredményt. Ennek megfelelően a Lisp parancssori végrehajtást gyakran "olvasás-kiértékelés-kiírás" vagyis OKK-ciklusnak, angolul "read-eval-print-loop"-nak, REPL-nek hívjuk.

A ciklus három ütemének egy-egy alapvető Lisp függvényt lehet megfeleltetni:

A read függvény beolvassa egy S-kifejezés írott reprezentációját, eredményként annak belső ábrázolását adja. Ha például bemenetként a (+ 1 2) szöveget kapja, egy listát leíró, cons cellákból álló láncolt struktúrát ad eredményül, amelynek első eleme a + szimbólum, második és harmadik eleme pedig rendre az 1 és 2 egészek. Ez történetesen érvényes programkód is, azaz kiértékelhető, mert az első helyen álló szimbólum egy operátor, amely alkalmazható a további elemekre, mint argumentumokra.

Az eval függvény kiértékeli az argumentumaként kapott listaszerkezetet, egy újabb struktúrát adva eredményül. Ez technikailag nem feltétlenül értelmezést jelent, mivel a Lisp megvalósítások programkódot először gyakran gépi kóddá fordítják, majd az így kapott kód futtatását a processzorra bízzák. A gyakorlatban azonban kényelmes értelmezésként gondolni rá: elsőként rekurzíve kiértékelődnek az argumentumok, majd ezek értékére alkalmazzuk a lista fejében hivatkozott függvényt vagy operátort, jelen esetben az összeadást. Az eredményként kapott érték (itt 3) a teljes kiértékelés végeredménye is egyben.

A print függvény feladata az argumentuma megjelenítése a felhasználónak. Egyszerű értékek esetén ez a feladat triviális, bonyolultabb struktúrák, listák esetében azonban a teljes struktúra bejárását igényli.

Ezen három függvény megléte esetén egy primitív OKK-ciklus írása nagyon egyszerű:

(loop (print (eval (read))))

ahol a loop rendhagyó művelet újra és újra kiértékeli az argumentumaként kapott kifejezést, a végtelenségig.