Lisp je jazykem mnoha dialektů -- existuje totiž ohromné množství různých interpreterů a aplikačních programů, které dávají uživateli možnost si dodefinovat své vlastní příkazy právě v Lispu (například známý Unixový textový editor Emacs), a jak už to tak bývá, každý, kdo si programoval svůj interpreter, se rozhodl, že si do jazyka přidá to, co mu tam chybí a že upraví věci, o kterých si myslí, že by se daly udělat lépe. Naštěstí všechny tyto dialekty mají mnoho společného, a tak si tu nadefinujeme náš vlastní velice jednoduchý dialekt, který bude obsahovat více méně jenom ono společné "jádro jazyka", a nazveme ho Ksp-Lisp . Kompletní interpreter Ksp-Lispu si můžete stáhnout z našeho archívu na Internetu (http://atrey.karlin.mff.cuni.cz/ksp).
Veškerá data jsou v Ksp-Lispu uložena jako objekty. Každý objekt má svůj typ a svoji hodnotu (což může být i odkaz na nějaký jiný objekt, jak uvidíme za chvíli). Typů existuje jen několik málo:
nil
.
(
', `)
',
`"
', `.
' ani mezeru a
neskládá se pouze z číslic) je přiřazen právě jeden objekt typu symbol, který
obsahuje odkaz na jeden libovolný objekt (nebo speciální hodnotu
undefined, pokud mu ještě nebyl žádný odkaz přiřazen).
`+
', `+1
',
`define
' či `nil
' jsou
správně utvořená jména symbolů, symbol `nil
'
standardně ukazuje na objekt nil.
a
, druhá b
(v některých
dialektech car
a cdr
).
Páry a nil
je zvykem používat k tvoření seznamů.
Prázdný seznam se vždy vyjadřuje objektem nil
,
neprázdný seznam pak párem, jehož a
ukazuje na první
prvek seznamu a b
na zbytek seznamu. Seznamy se
obvykle zapisují jako posloupnosti objektů uzavřené v závorkách, přičemž
jednotlivé objekty jsou odděleny libovolnou posloupností mezer, tabulátorů a
konců řádků -- tak například (+ 1 (2))
je tříprvkový
seznam, jehož prvním prvkem je symbol +
, druhým
číslo 1
a třetím seznam obsahující jako svůj jediný
prvek číslo 2
.
Tato struktura bude reprezentována párem, jehož a
bude ukazovat na symbol +
a
b
na druhý pár, jeho a
bude ukazovat na integerový objekt 1
,
b
na třetí pár, který bude mít
v a
uložen odkaz na čtvrtý pár
(a
ukazuje na integer 2
,
b
na nil
) a
v b
odkaz na nil
.
()
je jen jiný název pro
nil
.
Mimo `klasických' seznamů můžete vytvářet i zapeklitější struktury
-- "nekonečné" cyklické seznamy (stačí, aby b
posledního prvku ukazovalo na některý z prvků předchozích) nebo třeba
binární stromy (listy reprezentujeme čísly nebo symboly, vnitřní vrcholy
pak páry, jejichž a
se bude odkazovat na levý
podstrom a b
na podstrom pravý).
Důležitým rozdílem oproti klasickým procedurálním jazykům je, že v Lispu nikdy přímo nealokujete paměť (a tím pádem ani neuvolňujete) -- jakmile jakýkoliv objekt vznikne, interpreter pro něj sám nějaký kousek paměti přidělí a když už objekt nebude přístupný (to znamená, že se k němu nepůjde nijak dostat pomocí symbolů a odkazů mezi objekty), automaticky paměť uvolní k dalšímu použití (tomuto procesu se říká garbage collection [sbírání smetí]).
A jak se v Lispu programuje?
Inu, je to funkcionální jazyk, takže se všechny programy skládají z funkcí.
Každá funkce dostává své parametry (což jsou vlastně jen odkazy na objekty),
vrací nějakou hodnotu (opět odkaz) a případně způsobí nějaký postranní
efekt (side-effect), tedy založí, zruší čí změní nějaké globálně viditelné
objekty nebo třeba něco vypíše do výstupu. Každé volání funkce se zapíše jako
seznam typu (f 1 2 3)
, tedy prvním prvkem je funkce,
která se má zavolat (obvykle symbol ukazující na nějaký seznam, jenž je
definicí funkce) a všechny ostatní prvky se vyhodnotí jako parametry této
funkce (mohou to být třeba opět volání funkcí popsaná seznamy).
Každý interpreter Lispu má dva módy -- mód interaktivní
(v tom přímo zadáváte funkce a on je vyhodnocuje a obratem vypisuje výsledky;
velice šikovné pro ladění programů) a mód dávkový (tomu předáte nějaký soubor
a on jej zpracuje, jako by byl řádek po řádku zadán interaktivně; tak se
spouští již hotové programy). Nastartujete-li interpreter
Ksp-Lispu, objeví se vám prompt ("> ") oznamující,
že se s vámi komunikuje interaktivně. Když pak zadáte
(+ 1 2)
, Ksp-Lisp to pochopí jako
volání funkce +
s parametry
1
a 2
a jelikož funkce
+
, jak již název napovídá, slouží ke sčítání celých
čísel, vypíše vám obratem výsledek 3
.
(Přesněji: nejprve se vyhodnotil symbol +
a
zjistilo se, že ukazuje na funkci, té se předaly odkazy na objekty
1
a 2
, funkce jako výsledek
vrátila odkaz na objekt 3
, který byl vzápětí vypsán.)
Seznam (* (+ 1 2) (+ 3 4))
by způsobil zavolání již
několika funkcí a výsledkem, jak asi čekáte, by bylo číslo
21
. Interpret ukončíte znakem konec řádku (^D).
A takhle vypadají v Lispu všechny programy, i podmínky
jsou totiž funkce (lépe řečeno speciální formy, jak uvidíme za chvilku, protože
jejich argumenty se samočinně nevyhodnocují před tím, než jsou předány) --
(if p q r)
způsobí nejprve
vyhodnocení podmínky p a poté, pokud je splněna
(to znamená vyšlo-li něco jiného než nil
), vyhodnotí
se q, jinak r a vrátí se
jeho hodnota. Místo cyklů se obvykle používají rekurzivně se volající funkce.
A teď již konečně prozradíme přesná pravidla pro vyhodnocování výrazů, tedy vlastně pro provádění programů. Vyhodnocení je proces, jehož vstupem je nějaký objekt x a výstupem nějaký jiný objekt y, který nazveme hodnotou objektu původního. Vyhodnocování probíhá takto:
nil
nebo integer,
je výsledkem tentýž objekt.
lambda
pro normální
funkci nebo special
pro speciální formu, jeho druhý
prvek je seznam symbolů a, jimž se mají přiřazovat
parametry a zbytek seznamu v je samotný vnitřek funkce).
Nyní nejedná-li se o speciální formu, rekurzivním voláním této vyhodnocovací
procedury vyhodnotíme všechny zbylé prvky seznamu x,
uložíme si na zásobník, kam se odkazují symboly zmíněné na seznamu
a a necháme je ukazovat na vyhodnocené argumenty,
načež opět rekurzivním zavoláním vyhodnocujeme jednotlivé objekty seznamu
v. Až toto vyhodnocování skončí, obnovíme uložené obsahy
symbolů, v nichž jsme předávali parametry a jako výsledek vrátíme výsledek
posledního objektu ze seznamu v. Šlo-li by o speciální
formu, zachovali bychom se stejně, pouze bychom parametry předali
nevyhodnocené. Pokud není první prvek x pár, jeho složka
není požadovaný symbol a nebo nesouhlasí počet argumentů uvedených
v a s tím, jaké byly skutečně předány v
x, vyhlásíme běhovou chybu.
(max 0 (+ 1 2))
zpracujeme tak, že nejprve zjistíme,
že max
je symbol, který ukazuje na definici funkce
max, poté vyhodnotíme rekurzivně její argumenty: 0
se vyhodnotí na sebe samu, druhý parametr pak jako volání primitiva
pojmenovaného symbolem +
s parametry
1
a 2
. Nakonec zavoláme
funkci max
, předáme jí výsledky (objekty
0
a 3
) a výsledek
posledního objektu v její definici vrátíme jako výsledek našeho vyhodnocování.
Mimo to ještě existují funkce s proměnným počtem argumentů -- v jejich
definici je místo úplného seznamu parametrů jen seznam končící symbolem
&rest
následovaným ještě jedním symbolem, kterému se
přiřadí všechny zbývající argumenty jako seznam.
Následuje úplný seznam Ksp-Lispových primitiv a jejich
parametrů. (Uvědomte si ovšem, že primitiva samotná jsou objekty, takže i když
to není dobrým zvykem, si je můžete přejmenovat, jak chcete.
Pod zde uvedenými jmény jsou dostupná při startu interpreteru.)
Typewriterem
budeme označovat jména primitiv a
argumenty speciálních forem, které se samočinně nevyhodnocují,
zvyrazněně pak argumenty funkcí, `. . .'
značí, že funkce může mít argumentů libovolně mnoho.
(+ x...)
-
(s jedním parametrem funguje jako unární minus, jinak od prvního čísla odečte
druhé, od výsledku třetí atd.), *
a
/
.
(= x y)
t
pokud jsou stejná, nil
pokud nejsou.
Analogicky <
, >
, <=
, >=
a <>
.
(eq x y)
t
, pokud jak x, tak y ukazují na tentýž
objekt, jinak nil
. Pozor, to, že dva objekty stejně vypadají (například
jsou-li to dva seznamy se stejným obsahem), ještě nemusí znamenat, že
jsou opravdu stejné. Každému číslu odpovídá vždy právě jeden objekt, takže
se pro čísla eq
chová přesně jako =
.
(not x)
t
pokud x je nil
, jinak nil
.
(block x...)
(cons x y)
(list x...)
(geta x)
a
páru x, analogicky getb
(seta x y)
a
páru x na y, analogicky setb
(get s)
(defined? s)
t
, pokud symbol s na něco ukazuje, jinak nil
(set s x)
(number? x)
t
, pokud x je číslo, jinak nil
. Obdobné
funkce existují i pro ostatní typy: pair?
, symbol?
, nil?
a primitive?
(eval x)
(if c t f)
nil
, vyhodnotí f
a vrátí jeho
výsledek, jinak vyhodnotí t
a vrátí jeho výsledek.
(and x...)
nil
, skončí vyhodnocování
celého and
u s výsledkem nil
, jinak se pokračuje dalším argumentem ve stejném
duchu. Pokud ani poslední argument není nil
, vrátí se jeho hodnota jako výsledek.
(or x...)
nil
, pokud žádný takový není, vrátí se nil
.
(quote x)
quote
, jediným důsledkem je, že argument "propadne" skrz quote
nezměněný a nevyhodnocený. Například výsledkem (quote (1 2))
je seznam (1 2)
.
Mezi list
a quote
je jeden podstatný rozdíl: list
vám dá pokaždé nový
seznam, kdežto quote
vrací odkaz na stále tentýž. Výrazy typu (quote (...))
lze rovněž zkracovat jako '(...)
.
(define (f a...) y...)
f
s parametry a...
a tělem y
,
jinými slovy sestrojí seznam (lambda (a...) y...)
a přiřadí symbolu
f
odkaz na tento seznam. Pokud použijete define
se symbolem místo seznamu
jako druhým parametrem, výsledkem bude pouze nastavení tohoto symbolu, aby ukazoval
na objekt y
(jinými slovy define
pak bude fungovat stejně jako set
, pouze
bude své parametry automaticky quotovat).
(let ((l1 v1) (l2 v2) ...) x...)
l1
, l2
atd., načež začne vyhodnocovat výrazy
v1
, v2
atd. a jejich výsledky přiřazovat těmto symbolům. Poté vyhodnotí
posloupnost výrazů x...
a nakonec obnoví původní význam symbolů
a vrátí jako výsledek hodnotu posledního z uvedených výrazů. Tato lokální
definice symbolů se často používá podobně jako lokální proměnné v procedurálních
jazycích, pouze je nutné si dát pozor na to, že pomocí let
symbolům přiřazené
objekty jsou viditelné i ve funkcích zevnitř let
zavolaných.
(print x...)- vyhodnotí své argumenty a vytiskne je.
A nyní si na jednoduchém příkladu ukážeme, jak v
Ksp-Lispu něco jednoduchého naprogramovat -- bude to funkce
length
, jejíž hodnotou bude délka zadaného seznamu:
(define (length x) (if x (+ 1 (length (getb x))) 0) )
Délku počítáme rekurzivně: nejprve otestujeme, zda je seznam neprázdný (podmínka
u if
je nesplněna jen tehdy, je-li x
objekt nil
), pokud ano, vrátíme o jedničku
zvětšenou délku jeho zbytku (bez prvního prvku), jinak vrátíme nulu jakožto délku
prázdného seznamu.
Naším druhým příkladem bude funkce reverse
, která pro zadaný seznam vrátí jiný
seznam, jenž bude obsahovat tytéž prvky v opačném pořadí (bylo by také možné použitím
seta
a setb
změnit pořadí prvků v seznamu původním, ale to my nechceme):
(define (rev2 x y) (if x (rev2 (getb x) (cons (geta x) y)) y) ) (define (reverse x) (rev2 x nil) )
Tentokráte jsme si nadefinovali obecnější funkci rev2
, která vrátí seznam, jenž
vznikne připojením seznamu y
za obrácení seznamu x
. Taková funkce se dá snadno
naprogramovat rekurzivně a reverse
je vlastně rev2
s y=nil
.
Chyby v programu:
Komentare -- vse, co se vyskytne mezi ; a koncem radku je ignorovano
Soutěžní úlohy: Naprogramujte v Ksp-Lispu:
max
, která jako své parametry dostane n celých čísel a vrátí největší z nich.
equal
, která porovná dva objekty (čísla,
seznamy, symboly atd.) na ekvivalenci podle struktury (jinými slovy na číslech
a symbolech se bude chovat stejně jako eq
, ale
seznamy a podobné konstrukce z párů bude považovat za stejné i tehdy, pokud jsou
vystavěny z různých objektů, ale obsahují totéž). Vrátí t
při shodě, jinak nil
.(equal 1 1) = t
, (equal '(1 (2 3)) '(1 (2 3))) = t
, (equal
'(1 2) '(3)) = nil
.