Lisp II: Úvod do jazyka

…aneb funkcionálně programuje i strejda Google!
23. 07. 2008

Lisp je jiný.

To je první věc, kterou si musíte uvědomit, pokud chcete programovat v Lispu.

Systém vyhodnocování

Lisp pracuje se symbolickými výrazy. Nejjednodušší symbolický výraz Lispu je atom. Atom může být buď číslo, nebo symbol. Pomocí symbolů a čísel můžeme poté skládat složitější výrazy, například seznamy. Seznam se skládá z dalších symbolických výrazů, takže může klidně obsahovat další seznamy. Pro tuto chvíli nám bude stačit vědět, že seznam se skládá ze symbolických výrazů a je ohraničem závorkami. Příklad seznamu: (1 2 a (b c)). Toto je čtyřprvkový seznam (obsahuje dvě čísla, jeden symobl a jeden dvouprvkový seznam).

Čísla se vyhodnocují sama na sebe. Tedy když do posluchače napíšete číslo, Lisp vám vrátí totéž číslo. Zkuste si to. Docela exoticky působí možnost zapisovat zlomky přímo ve tvaru zlomku. Pokud chcete napsat jednu polovinu, napíšete zkrátka 1/2. Systém to pochopí jako zlomek a bude s ním tak pracovat. Například pokud k tomuto zlomku přičtete jedničku, Lisp vám nevrátí 1,5, ale vrátí vám skutečně 3/2.

Symboly představují proměnné, jak je známe z ostatních jazyků. Lisp nerozlišuje symboly pro funkce a symboly pro proměnné, takže symbol karkulka může být stejně tak „proměnná“ jako funkce. S vyhodnocováním symbolů je už to krapet složitější, protože jejich vyhodnocení závisí na tom, kde zrovna ten symbol použijeme. Obecně mohou nastat dva případy: na symbol není navázaná žádná hodnota, potom Lisp při pokusu o vyhodnocení vrátí chybu. V opačném případě se snaží symbol vyhodnotit na jeho vazbu.

CL-USER 23 > a
Error: The variable A is unbound.

Proměnnou a jsme zkrátka nenavázali, tak ji nemůžeme vyhodnocovat. Něco jiného je, když ji navážeme na nějakou hodnotu. To můžeme udělat nehezky pomocí makra setf:

CL-USER 24 : 1 > (setf a 10)
10

CL-USER 25 : 1 > a
10

Vidíme, že po navázání čísla deset na symbol a a následném vyhodnocení symbolu a již Lisp neháže chybu. Teď ale nastává drobný problém, protože v Lispu má každý symbol dva chlívečky, kam můžete něco uložit. Jeden chlíveček je „hodnotový“ a druhý „funkční“. Hodnotový slouží k ukládání hodnot, zato funkční pro ukládání funkcí. Před chvílí jsme uložili desítku do hodnotového chlívečku, proto můžete vzápětí provést toto:

CL-USER 26 : 1 > (defun a()5)
A

Tímto jsme do funkční části uložili primitivní funkci, která pouze vrátí číslo pět. Symbol a zavoláme prostým napsáním a, funkci a zavoláme takto: (a).

CL-USER 27 : 1 > a
10

CL-USER 28 : 1 > (a)
5

K čemu to je dobré? Je to primárně proto, abyste si mohli pojmenovat třeba argumenty funkcí jak chcete. V Lispu existuje vestavěná funkce list, kterou nelze přepsat. Pokud by nebyly k dispozici dva chlívečky, již byste symbol list nemohli použít jinak než jako funkci. Takhle si můžete klidně zvolit list jako název argumentu nějaké funkce. Systém, kdy se veme funkce a kdy hodnota, je trochu složitější a budeme se jím zabývat později.

Prefixová notace

První věc, která vás zaskočí, je, že Lisp používá prefixovou notaci, namísto klasické infixové. Co to znamená? Že veškeré operátory se umisťují před samotné operandy. Například součet trojky a sedmičky byste v klasickém jazyce zapsali jako 3 + 7. V Lispu by to bylo + 3 7, přesněji (+ 3 7). V Lispu pracujeme se seznamy a pokud chceme vyhodnotit složitější výraz, musí být v seznamu. Prefixová notace má mnoho výhod:

  • Jednodušeji se píše parser, protože ten hned na začátku ví, co se s čím bude dělat. Stejně tak to vždy víte vy. Stačí se kouknout na první prvek seznamu a přesně víte, jaká operace bude probíhat.
  • Díky prefixové notaci musíte také správně uzávorkovávat, protože jinak by vznikl neplatný výraz: výraz (+ 3 * 5 2) se pravděpodobně nevyhodnotí tak, jak byste očekávali; musíte ho zapsat správně do seznamů: (+ 3 (* 5 2)).
  • Jednotnost jak u funkcí, tak u operátorů. Stejně jako je na začátku seznamu operátor, je vždy na začátku seznamu i funkce.
  • Možnost použití více operandů zároveň. Konkrétně u sčítání můžete sčítat více čísel zároveň, ne pouze dvě: (+ 1 2 3 4). V jiném jazyce by se tohle zapsalo jako 1 + 2 + 3 + 4.

Nevýhoda prefixového zápisu je zřejmá – je to nezvyk a v počátcích to může programátora docela mást. Obzvláště blbě vypadají porovnávací operátory: (< 5 10). V běžném jazyku by se napsalo čitelnější 5 < 10.

Ještě se trochu blíže podíváme na základní operátory. Použití by již mělo být vcelku jasné. Například (- 10 5) odečte pětku od desítky (ekvivalentní matematický zápis: 10 - 5). Stejně jako plus, i minus bere více argumentů: (- 15 10 2 3) ⇒ vrátí nulu (ekvivalentní matematický zápis: 15 - 10 - 2 - 3). Podobně u násobení a dělení:

CL-USER 2 > (* 10 3)
30

CL-USER 3 > (* 3 6 7)
126

CL-USER 4 > (/ 80 5)
16

CL-USER 5 > (/ 80 5 4)
4

Kombinace více operátorů by měla být také zřejmá. Zkrátka místo čísla vložíme seznam s dalším výrazem. (3 + 5) * (9 - 1) * 2 by se zapsalo: (* (+ 3 5) (- 9 1) 2).

Pozor na to, že operátory plus a krát mohou být použity bez argumentu. Lisp poté vrátí nulový prvek, u sčítání nulu a u násobení jedničku. U dělení a odečítání je vždy třeba alespoň jeden argument. V případě, že je právě jeden, vrací Lisp opačnou hodnotu.

CL-USER 13 > (+)
0

CL-USER 14 > (*)
1

CL-USER 15 > (/ 5)
1/5

CL-USER 16 > (- 5)
-5

Na prvním místě seznamu musí být vždy nějaký operátor nebo funkce. Pokud není, Lisp vyhodí chybu, protože neví, co má s takovým seznamem dělat:

CL-USER 29 : 1 > (1 2 3)
Error: Illegal argument in functor position: 1 in (1 2 3).

Zpět k vyhodnocování výrazů

Teď se již můžeme vrátit k vyhodnocování výrazů. Říkali jsme, že v každém symbolu může být uložena funkce a hodnota. Z výše řečeného poměrně jasně plyne, jak se bude Lisp chovat a kdy který chlív zvolí. Pokud se bude symbol vyskytovat na prvním místě seznamu, zvolí funkční chlív, pokud se bude vyskytovat jinde, zvolí se hodnota. Ukázkový příklad:

CL-USER 30 : 2 > (setf fun 10)
10

CL-USER 31 : 2 > (defun fun (cislo)
                   (* cislo 2))
FUN

CL-USER 32 : 2 > (fun fun)
20

Nejprve na symbol fun navážeme hodnotu deset. Potom na symbol fun navážeme funkci, která vrátí dvojnásobek předaného čísla. A následně zavoláme (fun fun). První fun se vyhodnotí jako funkce (je na prvním místě seznamu) a druhý fun se vyhodnotí jako desítka (není na prvním místě).

To by mohlo pro dnešek stačit, příště se podíváme blíže na funkce.