A LISP programozási nyelv

Nyelvi elemek, változók, kifejezések

Számok

Több Lisp változat csak egész típusú számokat enged használni, de a Common Lisp már több fajta számtípusokat különböztet meg és enged használni. Ezek az

integer – egész
ratio – racionális
float – lebegőpontos
complex - komplex

Együttesük a number típus, amely magába foglalja az összes számtípust. Vagyis a fenti típusok a number típus altípusai.

A number típus felépítése:

number ::= real | complex
real ::= rational | float
rational ::= integer | ratio

Az elnevezések a matematika terminológiáját követik, de nem feltétlenül pontosak. Az integer és a ratio típus pontosan megegyező a matematika terminológiájával. A float típus viszont a valós számok közelítésére használható. A real típus minden Common Lisp-beli számot tartalmaz, amely valós számot reprezentál, viszont vannak olyan valós számok, amelyeknek nincs pontos Common Lisp beli reprezentánsuk. Csak a real típusú számok rendezhetőek a <, >, <= és >= függvényekkel.

A number típusra a megszokott matematikai függvények használhatók (pl. +, -, *, /, floor, ceiling, mod, sin, cos, tan, sqrt, exp, ...). Ezek mindegyike elfogadja argumentumként a fent említett számtípusokat.

A +, -, *, / függvények az eredményt a megfelelő típusra konvertálják. Pl.: (+ 3 3/4) = 15/4
A számok vizsgálatát a numberp függvénnyel végezhetjük el. Ha az argumentum szám akkor a visszatérési értéke true egyébként false.
Egy szám párosságát vagy páratlanságát az oddp evenp függvényekkel vizsgálhatjuk meg. Ezen felül hasznos lehet a minusp és plusp függvény is ami megadja, hogy egy szám negatív vagy pozitív-e.
Gyakran el kell tudnunk dönteni, hogy egy szám értéke nulla vagy valami más. Erre használhatjuk a zerop függvényt.

Integer

Az integer típus a matematikai egész számokat reprezentálja. A legtöbb programozási nyelvvel ellenttétben a Common Lisp nem tesz megkötést egy integer hosszára vonatkozóan, tetszőlegesen nagy lehet, a méretet csak a memória mérete korlátozza.

A Common Lisp bármely implementációjában van az integer típusnak egy hossza, ami alatt ezeket hatékonyabban kezeli. Minden ilyen integer neve fixnum. A nem fixnum integereket pedig bignum-nak nevezik. A Common Lisp-et úgy tervezték, hogy ezt a megkülönböztetést a lehető legjobban elrejtse, és csak nagyon indokolt esetben tudassa a felhasználóval a fixnum és bignum közti különbséget. Az, hogy pontosan mely számok tartoznak a fixnum-ok közé implementáció függő, általában a [-2^n,...,2^n - 1], n>=15 intervallum elemei.

integer ::= [sign] {digit}+
sign ::= + | -
digits ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9

példák:

0 -0 +6 28 1024.

A számok végén a ’.’ karakter használata opcionális.

Ratio

A ratio típus a matematikai racionális számokat reprezentálja, azaz azok a számok tartoznak ide, amelyek felírhatók két egész szám hányadosaként. Az integer és a ratio típus együtt alkotják a rational típust. Egy racionális szám kanonikus reprezentációja egy integer, ha a hányados értéke egész, különben egy számláló és egy nevező (egészek), amelyek legnagyobb közös osztója 1 és a nevező pozitív. A hányadost ’/’ jelöli. Lehetséges a racionális számokat nem kanonikusan használni, azaz nem egszerűsítve, de a Lisp prin1 függvénye mindig a kanonikus alakot írja az outputra.

ratio ::= [sign] {digits}+ / {digits}+
sign ::= + | -
digits ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9

A második sorozat nem állhat csupa 0 elemekből.

példák:

2/3 4/6 -17/23 10/5

Ha bármilyen számítás olyan értéket eredményez, amely két integer hányadosa úgy, hogy a számláló a nevező egész számú többszöröse, akkor az eredmény azonnal a megfelelő integer típusra konvertálódik.

(defun f (a b) (/ a b)) (f 3 4) ;értéke 3/4 ratio típus (f 10 2) ;értéke 5 integer típus!

Float

A Common Lisp több fajta lebegőpontos szám típus implementálását engedélyezi, amelyek együtt alkotják a float típust. Egy lebegőpontos szám egy (matematikai) racionális szám, amely az alábbi formulával írható le:

s*f*b^(e-p), ahol

s +1 vagy -1,
b 1-nél nagyobb egész,
p 0-nál nagyobb egész,
f a [-b^(p-1),...,b^(p-1)] intervallumból való,
e egész.

p értéke és e hossza függ az implementációtól és az implementáción belüli lebegőpontos szám típusától. Például tekintsük a lebegőpontos nullát. Az implementációtól függően létezhet egy „mínusz nulla” is, amely nem azonos a „pozitív nullával”.

A lebegőpontos számok a pontosság és a méret függvényében definiálhatók. Egy jó minőségű, lebegőpontos számokkal operáló alkalmazás a pontosságra törekszik, ennek hátránya a hordozhatóság tulajdonság elvesztése. Pontosan ezért külünböztetünk meg lebegőpontos számokat:

short-float: a legkisebb rögzített pontosság mellett definiált
long-float: a legnagyob rögzített pontosság mellett definiált
single-float, double-float: a short- és a long-float közötti

A lebegőpontos számok BNF-je:

floating-point-number ::= [sign] {digit}* decimal-point {digit}* [exponent]
| [sign] {digit}+ [decimal-point {digit}*] exponent
sign ::= + | -
decimal-point ::= .
digit ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
exponent ::= exponent-marker [sign] {digit}+
exponent-marker ::= e | s | f | d | l | E | S | F | D | L

Ha nincsen exponens megadva vagy ha az exponens jelző e (vagy E), akkor a pontos formátum nincs mehatározva.

Az s, f, d, l (ill. S, F, D, L) rendre a short, single, double, long formátumokat határozzák meg.

példák:

0.0 ;lebegőpontos nulla az alapértelmezett formátumban 0E0 ;lebegőpontos nulla az alapértelmezett formátumban -.0 ;ez lehet minus nulla ill. plusz nulla is, implementációfüggő 0. ;az egész nulla, NEM a lebegőpontos nulla! 0.0s0 ;lebegőpontos nulla short formátumban 0s0 ;lebegőpontos nulla short formátumban 3.1415926535897932384d0 ;a pi közelítése double formátumban

Complex

A komplex számokat a szokásos módon reprezentáljuk Lispben, azaz egy valós és egy képzetes résszel. Ezek mindegyike rational típusú, tehát integer, ratio vagy float típusú, de a kettő típusa azonos kell legyen. Ha a listában szereplő számok nem azonos típusúak, akkor a Lisp azonos típusra konvertálja őket.

Egy complex típusú szám könnyen felismerhető, a #C karakterek után egy listában a valós illetve a képzetes számok szerepelnek.

példák:

#C(3.0s1 2.0s-1) ;a valós és képzetes részek short-float formátumban #C(5 -3) ;a valós és képzetes részek egészek #C(5/3 7.0) ;átkonvertálódik #C(1.66666 7.0) -re #C(0 1) ;i

Karakterek

A karaktereket a character típus foglalja magába. Egy karakter objektumot a #/ jelek után írt karakterrel azonosítunk. #\g tehát egy karakterobjektum a ’kis g’ karakterhez. Ez megfelelően alkalmazható karakterek kiíratásához. A nem látható karakterekhez nevek tartoznak. Például a space név jelenti a szóköz karaktert, tehát ha egy szóközt szeretnénk az outputra kiírni, akkor ezt megtehtjünk a #\space karakterobjektum alkalmazásával.

A Common Lisp definiál egy meghatározott karakterekből álló standard-char altípust. Ennek egyik oka, hogy az a Common Lisp program, amely csak a standard karaktereket használja, azt bármely Common Lisp implementáció képes olvasni. Illetve az a Common Lisp program, amely csak standard karaktereket használ adat objektumként az a program nagy valószínűséggel hordozható.

A standard karakterek a következők:

két nem látható karakter: #\space, #\newline

illetve 94 általános karakter:

! " # $ % & ' ( ) * + , - . / 0 1 2 3 4 5 6 7 8 9 : ; < = > ? @ A B C D E F G H I J K L M N O P Q R S T U V W X Y Z [ \ ] ^ _ ` a b c d e f g h i j k l m n o p q r s t u v w x y z { | } ~

Karakterek jellemzői

A character típus minden objektumának három attribútuma van: code, bits és font.

A code attribútum arra hivatott, hogy megkülünböztesse a karakterek betűképét és formázó funkcióit.
A bits attribútummal extra flag-eket lehet hozzárendelni a karakterekhez.
A font pedig a karakterek képének stílusát állíthatja (pl. félkövér, dőlt...)

példa: font attributum használata: #3\a az a betűt jelenti 3-mas font típussal, ha a 3-mas font típus a görög betűk, akkor ez ekvivalens a #\alfa karakter objektummal.

String karakterek

Minden karakter, amely bits és font attributumai nullák, azok állhatnak egy string belsejében. Az ilyen karaktereket a character típus egy új altípusába soroljuk: string-char.

Tömbök

A tömb egy olyan objektum, melynek komponensei a derékszögű koordináta rendszer szerint rendezettek. Ezek a komponensek tetszőleges Lisp objektumok lehetnek.

Egy tömb dimenzióinak a száma a tömb rangja. A rang egy nem negatív egész szám. Hasonlóképpen minden dimenzió egy nem negatív egész szám. A tömb összes elemének a száma a dimenziók szorzata.

A Common Lisp bizonyos implementációja határt szabhat egy tömb rangjára vonatkozóan, de ez a határ nem lehet kisebb 7-nél. Ebből következően bármilyen Common Lisp program felteheti, hogy a használt tömb rangja 7 vagy annál kisebb. Jelenleg a Common Lisp 25 dimenzióig támogatja a tömböket.

Engedélyezett, hogy egy dimenzió nulla legyen. Ebben az esetben a tömbnek nincsenek elemei és bármilyen hozzáférési kísérlet hibát eredményez. Mindazonáltal a tömb többi tulajdonsága elérhető, mint például a dimenziók maguk. Ha a rang nulla, akkor nincsenek dimenziók, definíció szerint ekkor a tömb elemeinek a száma 1. Azaz egy nulla rangú tömbnek van 1 eleme.

Egy tömb objektumot az indexek sorozata határozza meg. A sorozat hossza megegyezik a tömb elemeinek a számával. Minden index egy nemnegatív egész, amely határozottan kisebb a megfelelő dimenziónál. A tömb indexelése 0-val kezdődik.

Legyen t egy 3x5 dimenziós tömb. Ekkor a tömb elemeire való hivatkozásnál az első index 0,1 vagy 2 míg a második index 0, 1, 2, 3 vagy 4 lehet. A tömb elemeire hivatkozni az aref függvénnyel tudunk. Például (aref t 2 1) a tömb (2, 1)-edik elemére hivatkozik. Ha t nulla rangú lenne, akkor nem írunk indexeket az aref függvénybe és (aref t) a tömb egyetlen elemét adja eredményül.

A tömbök tehát lehetnek többdimenziósak, megoszthatják tartalmukat más tömb objektumokkal és dinamikusan változtathatják (növelhetik vagy csökkenthetik) méretüket a létrehozásuk után.

Műveletek

Tömböt a make-array függvény vagy a #karakter segítségével lehet létrehozni. Az indexelés mindig 0-val kezdődik. Az összes tömbelem kezdeti értéke NIL. Az elemekre indexek alapján lehet hivatkozni az aref függvénnyel. Pl.:

(make-array '(3 3)) ;egy háromdimenziós tömb, amely három háromelemű listát tartalmaz: ;((NIL NIL NIL) (NIL NIL NIL) (NIL NIL NIL))
(make-array 4) ;eredménye egy négyelemű tömb: (NIL NIL NIL NIL)
#(1 2 3) ;eredménye egy 1 dimenziós 3 elemű tömb
#2((1) (1 2)) ;eredménye egy 2 dimenziós 2 elemű tömb

További műveletek:

(setq a (make-array 3)) ;az a szimbólum értéke a második argumentumban megadott tömb lesz #(NIL NIL NIL)
(aref a 1) ;a 1 indexű elemét adja vissza NIL
(setf (aref a 1) 3) ;a második elemét 3-ra állítja 3
;a értéke ekkor #(NIL 3 NIL)
(aref a 1) ;a második eleme most már 3

Tömböket veremszerűen is lehet kezelni:

(setf a (make-array 1)) ;setf itt hasonlóan működik, mint a setq, vagyis a értéke a tömb lesz #(NIL)
(push 5 (aref a 0)) ;az a tömb első elemének értéke 5 lesz (5)
(pop (aref a 0)) ;kiveszi a tömb első elemét az 5-öt ()
(setf (aref a 0) 5) ;a tömb első elemét 5-re állítja (5)
(incf (aref a 0)) ;incf kiolvassa az értéket, eggyel növeli, és visszaírja (6)
(aref a 0) ;(6)

Vector

Common Lisp-ben az egydimenziós tömböket egy vector nevű altípusba soroljuk. Ez a típus felel meg a hagyományos vektor típusnak. A vektorok és listák egyaránt sorozatoknak tekinthetők a különbség köztük, hogy a vektor bármely elemét azonnal el tudjuk érni, míg a lista esetében ez egy lineáris művelet. Másrészt egy elem hozzáadása a listához konstans időt vesz igénybe vektor esetében pedig lineáris.

Lisp-ben egy általános vektort (azaz egy egydimenziós tömböt, amelynek elemei tetszőleges típúsú objektumok lehetnek) az elemeik felsorolása jelez a #( és ) szimbólumok között.

példák:

#(a b c) ;egy 3 hosszú vektor #() ;egy üres vektor #(2 3 5 7) ;egy vektor, amely a 10 alatti prímszámokat tartalmazza

Különböző implementációk más-más vektor típusok reprezentálását eredményezhetik, például, hogy egy vektor elemeinek azonos típusúnak (legtöbb esetben ezek szám típusok) kell lenni. Minden implementáció megkülönbözteti azokat a tömböket, amelyek karaktereket tartalmaznak, ezek egydimenziós változatait egy új altípusba sorolja, ez a string típus.

Bit Tömbök

A tömbök egy speciális esetei a bit tömbök. Ezek olyan tömbök melynek elemei csak 0 vagy 1 lehet. Ezek létrehozása a make-array függvénnyel és a #*karakterrel lehetséges.
példák:

(make-array 10 :element-type 'bit) - 10 elemű bit tömb létrehozása
#*100000 - 6 elemű bit tömb létrehozása

A bit tömbre alkalmazhatóak a bitenkénti logikai műveletek is. (Bitenkénti and, or, not, xor ...)
(bit-and #*1000 #*1010) eredénye #*1000
(bit-xor #*1100 #*1010) eredménye #*0110
(bit-not #*1111) eredménye #*0000

String

A string tehát egy karaktereket (egészen pontosan string-char típusu objektumokat) tartalmazó vektor. Vagyis a string típus a vector típus egy altípusa.

Ezeket a speciális vektorokat jelölésben is megkülönzöztetjük. Egy string típusú objektum, karakterek sorozata amelyeket " jelek határolnak. Ha a " karaktert szeretnénk egy string típus belsejében használni, akkor elé egy \ karaktert kell írnunk.

példák:

"abc" ;egy 3 hosszú sztring "" ;egy üres sztring "\"abc\"def." ;egy 9 hosszú sztring "|x| = |-x|" ;egy 10 hosszú sztring

Műveletek:

Mivel a vector és a string típus mind a tömb típus altípusai, ugyanazok a műveletek alkalmazhatóak rájuk.

Kifejezések

A kifejezések alapegységei az atomok. Az atom lehet numerikus (egy szám és az esetleges előjele), illetve szimbolikus (betűkből és számokból álló karaktersorozat, aminek az első karaktere betű). A Lisp alapvető szerkezete a lista. A lista atomokból épül fel úgy, hogy, zárójelek között megadjuk a listát felépítő atomokat szóközzel elválasztva. Egy lista atomokból és listákból állhat.
Az atomokat és listákat összefoglaló néven szimbolikus kifejezéseknek, S-kifejezéseknek vagy röviden kifejezéseknek nevezzük. Az alábbiak tehát S-kifejezések:

T MACSKA 27 (KUTYA MACSKA) (KUTYA (MACSKA (BARÁTSÁG))) (CATCH (22)) (1 2 3 4) (PLUS 20 30)

S-kifejezések formálisan:

  1. Minden atom S-kifejezés.
  2. Ha X és Y S-kifejezés, akkor (X . Y) is S-kifejezés, amit párnak nevezzük
  3. Ha S1,S2,...Sk S-kifejezések, akkor (S1 S2 ... Sk) is S-kifejezés, amit listának nevezünk.
  4. Minden olyan objektumot, ami az 1, 2, 3 szabály alkalmazásával áll elő S-kifejezésnek nevezünk.

A LISP nyelvben mind a programok, mind pedig az adatok, amelyeken a programok dolgoznak, S-kifejezésekből állnak. Egy LISP program S-kifejezések egymásutánja.

Mindent zárójelezni kell, nincs precedencia. Ennek köszönhetően nagyon egyszerű a nyelvtan. Így könnyen irható olyan program, mely a feladata megoldásához egy másik programot (kifejezések sorozatát) generál. Ez különösen a mesterséges intelligencia alkalmazásoknál fontos.

Beépített konstansok:

Megjegyzések

A kommenteket ;-vel kell kezdeni. Hatásuk a sor végéig tart. Többsoros komment írására nincs lehetőség.

Változók

Az itt leírtak a Common Lisp-re igazak általánosan. Mint minden nyelvben a (Common) Lisp-ben a változók nevesített helyek amelyek értéket tárolhatnak. A Java-val és a C++-al ellentétben a Lisp-ben nem kell megadni típust az objektumnak. Minden változó tárolhat bármilyen típusú értéket, és maga az érték tárolja a típust ami futásidőben lekérdezhető. Ennélfogva a (Common) Lisp dinamikusan típusos nyelv(pl ha + függvénynek nem szám típust adunk meg típus hibát fog dobni). Viszont a nyelv erősen típusos is, tehát minden típushibát detektálni fog. Ha egy változóhoz új értéket rendelünk hozzá, akkor az csak azt változtatja meg, hogy melyik objektumra mutat a változó, de a korábbi objektumot nem változtatja meg. Viszont mutable objektumoknál lehetőség van az objektum módosítására, ami az összes arra az objektumra mutató referenciának látható lesz.

Környezetek

Lexikális

Lexikálisan kötött változok hatóköre fizikálisan arra a környezetre korlátozódik ahol a kötés létrejött. A blokkra nem lexikálisan hivatkozó referenciák nem látják ezeket a változókat. Ez hasonlít a C illetve a Java lokális változóihoz. A lexikális környezet megadásához Common Lisp-ben a LET kulcsszót használjuk. A LET kulcsszó használata:

(let (variable*) body-form*)

Ahol a variable a változók listája, a body-form a környezet törzse(lásd szekvencia). Pl:
(let ((x 10) (y 20) z) ...)

Itt három változó van x,y és z és rendre a 10 20 és NIL értéket veszik fel. Ha nem adunk meg értéket akkor alapértelmezetten a NIL értéket fogja a változóhoz kötni.

Dinamikus

A modern nyelvek mindegyike elsődlegesen a lexikális(lokális) környezetet támogatja és ajánlja, mert segíti az átláthatóságot. Viszont globális változókra szükségünk lehet, ezért ez szinte minden programozási nyelvben megjelenik. A Lisp megoldása a globális változókra az úgynevezett dinamikus változók, a használhatóság és a kezelhetőség előnyeit. Common Lispben kétféle út van globális változók létrehozásához: DEFVAR és DEFPARAMETER. Mindkettő kér egy változónevet egy kezdőértéket és opcionálisan megható egy dokumentációs string. Globális változóknál konvencionálisan *-gal kezdődik és végződik a név az átláthatóság végett. Pl:

(defvar *count* 0 "Count of widgets made so far.") (defparameter *gap-tolerance* 0.001 "Tolerance to be allowed in widget gaps.")

A különbség a DEFVAR és a DEFPARAMETER között, hogy a DEFPARAMETER minden esetben értékül adja az értéket a változónévhez, a DEFVAR csak akkor, ha a változó még nem volt definiálva. Illetve DEFVAR esetén nem kötelező kezdeti értéket megadni.

Konstansok

A DEFCONSTANT kulcsszó segítségével lehet „konstans változókat” létrehozni. Ezek mind globális konstansként lesznek definiálva. A DEFCONSTANT alakja:

(defconstant name initial-value-form [ documentation-string ])

Ezután a név felhasználása csak a konstansra való hivatkozásként jelenhet meg, tehát nem lehet függvényparaméter valamint nem köthető hozzá új érték. Ezért gyakran a globális konstansokat +-al kezdik és fejezik be a könnyebb olvashatóság végett.

Értékadás

Ha már megvan a kötés a változóhoz akkor két dolgot tehetünk: Lekérdezzük a jelenlegi értékét vagy újat adunk neki. Új érték adását egy változóhoz Common Lispben a SETF makró segítségével tehetjük meg. Az egyszerűsített alakja a következőképp néz ki:

(setf place value)

Az alábbi példa az x-hez a 10 értéket rendeli
(setf x 10)

De mint ahogy korábban is mondtuk, nem mutable esetben az új kötés nincs hatással a korábbi kötésekre. És hasonlóképp nincs hatással az értékre ami korábban tárolva volt. Ennélfogva, a SETF ebben a függvényben:
(defun foo (x) (setf x 10))

nincs hatással bármilyen foo kívüli értékre. Az új kötés ami a foo hívásakor kreálódott átállítódik 10-re, azonnal kicserléve bármilyen értéket is kapott mint argumentum. Tehát:
(defun foo (x) (setf x 10))

a print 20-at fog kiírni, és nem 10-et mert itt csak y értéke adódott át és a függvényben lévő x értéke változott meg a SETF után. Érdekesség, hogy az alábbi helyett
(setf x 1) (setf y 2)

lehet összevonni is az értékadást
(setf x 1 y 2)

Illetve akár egymásba is ágyazhatjuk:
(setf x (setf y (random 10)))

Itt x és y is ugyanazt a random értéket fogja megkapni.