Lisp IV: Tečkové páry, seznamy, kvótování

…aneb funkcionálně programuje i strejda Google!
6. 08. 2008

V dnešním článku o Lispu si povíme něco o tečkových párech a o seznamech. Konečně budeme moci stvořit funkci, která vrátí trochu složitější výraz než pouze číslo.

Tečkové páry

Základní struktura v Lispu je tvořena tzv. tečkovými páry. O co jde? Jedná se o dvojici hodnot, která se navenek tváří jako jakási jednolitá struktura, je to tedy další výraz a jakožto takový ho můžeme například vrátit ve funkci. Jak vytvoříme tečkový pár? Obvykle se tvoří pomocí funkce cons, která bere dva argumenty: levou část tečkového páru a pravou část tečkového páru. Tečkový pár běžně zapisujeme takto: 1 . 2. Takovýto tečkový pár bychom vytvořili touto konstrukcí:

> (cons 1 2)
(1 . 2)

Samozřejmě jednotlivé složky můžeme také číst. K tomu slouží funkce car [kar] a cdr [kudr]. Funkce car vrací první složku tečkového páru, kdežto cdr vrací druhou část. Předvedeme si to na příkladech:

> (car (cons 1 2))
1

> (cdr (cons 1 2))
2

Argument předaný funkci cons se samozřejmě vyhodnocuje, takže když předáme (cons (+ 1 2) 3), tak dostaneme tečkový pár (3 . 3). Základy práce s tečkovými páry máme za sebou, takže už můžeme naprogramovat výpočet kořenů kvadratické funkce:

(defun na2(a)
  (* a a))

(defun deter(a b c)
  (- (na2 b) (* 4 a c)))

(defun x12(a b c)
  (cons
   (/ (+ (- b) (sqrt (deter a b c))) (* 2 a))
   (/ (- (- b) (sqrt (deter a b c))) (* 2 a))))

> (x12 1 5 6)
(-2.0 . -3.0)

Jak to celé funguje? První dvě funkce by měly být jasné – první počítá druhou mocninu a druhá funkce vrací determinant. Třetí funkce vrací tečkový pár, na jehož první pozici se nachází jeden kořen a na druhé pozici druhý kořen. Počítá se to podle klasického vzorečku, akorát převedeného do Lispových pravidel. Funkce sqrt vrací druhou odmocninu. Po zavolání funkce proběhnou výpočty a objeví se tečkový pár. Vzhledem k tomu, že tam dělíme a odmocňujeme, tak bude výsledek reálné číslo (proto ta nulová desetinná část).

Složené tečkové páry

Už se mílovými kroky blížíme k základnímu stavebnímu kameni všech Lispových programů – k seznamům. Jak už jsme si řekli, tečkový pár obsahuje dvě části – levou a pravou. Do těchto částí můžeme uložit prakticky cokoliv. Záleží jen na nás, co necháme na daná místa vyhodnotit. Takže se může stát, že třeba v pravé části tečkového páru bude další tečkový pár. Příklad:

> (cons 1 (cons 2 3))
(1 2 . 3)

Vidíme, že už to skoro vypadá jako seznam :-). Lisp používá takovou trochu zkrácenou verzi značení tečkových párů. Existuje více modelů, ten nejpřímější a nejsložitější vypadá takto: (1 . (2 . 3)). Ale značení není zase tak důležité, navíc bývá v případě potřeby intuitivní.

I na tento složený tečkový pár můžeme aplikovat naše staré známé funkce car a cdr. Jaké budou dávat výsledky? car vrátí jedničku, kdežto cdr vrátí zase tečkový pár:

> (car (cons 1 (cons 2 3)))
1

> (cdr (cons 1 (cons 2 3)))
(2 . 3)

Výsledky jsou očekávatelné. Na prvním místě tečkového páru je číslo jedna, na druhém místě je tečkový pár. Nejhezčí na těchto funkcích je, že je můžeme skládat. Takže pokud bychom z toho tečkového páru chtěli vytáhnout třeba dvojku, použijeme tento seznam:

> (car (cdr (cons 1 (cons 2 3))))
2

Jak bude probíhat vyhodnocování? Nejprve vznikne tečkový pár (1 . (2 . 3)). Z tohoto tečkového páru vememe cdr, což nám vrátí (2 . 3) a odtud pomocí car vytáhneme dvojku. A abychom nemuseli takto složitě skládat car a cdr, můžete používat zkráceniny ve tvaru CAAR, CADR, CDAR atd. Výraz (cadr x) je ekvivalentní složenině (car (cdr x)). Výraz (cdar x) je ekvivalent pro (cdr (car x)). Předchozí výraz bychom zkráceně zapsali jako:

> (cadr (cons 1 (cons 2 3)))
2

Seznamy

Poslední poznámka před tím, než si nadefinujeme seznam. V Lispu existuje false hodnota, které se říká NIL. Je to lispový ekvivalent pro nepravdu. Pravda se mimochodem značí pouhým t. Obdobnou hodnotu jako NIL může plnit prázdný seznam. Prázdný seznam vypadá docela intitivně takto: (). Jsou to ekvivalenty.

Jak tedy vypadá seznam? Seznam je vnitřně konstruován jako posloupnost do sebe vnořených tečkových párů, na jehož poslední místě je prázdný seznam / NIL. Příkladem jednoduchého seznamu může být výraz (cons 1 nil). Toto se vyhodnotí na jednoprvkový seznam (1). Víceprvkové seznamy bychom zapsali takto:

> (cons 10 (cons 15 (cons 20 nil)))
(10 15 20)

> (cons -5 (cons 5 nil))
(-5 5)

V minulých článcích jsme si říkali, že seznamy se do sebe mohou zanořovat. Jak to provedeme, by již mělo být jasné – zkrátka namísto přímé hodnoty vložíme další tečkový pár. Seznam ((1 2) (3 4)) bychom mohli zapsat takto:

> (cons (cons 1 (cons 2 nil)) (cons (cons 3 (cons 4 nil)) nil))
((1 2) (3 4))

Pozor na to, že i vnitřní seznamy musí být vždy ukončeny prázdným seznamem (NIL).

Protože seznamy jsou zakuklené tečkové páry, můžeme na ně používat klasické konstruktory car a cdr. Je si musíte uvědomit, co která funkce vrací. V případě jednoduchého seznamu (ve kterém není zanoření) vrací car vždy konkrétní číslo a cdr vrací seznam, tzv. ocas seznamu. Proto nemůžete u jednoduchého seznamu udělat dvakrát car za sebou, protože car z čísla udělat nejde. Příklady:

; pro jednoduchost uložíme seznam do proměnné
> (setf seznam (cons 1 (cons 2 (cons 3 nil))))
(1 2 3)

> (car seznam)
1

> (cdr seznam)
(2 3)

> (cadr seznam)
2

> (caddr seznam)
3

> (cddr seznam)
(3)

> (cdddr seznam)
NIL

Vidíme, že pokud jsme naposledy volali funkci car, bylo výsledkem číslo, kdežto když jsme volali pouze cdr, vrátil se nám seznam. Pouze v případě, kdy jsme provedli třikrát cdr, vrátilo se nám NIL (což je ale vlastně také seznam – prázdný). NIL se vrátilo proto, že výsledkem cdr u posledního tečkového páru v seznamu je zkrátka NIL.

Kvótování

Vytvářet seznamy pokaždé pomocí funkce cons může být velice kontraproduktivní, protože je to jednoduše zdlouhavé a nepřehledné. Proto existuje speciální operátor quote. Tento operátor ruší klasický vyhodnovací proces, což má mimojiné za následek to, že můžeme konečně vytvořit seznam jednoduchým a pohodlným způsobem. Operátor quote zkrátka převeme argument a vrátí ho v nevyhodnocené podobě. Příklady s jednoduchými výrazy:

> (quote a)
A

> (quote abrakadarba)
ABRAKADARBA

Abychom si to ještě více zjednodušili, můžeme namísto operátoru quote používat zkratku v podobě apostrofu:

> 'a
A

> 'abrakadabra
ABRAKADABRA

Jak to využijeme na tvorbu seznamů? Zkrátka předáme operátoru seznam a on ho nevyhodnotí a vrátí nám ho v nezměněné podobě:

> (quote (a 1 2 3))
(A 1 2 3)

> '(a 1 2 3)
(A 1 2 3)

Jak vidíme, seznam se skutečně nevyhodnotil. Kdyby se vyhodnocoval, Lisp by zařval, protože na symbolu a nemáme navázanou žádnou funkci.

Funkce list

Dnes si ještě představíme ještě jednu funkci na tvorbu seznamů. Tou je funkce list. Funkce list je trošičku podobná operátoru quote, akorát bere libovolný počet argumentů a jednotlivé argumenty před naházením do listu vyhodnocuje. Takže nejprve jednoduché příklady:

> (list 1 2 3)
(1 2 3)

> (list 5)
(5)

> (list)
NIL

A teď ten rozdíl oproti quote. Pokud zkusíte zakvótovat tento seznam: (+ 2 5), tak nedojde k jeho vyhodnocení a výsledný seznam bude vrácen v této nezměněné podobě.

> (quote (+ 2 5))
(+ 2 5)

> '(+ 2 5)
(+ 2 5)

Kdežto funkce list své argumenty vyhodnocuje, takže když listu předáme stejný argument, bude v seznamu pouze sedmička:

> (list (+ 2 5))
(7)

Proto taky nemůžeme pouze pomocí list vytvořit seznam tvořený ze symbolů. Pokud chceme vytvořit seznam (a b c), musíme jednotlivé symboly kvótovat:

> (list a b c)
Error: The variable A is unbound.

> (list 'a 'b 'c)
(A B C)

Quote zkrátka veme svůj argument a vůbec se nepárá s vyhodnocováním, na rozdíl od list, což není speciální operátor, ale obyčejná funkce, a proto tam probíhá klasický vyhodnocovací proces.