Bár sokmindent csinál a Lisp máshogyan, mint az egyéb, elterjedt programozási nyelvek (legyenek azok akár imperatívak, akár funkcionálisak, akár deklaratívak), mégis van a Lisp nyelvcsaládnak egyetlen jól meghatározható tulajdonsága, amely alapvetően különbözteti meg más nyelvektől: ez pedig a programozhatóság.
De mit is jelent egy programozási nyelv programozhatósága? Azt, hogy a nyelvet a nyelv eszközeivel bővíthetjük, változtathatjuk. Egy valódi Lisp program általában úgy épül fel, hogy először egy csomó, az adott probléma megoldásának leírásában hasznos nyelvi konstrukciót vezetünk be, majd utána a tényleges megoldást már ezen az így kiterjesztett "Lisp'" nyelven fogalmazzuk meg. Az alábbiakban kiderül, hogyan is történik ez a gyakorlatban.
Amikor új nyelvi elemekkel akarjuk bővíteni a Lispet, valójában olyan programokat kell írnunk, amelyeknek a bemenete és a kimenete is programkód. Természetesen megtehetjük például C++ nyelven is, hogy írunk egy programot, aminek a bemenete és a kimenete is C++ nyelvű kód, majd ezt, mint elő-fordítót, ráeresztjük a programunkra a tényleges C++ fordító előtt. Mégsem csinál szinte senki ilyet. Mi ennek az oka?
Elsősorban az, hogy egy ilyen programnak nagy az infrastrukturális igénye. Be kell parzolnunk a C++ programot valamilyen belső reprezentációba, majd végre kell hajtani a teljes típuskalkulust, és csak ezután jön a lényegi lépés, a program belső reprezentációján végzett transzformáció. Ezekután a parzolás ellentéte következne, amivel újra megkapjuk szöveg-formátumban a feldolgozás eredményét. Egy következő lépés lenne még mindennek az integrációja a build-folyamatba.
Ezzel szemben a Lisp programoknak van egy jóldefiniált, standardizált reprezentációja, a fordító- és interpreterprogramok pedig felületet nyújtanak, amin keresztül beregisztrálhatjuk átalakító-programjainkat, hogy azok a kódfa megfelelő rész-fáira automatikusan aktiválódjanak.
A szövegesen S-kifejezésekkel reprezentált programszövegből a beolvasó legelőször egy cons-cellákból felépített, láncolt listákkal reprezentált fát épít, és csak utána történik bármiféle értelmezés, fordítás. Ez azért fontos, mert így a beolvasás után már ugyanazokkal az eszközökkel manipulálhatjuk a forráskódot, mint amikkel egyébként bármilyen más listát manipulálunk. Lássunk egy példát: a következőkben egy elágazás S-kifejezését látjuk, alatta pedig azt a kódot, ami futásidőben létrehozza azt a fát, amit a beolvasó is létrehozna a fenti kifejezésből:
Az olvashatóság érdekében előszöris a QUOTE makró helyett használhatjuk az aposztróf karaktert, másrészt az aposztróf listákra is működik, és pont a fenti kóddal ekvivalens a működése. A fenti példa második kifejezése tehát átírható az alábbi formára, amiből rögtön látszik is, hogy mennyire közeli a kapcsolat a program és belső reprezentációja között:
A fentiek értelmében tehát egy makró egy speciális függvény, amelynek a bemenete és a kimenete programkód-fa. Makrókat ennek megfelelően a függvényekhez hasonlóan definiálhatunk, a DEFMACRO makró segítségével. Ahogy a függvényeknek formális paraméterei vannak, úgy a makróknak paraméter helyett szintaxisuk van, vagyis a programfának egy alakja, amiben az egyes részfák a makróalkalmazás helyének megfelelő értéket vesznek fel. Ezt a programfa-alakot a függvények formális paramétereihez hasonlóan specifikáljuk:
A kifejezés értékének egy fának kell lennie, amely értelmes Lisp programot reprezentál (természetesen további makróalkalmazásokat is tartalmazhat). Az alábbi példa szekvenciává lapítva valósítja meg a fordítási időben ismert iterációt tartalmazó ciklust:
A szintaxis-specifikációban a &body kulcsszó azt jelenti, hogy a második paramétertől kezdve tetszőleges számú paraméter megengedett, és ezek összessége lesz, mint lista, a body formális paraméter aktuális értéke.
Fontos kihangsúlyozni, hogy a makró nem a paraméterek értékét kapja meg, hanem a programkód-fát. Hiszen ha nem így lenne, akkor a fenti makró a kimenetére n-szer csak a törzsének értékét tudná írni, ami nyilván nem azonos azzal, mintha n-szer megismételjük a törzsben szereplő kifejezés kiértékelését. Ez természetesen azt is jelenti, hogy az n értéke literális szám-érték kell hogy legyen. Ugyanakkor vegyük észre azt is, hogy makrókból szabadon hívhatunk tetszőleges függvényeket, és ezek a függvények fordítási időben fognak lefutni.
A fenti makródefiníció alapján a további két program teljesen ekvivalens (sőt, a fordító az elsőből közvetlenül a másodikat generálja):
Ha szeretnénk ellenőrizni, hogy a makrónk valóban így viselkedik, a MACROEXPAND-1 nevű függvénnyel kiértékelhetünk egy makróalkalmazást anélkül, hogy a makró kimenete programként értelmeződne. Az alábbi kifejezés értéke tehát a fenti, ötsoros PROGN:
Mivel általában a makrók jelentős részét teszi ki kódfa-vázba való behelyettesítés, az imént ismertetett '-hoz hasonlóan egyéb, speciális szintaxis segíti az ilyenek olvasható leírását.
A fordított aposztróf, `(backquote) viselkedése megegyezik az ismertetett aposztróféval, egyetlen kivétellel: ilyenkor használható belül a vessző, ,(unquote) is, ami az aposztróf ellentéte: az őt követő kifejezést mégis kiértékeli, és az eredményt teszi az éppen épülő listába. Egy speciális esete a ,@(splicing unquote), amikor a kifejezés értéke egy lista, és mi azt szeretnénk, hogy ez belapítva kerüljön a kimenetbe.
Az előbbi makrónk így átírható a következő, sokkal olvashatóbb és kényelmesebb formába:
Tegyük fel, hogy sokszor kell változókat nil-re állítanunk a programunkban, és ezért ezt szeretnénk lerövidíteni. Ekkor definiálhatjuk pl. a következő makrót:
Bevezethetünk a Fortran programozási nyelv aritmetikai if-jéhez hasonló szerkezetet:
Utóbbira példa a when-bind makró:
Létrehozhatunk olyan nyelvi konstrukciókat, amelyek hasonlítanak a C# using kulcsszavához. Ilyen pl. a with-open-file, amely megnyit egy fájlt, hozzáköti egy változóhoz, kiértékeli a törzsében lévő kifejezéseket, majd lezárja a fájlt. Utóbbi akkor is megtörténik, ha a törzs kiértékelése során kivétel keletkezik, vagy bármi egyéb módon kiugrik a vezérlés a törzsből. Ezt az unwind-protect speciális operátor használatával éri el, amelynek megadható két blokk, melyek közül a második megfelel egy Java-ból vagy C#-ból ismert finally blokknak.
A setf beépített makró a setq-nak egy általánosítása: az első argumentuma nem csak változó lehet, hanem egy hívás is. Egy (setf x y) hívást értelmezhetünk a következőképpen: “x kiértékelésének eredménye legyen y”. Ennek eléréséhez a setf megnézi az x-et, és ha az csak egy szimbólum, akkor egy setq-t készít. Azonban ha változó helyett egy olyan függvényhívást talál ott, ami valamilyen adat kiolvasását végzi el, akkor a függvény “inverzét” írja be, ami ugyanezen adat írását teszi meg. Pl. a (setf (car lst) 480) hívás az alábbira bomlik ki: (progn (rplaca lst 480) 480). (Az rplaca függvény a lista első elemét átállító függvény.) Minden gyakran használt hozzáférő függvénynek van előre definiált inverze, pl. a car, cdr, nth, aref, get, gethash függvényeknek, illetve a defstruct által létrehozott kiolvasó függvényeknek is. Általánosított változóknak hívják azokat a kifejezéseket, amelyek lehetnek a setf első argumentumai. Ez valamelyest hasonlít a C++ referenciáira: ha pl. egy int-eket tároló adatstruktúránknak egy hozzáférő függvényének a visszatérési típusa int helyett int&, akkor az azt jelenti, hogy a függvényhívás szerepelhet egy értékadás bal oldalán. Azonban az általánosított változók -a C++ referenciákkal ellentétben- olyankor is használhatóak, ha valamilyen bonyolultabb művelet szükséges az adatstruktúránk frissítéséhez. Gondoljunk például a set körüli problémákra: A find függvény olyan iterátort ad vissza, amelyen keresztül nem lehet módosítani a megtalált értéket. (A C++ szabvány szerint ugyan van egy olyan overloadja a find függvénynek, aminek a visszatérési típusa nem látszik const_iterator-nek, azonban furcsa módon mégsem lehet rajta keresztül módosítani a mutatott értéket.) Ennek az az oka, hogy nincs jó megoldás annak elérésére, hogy ha a mutatott értéket valaki megváltoztatja az iterátoron keresztül, akkor lefusson az a kód, ami frissíti a set által belsőleg használt keresőfa szerkezetét. Lispben könnyedén definiálhatnánk a find inverzét, és akkor használhatnánk setf-ben.
A setf-re építve írhatunk egyéb makrókat is, pl.:
Ezt a makrót ugyanúgy használhatjuk, ahogyan a defun-t haszáljuk, azonban az így létrejött fügvényeknek van egy extrájuk: minden híváskor megnézik, hogy nem volt-e esetleg már korábban meghívva a függvény ugyanezekkel a paraméterekkel, és amennyiben igen, akkor most már nem fut le, hanem rögtön visszakapjuk az akkor kiszámolt és eltárolt értéket. (Egyébként hasonló dolgot csinálhatunk más nyelvekben is: C++: http://slackito.com/2011/03/17/automatic-memoization-in-cplusplus0x/ . Azonban az itt leírt változat (azon túl, hogy nem illeszkedik bele olyan szépen a nyelvbe, mint a Lispes megoldás) nem kezeli a rekurzív függvényeket. A Python megoldás viszont egészen elegáns: http://wiki.python.org/moin/PythonDecoratorLibrary#Memoize )
Az anaforák olyan szavak, amelyek visszahivatkoznak a szövegnek egy korábbi szavára. Az anaforikus makrók olyan változóhoz kötnek hozzá értéket, amelynek nem a makró használatakor adjuk meg a nevét, hanem a makró definíciójakor lett rögzítve.
Előfordulhat, hogy valami hosszú számolás után csak akkor szeretnénk további műveleteket végezni az eredménnyel ha az nem null lett. Ekkor el kell tárolnunk egy változóba a számolás eredményét, egy if-fel ellenőrizni, hogy nem null-e, majd elvégezni a további műveleteket, ahol hivatkozunk az eredményt tároló változóra. Ez kinézhet pl. a következőképpen:
Az aand makró az and-re hasonlít, de a második argumentumtól kezdve az it változóhoz mindig hozzá van kötve az előző argumentum eredménye. Pl. egy adatbázisból való lekérdezésnél megspórolhatunk sok egymásba ágyazott, null értékeket kezelő if-et:
A lambda szimbólummal bevezethetünk névtelen függvényeket. Ezek általában nem lehetnek rekurzívak, mivel a törzsükben nem tudjuk leírni a nevüket, mivelhogy nincs nekik. Viszont az alambda makró használata esetén a self szimbólum értéke mindig az éppen definiálás alatt levő függvény lesz. Pl. a faktoriális függvényt definiálhatjuk az alambda-val rekurzívan:
Az alrec makró ahhoz hasonlít, mint amikor a Haskell foldl függvényének egy lambdát adunk át, azonban a lambdával létrehozandó függvény paramétereinek nevei kötöttek, és így rövidebb és tisztább kódot kapunk:
A fent ismertetett, "függvényhívás-szerűen" alkalmazható makrókon kívül léteznek másfajta makrók is. Programozható például maga a beolvasó is, a megismert ' és ` karakterek speciális értelmezése is így van implementálva (tehát ha nem lennének, akár magunk is bevezethetnénk a nyelvbe). Egy másik lehetőség, hogy konkrét szimbólumok előfordulásához kössünk makrókat, így aztán megoldható, hogy ne (FOO), hanem FOO formában használhassuk a makrót (persze ilyenkor értelemszerűen nem lehetnek paraméterei).
Ezeknek, és az egyéb makró-lehetőségeknek a bemutatása nem képezi ezen bevezető tárgyát, az érdeklődő a szakirodalomban olvashat utánuk, például Paul Graham On Lisp című könyvében.