UWAGA: Jeśli nie miałeś jeszcze doczynienia z językiem lisp a chciałeś go poznać polecam przeczytać najpierw artykuł "Jak zacząć naukę programowania w języku LISP".
Lisp jest to skrót od LISt Processing. Jest to język programowania, który został opracowany przez Johna McCarthiego w latach 50-tych. Został zainspirowany "Rachunkiem Lambda" Alonzo Churcha, od tej nazwy pochodzi charakterystyczne dla lispa wyrzenie lambda, które toworzy nową funkcję anonimową. Lisp jest drugim najstarszym językiem programowania, który jest nadal w użyciu. Drugim jest Fortran.
Common Lisp jest wynikiem próby ustandaryzowania różnych dialektów języka Lisp, przez instytucje ANSI.
Każde wyrażenie w Lispie składa się z wyrażeń (tzw. S-Wyrażenia), które zapisane są za pomocą list. Listę zapisujemy w ten sposób
(+ 1 2 3 4 5)
Lisp używa notacji prefiksowej tzn że funkcje są zapisywane jako pierwszy element listy, natomiast parametry jako następne elementy listy
Wyrażenia można zagnieżdżać
(* 1 (+ 2 (- 4 5)))
Powyższe wyrażenie może być zapiane w postaci algebraicznej jako
1 * (2 + (4 - 5))
Co ciekawe w języku LISP występują tylko dwa rodzaje "bytów", które mogą być użyte jako pierwszy element. Są to funkcje, (np.
+
jest to zwykła funkcja, a nie operator jak w innych językach), które wywołują wszystkie argumenty przed wywołaniem
funkcji. Oraz formy specjalne, które mogą inaczej wywoływać swoje argumenty. Np. if
nie wywoła kodu dopóki
nie spełniony będzie wartunek. W języku Commons Lisp można takie specjalne formy dodawać sememu, za pomocą makr, które działają jak funkcje,
ale operują na argumetnach jak na kodzie. tzn. gdy wywołamy kod (foo (bar 10 (x y))))
jeśli symbol foo
jest to makro to dostanie ono jako argument listę '(bar 10 (x y))
. Makro może przetworzyć tą listę w kod, który zostanie wywołany zamiast
tego co przekazane do makra. Można np. za pomocą makra zamienić wyszstkie wywołania funkcji +
, będzie to symbol, na symbol -
.
Zmienne w Lispie zapisujemy za pomocą funkcji setq
.
(setq x 10)
=> 10
x
=> 10
(setq y (* x 20))
=> 200
Wartość po znakach =>
określa wartość zwracaną przez intepreter Lispa.
W języku Common Lips w odrużnieniu od jezyka Scheme, istnieją dwie przestrzenie nazw na funkcje oraz na zmienne. Dlatego można mieć funkcję oraz zmienną o tej samej nazwie.
Zmienne lokalne zpisujemy za pomocą konstrukcji let
.
(setq x 10)
(let ((x 20))
(* x 30))
=> 600
(let ((zmienna1 wartość)
(zmienna2 wartość))
wyrażenie)
W powyższym kodzie zmienna x została przesłonięta przez konstrukcje let
dlatego w jej wnętrzu zmienna x ma wartość 20 nie 10. Po wyjściu z konstrukcji let
zmienna nie jest już widoczna.
Aby usunąć zmienną używamy funkcji (makunbound 'zmienna)
.
Kostrukcja let*
działa tak jak let z tym że zmienne są przypisywane sekwencyjnie tzn. zmienne mogą się odwoływać do już przypisanych zmiennych.
(let* ((x 10)
(y (+ x 10)))
(* x y))
Aby przypisać stałą należy użyć funkcji defconstant
. Przyjeło się, że stałe zapisuje się ze znakiem +.
(defconstant +pi+ 3.14159265358979)
Zmienne gloalne przypisuje się za pomocą funkcji defvar
lub defparameter
. Przyjeło się zmienne globalne zapisywać ze znakami *.
(defvar *lista* '(1 2 3 4))
(defparameter *zmienna* 10)
Funkcja defvar
w odróżnieniu od defparameter
przypisuje zmiennej wartość tylko raz na samym początku.
Wszystkie zmienne są zmiennymi leksykalnymi, tzn. widocznymi tylko wewnątrz struktury, w której są definiowane, chyba że zapisano innaczej. Za pomocą funkcji special
mamy możliwość zadeklarowania zmiennych jako dynamiczne.
(defun foo (x)
(declare (special x))
(bar))
(defun bar ()
(+ x 3))
W powyższym kodzie jeśli wywołamy funkcje (foo 3)
zostanie zwrócona wartość 6
, chociaż w funkcji bar
nie zdefiniowano zmiennej x
, zmienna przyjmie wartość 3
.
Instrukcja if
.
(if (warunek)
wyrażenie-dla-prawdy
wyrażenie-dla-fałszu)
Jęśli warunek jest spełniony wywołane zostanie pierwsze wyrażenie w przeciwnym wypadku zostanie wywołane drugie wyrażenie. Wyrżenia w instrukcji if
mogą być tylko pojedyncze. Jęśli chcemy użyć kilku wyrażeń należy użyć konstrukcji progn
która zwraca ostatnie wyrażenie.
Instrukcja when
.
(when (warunek) wyrażenie ...)
Wyrażenie w insturkcji when
jest wywoływane jeśli warunek jest spełniony. Jest równoważne poniższemu wyrażeniu if
.
(if (warunek)
(progn
wyrażenie
...))
Wyrażenie unless
.
(unless (warunek) wyrażenie ...)
Wyrażenie w instrukcji unless
jest wywoływane jeśli warunek jest niespełniony. Jest równoważne poniższemu wyrażeniu if
:
(if (not (wyrażenie))
(progn
wyrażenie
...))
Intrukcja cond
.
(cond ((warunek1) wyrażenie ...)
((warunek2) wyrażenie ...)
(t wyrażenie ...))
Warunki w instrukcji cond
sprawdzane są sekwencyjnie. Zwracane jest to wyrażenie dla którego warunek jest spełniony. Dodatkowo można na końcu zastosować warunek t
który zawsze będzie spełniony (jako alternatywa).
Instrukcja case
.
(case zmienna
(wartość1 wyrażenie ...)
(wartość2 wyrażenie ...)
(otherwise wyrażenie ...))
Wywoływane jest wyrażenie dla którego zmienna przyjmuje odpowiednią wartość lub wyrażenie w klauzuli otherwise
.
Pętla dotimes
.
(dotimes (zmienna n opcjionalna-zwracana-wartość)
wyrażenie
...)
Pętla dotimes
wywoła wyrażenie n razy. W każdej iteracji pętli zmienna przyjmie kolejną wartość (od zera do n-1). Opcjionalnie można w instrukcji dotimes
określić wartość zwracaną przez pętle. Jeśli nie określimy tej wartość pętla zwróci wartość nil
.
(dotimes (i 10)
(print i))
0
1
2
3
4
5
6
7
8
9
=> NIL
Powyższy kod wyświetli liczby od 0 do 9 i zwróci wartość nil
.
Pętla dolist
.
(dolist (e '(1 2 3 4 5 6) opcjonalna-zwracana-wartość)
wyrażenie
...)
Pętla dolist
wywoła się tyle razy ile jest elementów listy. W każdej iteracji zmienna e
przyjmie kolejną wartość z listy. Opcjionalnie można określić wartość zwracaną przez pętle. Jęsli nie określimy tej wartość to pętla zwróci wartość nil
.
Pętla do
.
(do ((zmienna1 wartość-początkowa step)
(zmienna2 wartość-początkowa step))
(warunek-zakończenia wartość-zwracan)
wyrażenia
...)
Pętla do
może iterować po kilku zmiennych. Dla każdej z nich określa się wartość początkową oraz wyrażenie step
, które zostanie wywołane po każdej iteracji. Zakończenie pętli następuje wtedy, gdy zostanie spełnony warunek. Pętla zwróci określoną wartość.
(do ((i 0 (incf i))
(j 10 (decf j)))
((zerop j) 'done)
(print (+ i j)))
Powyższy kod iteruje po dwóch zmiennych, z których jedna jest zwiększana w każdej iteracji a druga zmniejszana. Pętla zostanie przerwana jesli zmienna j
dojdzie do zera. Zwrócona zostanie wtedy wartość 'done. W każdej iteracji pętli zostanie wyświetlona suma dwóch liczników "i" i "j" (zostanie wyświetlone 10 razy liczba 10). Makra incf i decf
odpowiednio zwiększają i zmnieją wartość swojego argumentu o 1.
Pętla loop
w postaci prostej przyjmuje postać:
(loop
wyrażenie
...)
jest to pętla nieskończona.
(let ((i 10))
(loop
(when (zerop i) (return))
(print (decf i))))
9
8
7
6
5
4
3
2
1
0
=> NIL
Za pomocą makra return
można przerwać pętle.
Istnieje pożliwość zastosowania rozszeżonej pętli loop
. Poniżej przedstawiono jej możliwości.
Aby iterować po kolejnych liczbach od 1 do n:
(loop
for i from 1 to n)
Aby iterować po liczbach od 0 do n-1:
(loop
for i from 1 below n)
Aby iterować po liście wartości:
(loop
for element in (list 1 2 3 4 5 6) do wyrażenie ...)
Wyrażenie do
określa co ma zostać wywołane w każdej iteracji.
Aby iterować po tablicy, wektorze lub ciągu znaków :
(loop
for c across string collect c)
collect c
tworzy listę z kolejnych wartości c
.
Aby iterować po kilku listach używamy wyrażenie and
:
(loop
for i in list1 and j in list2 collect (list i j))
Aby iterować po tablicy asocjacyjnej używamu notacji kropki:
(loop
for (k . v) in (pairlis '(a b c) '(1 2 3)) do
(format t "~a => ~a~%" k v))
C => 3
B => 2
A => 1
Fukcje format
omówiono tutaj.
Aby iterować po parach cons
używamy wyrażenia on
:
(loop
for para on (list 1 2 3 4 5 6) do
(format t "~a => ~a~%" (car para) (cadr para)))
1 => 2
2 => 3
3 => 4
4 => 5
5 => 6
6 => NIL
Funkcja cadr
zwraca drugi element listy. Funkcje format
omówiono w sekcji.
Aby iterować po kluczach tablicy haszującej można użyć:
(loop
for k being the hash-keys of tablica-haszująca collect k)
Aby iterować po wartościach tablicy hashującej należy użyć poniższego kodu:
(loop
for v being the hash-values of tablica-haszująca collect v)
Poniższy kod wyświetla wszytkie klucze i wartości tablicy hashującej.
(loop
for k being the hash-keys of tablica-haszująca do
(format t "~a => ~a~%" k (gethash k tablica-haszująca)))
Tworzenie listy jeśli warunek jest spełniony:
(loop
for i in (list 0 1 2 3 4 5 6)
when (evenp i) collect i)
=> (0 2 4 6)
Powyższy kod utworzy listę wszystkich liczb parzystych (funkcja evenp
).
Wykonywanie operacji jeśli warunek jest spełniony.
(loop
for i from 0 while (< i 10) collect i)
=> (0 1 2 3 4 5 6 7 8 9)
W powyższym kodzie za pomocą collect
tworzymy listę od 0 do 9.
Wartość prawdy zapisywana jest jako t
. Natomiast wartość fałszu jako pusta lista ()
lub wartość nil
.
Instrukcja and
(jest to specjalny operator).
(and wyrażenie1 wyrażenie2 wyrażenie3 ...)
Powyższy kod zwróci wartość prawdy (t
) jeśli wszystkie wyrażenia zwrócą wartość prawdy. W przypadku kiedy którekolwiek wyrażenie zwróci wartość nil
.
Instrukcja or
(jest to specjalny operator).
(or wyrażenie1 wyrażenie2 wyrażenie3 ...)
Powyższy kod zwróci wartość prawdy jeśli którekolwiek z wyrażeń zwróci wartość prawdy. Jęśli to nastąpi następne wyrażenia nie będą przetwarzane.
Instrukcja not
.
(not wyrażenie)
Instrukcja not
zwróci wartość prawdy jęsli wyrażenie zwróci wartość fałszu, natomiast wartość fałszu jeśli wyrażenie zwróci wartość prawdy.
Symbole definujemy za pomocą funkcji quote
lub specjalnego znaku apostrofu '.
(setq x (quote foo))
x
=> FOO
(setq y 'foo)
y
=> FOO
Symbole zapisywane są dużymi literami, bez zależności od tego jak zostały zdefiniowane.
Podstawowym typem danych w Lispie są listy, listy składają się z par. Aby utworzyć pare używamy konstrukcji cons
. Pierwszy element pary jest to bierzący element, listy natomiast drugi element listy jest to następna para tzn. reszta listy. Ostani element listy musi być listą pustą ()
lub nil
.
(cons 1 2)
=> (1 . 2)
Aby utworzyć listę należy użyć konstrukcji:
(cons 1 (cons 2 (cons 3 nil)))
=> (1 2 3)
'(1 . (2 . (3 . nil)))
=> (1 2 3)
Lub użyć funkcji list lub cytowania listy - znaku apostrofu '.
(list 1 2 3 4)
=> (1 2 3 4)
'(1 2 3 4)
=> (1 2 3 4)
Aby odwołać się do elementów listy należy używamy funkcji car
lub first
która zwraca pierwszy element listy lub cdr
lub rest
zwracająca reszte listy.
(car '(1 2 3 4 5))
=> 1
(cdr '(1 2 3 4 5))
=> (2 3 4 5)
Można też użyć jednej z funkcji: second, third, fourth, fifth, sixth, seventh, eighth, ninth, tenth
, które zwracają odpowiednio elementy od 2 do 10 lub użyć funkcji (nth numer lista)
, która zwraca n-ty element listy. Trzeba pamietać że listy przetwarzane są sekwencyjnie.
Aby skopiwać listę użwamy funcji copy-list
.
(copy-list lista)
Funkcja null
zwraca wartość fałszu jeśli argument jest nie jest listą.
Funkcja listp
zwraca prawdę jeśli argument jest listą.
Funkcja endp
zwraca wartość prawdy jeśli jest to koniec listy.
Za pomocą funkcji push
oraz pop
możemy odpowiedznio odkłożyć i zdjąć elementy ze stosu (listy).
(setq stos nil)
(push 10 stos)
(push 20 stos)
(push 30 stos)
(pop stos)
stos
=> (20 10)
Wektory tworzymy za pomocą funkcji vector
, znaku # lub funkcji make-sequance
. Wektory są indeksowane od zera.
(vector 1 2 3 4)
=> #(1 2 3 4)
#(1 2 3 4)
=> #(1 2 3 4)
(make-sequence 'vector 10 :initial-element 0)
=> #(0 0 0 0 0 0 0 0 0 0)
Funkcja make-sequence
przyjmuje argument kluczowy (keyword argument) :initial-element
, którego wartością jest element jakim zostanie wypełniony wektor.
Aby odwołać się do elementów wektora używamy funkcji aref
lub elt
, aby przypisać jakąć wartość do elementów wektora używamy funkcji setf
która działa jak setq
, z tym że jako drugi argument przyjmuje miejsce gdzie zostanie zapisana wartość przekazana jako drugi argument.
(setq v #(1 2 3 4))
(setf (elt v 1) 10)
v
=> #(1 10 3 4)
Ciągi znaków zapisujemy używając podwójnego cudzysłowu " lub funkcji make-string
.
"jakiś napis"
=> "jakiś napis"
(make-string 10)
=> ""
Aby odwołać się do elementu ciągu należy użyć funkcji elt
lub aref
jak w przypadku wektorów.
Aby przekonwertować ciąg znaków na liczbę można użyć funkcji parse-integer
.
(parse-integer "256")
=> 256
Funkcja char-code
zwraca kod ASCII dla danego znaku.
(char-code #\a)
=> 97
Poszczególene znaki zapisujemy stosując znaki #\
Funkcja code-char
zwraca znak dla danej wartości liczbowej.
(code-char 100)
=> #\d
Aby utworzyć tablicę nalerzy uzyć funkcji make-array
.
(setq array (make-array '(4 4) :initial-element 0))
=> #2A((0 0 0 0) (0 0 0 0) (0 0 0 0) (0 0 0 0))
Powyższy kod utworzy talicę (macierz) cztery na cztery.
Aby odwołać się do elementów tablicy (macierzy) należy użyć funkcji aref
.
(setf (aref array 1 1) 1)
array
=> #2A((0 0 0 0) (0 1 0 0) (0 0 0 0) (0 0 0 0))
Tablice mogą mieć zmieną liczbe elementów, aby utworzyć taką tablicę należy użyć parametru kluczowego :adjustable
z wartością t.
(setq aa (make-array 10 :adjustable t :fill-pointer 0))
Aby dodać element do takiej tablicy należy użyć funkcji (vector-push-extend wartość tablica)
.
Listy asocjacyjne składają się z par klucz wartość.
(setq alist '((a . 1) (b . 2) (c . 3)))
Funkcja assoc
zwraca pare klucz wartość aby odwołać się do wartości należy użyć funkcji cdr
.
(cdr (assoc 'a alist))
=> 1
Dodawanie elementów do list asocjacyjnej.
(setf alist (acons klucz wartość alist))
Aby utworzyć talbicę asocjacyną można użyć funkcji pairlis
.
(setq alist (pairlis '(a b c) '(1 2 3)))
=> ((a . 1) (b . 2) (c . 3))
Aby można było użyć łańcuchów znaków jako kluczy należy funkcji assoc
przekazać parametr kluczowy :test
z wartością #'equal. Funkcja asooc
standardowo do porównań używa funkcji eq.
Listy właściwości są zapisywane za pomocą słów kluczowych.
(setq plist '(:a 1 :b 2 :c 3))
Aby odwołać się do elementów listy właściwości używamy funkcji getf
.
(setf (getf plist :a) 10)
Aby usunąć element z listy używamy funkcji remf
.
(remf plist :b)
Są podobne do tablic asocjacyjnych tylko są bardziej efektywne. Aby utworzyć tablicę haszującą należy użyć funkcji make-hash-table
.
(setq hash (make-hash-table :test #'equal))
Funkcja make-hash-table
przyjmuje dodatkowy parametr kluczowy test, za pomocą którego sprawdzane będą klucze, użycie funkcji #'equal
umożliwi użycie napisów jako kluczy.
Aby odwołać się do w tablicy używamy funkcji gethash
.
(setf (gethash 'foo hash) "Napis")
(setf (gethash 'bar hash) "text")
(gethash 'bar hash)
=> "text"
Aby usunąć element z listy nalezy użyć funkcji remhash
.
(remhash 'foo hash)
Wyczyszczenie całej tablicy następuje po wywołaniu funkcji (clrhash tablica)
.
Funkcja (hash-table-count tablica)
zwraca liczbę wpisów w tablicy haszującej.
Sekwencja jest to grupa typów danych takich jak listy, wektory, tablice, ciągi znaków.
Poniżej przedstawiono funkcje działające na sekwencjach:
make-sequance
(make-sequance typ rozmiar)
Funkcja tworzy sewencje o podanym typie (list, array, string, vector
) o danym rozmiarze. Przyczym można tworzyć tylko jednowymiarowe tablice.
concatenate
(concatenate typ sekwecja sekwencja)
Funkcja łączy dwie sekwecje w jedną, typ może przyjmować jedną z wartości: list, vector, string
.
elt
(elt sekwecja n)
Funkcja zwraca n-ty element sekwecji.
aref
(aref sekwecja n)
Działa tak jak elt
z tym że może być używana dla tablic wielowymiarowych.
subseq
(subseq sekwencja pocątek koniec)
Funkcja zwraca sekwecję zaczynającą się od elementu początek a kończącą się na wartości koniec.
copy-seq
(copy-seq sekwencja)
Zwraca kopie sekwencji.
reverse
(reverse sekwencja)
Odwraca kolejność elementów w sekwecji.
nreverse
(nreverse sekwencja)
Tak jak reverse
z tym że może modyfikować swój argument (jest destrukcyjna).
length
(length sekwencja)
Zwraca liczbę elementów w sekwencji.
count
(count 3 '(2 3 4 5 2 3))
Funcja zwraca liczbę elementów występujących w sekwencji, w tym przypadku 2.
count-if
(count-if #'oddp '(1 2 3 4))
Funkcja zlicza ile razy funkcja przekazana jako drugi parametr zwróci wartość t
czyli prawdę.
count-if-not
(count-if-not #'funkcja sekwencja)
Odwrotna wersja funkcji count-if
.
remove
(remove element sekwencja)
Funkcja usuwa wszystkie wystąpienia danego elementu z sekwencji.
remove-if
(remove-if #'funkcja sekwencja)
Usuwa wszystkie elementy dla których podana funkcja zwróci wartość prawdy (t
).
remove-if-not
(remove-if-not #'funkcja sekwencja)
Odwrotna wersja funkcji remove-if
.
substitute
(substitute zastąpiony zastępowany sekwencja)
Funkcja zastępuje każdy element zastąpiny zastępownym.
substitute-if
(substitute-if zastępowany #'funkcja sekwencja)
Zastępuje wszystkie wystąpienia elementu zastępowany
dla których #'funkcja
zwróci wartość prawdy.
substitute-if-not
(substitute-if-not element #'funkcja sekwencja
Odwrotan wesja fukcji substitute-if
.
remove-duplicates
(remove-duplicates sekwecja)
Usuwa wszystkie powtarzające się elementy z sekwencji.
merge
(merge 'typ (1 2 3) (4 5 6) #'>)
Funkcja łączy dwie sekwecje o pdanym typie ustalając kolejność za pomocą funkcji przekazywanej jako czwarty parametr.
position
(position element sekwencja)
Funkcja zwraca pozycje elementu w sekwencji lub wartość nil
.
find
(find element sekwencja)
Funkcja zwraca znaleziony element w sekwencji lub wartość nil
.
search
(search sekwecja1 sekwencja2)
Funcja zwraca posycje wystąpienia sekwencji1
w sekwencji2
.
sort
(sort sekwencja #'>)
Funkcja srtuje sekwencje za pomocą funkcji przekazywanej jako drugi parametr.
Struktura jest to typ danych który zawiera pola którym można przypisać odpowiednią wartość.
Aby użyć struktury należy ją najpierw zadeklarować, robimy to za pomocą makra defstruct
.
(defstruct struktura pole1 pole2 pole3)
Makro defstruct
automatycznie utworzy funkcje make-struktura
tworzący nowy obiekt strukturalny (rekord) oraz funkcje odwołujące się do poszczególnych pól struktura-pole1, struktura-pole2 i struktura-pole3
.
(setq foo (make-struktura :pole1 1 :pole2 2 :pole3 3))
(setf (struktura-pole1 foo) "Lorem Ipsum")
(print (struktura-pole1 foo))
Powyższy kod tworzy jeden rekord o nazwie foo
następnie przypisuje polu pole1
wartość "Lorem Ipsum", następnie wyświetla jego zawartość. Jeśli nie przypiszemy wartości pola przy tworzeniu rekordu, będzie on miał wartość nil
.
Instnieje możliwość zastosowania wartości domyślnych dla pól aby nie trzeba było określać ich przy tworzeniu rekordu.
(defstruct struktura
(text "domyślny")
(liczba 0)
foo)
Istnieje możliwość tworzenia rekordów bez użycia prametrów kluczowych za pomocą zdefiniowanego konstruktora.
(defstruct (struktura
(:constructor
create-struktura (foo bar baz)))
foo bar baz)
(setq x (create-struktura "Lorem" "Ipsum" "Dolor"))
(print (struktura-foo x))
Isnieje możliwość dziedziczenia tzn. że pola z struktury, która jest potomkiem zostaną przekazane od rodzica.
(defstruct (struktura2
(:include struktura))
quux)
(setq x (make-struktura2 :foo "Lorem" :quux "Amet"))
(print (struktura2-foo x))
=> "Lorem"
(print (struktura2-quux x))
=> "Amet"
Istnieje możliwość zdefiniowania własnej funkcji wyświetlającej rekord.
(defstruct (struktura
(:print-function
(lambda (struct stream depth)
(format t "#<struktura foo: ~a bar: ~a baz: ~a>"
(struktura-foo struct)
(struktura-bar struct)
(struktura-baz struct)))))
foo bar baz)
(setq foo (make-struktura :foo "Lorem" :bar "Ipsum" :baz "Dolor"))
(print foo)
#<struktura foo: Lorem bar: Ipsum baz: Dolor>
Aby utworzyć nowy typ obiektu czyli klasę należy użyć makra defclass
.
(defclass klasa ()
(foo bar baz))
Aby znależć objekt klasy należy użyć funkcji find-class
.
(find-class 'klasa)
=> #<STANDARD-CLASS KLASA>
Aby utworzyć nowy obiekt danej klasy nalezy użyć funkcji make-instance
.
(setq objekt (make-instance 'klasa))
Aby odwołać się do poszczególnych pól obiektu mależy użyć funkcji slot-value
.
(setf (slot-value objekt 'foo) "Lorem")
(setf (slot-value objekt 'bar) "Ipsum")
(setf (slot-value objekt 'baz) "Dolor")
(print (slot-value objekt 'foo))
=> "Lorem"
Jęśli nastąpi odwołanie do pola, za pomocą funkcji slot-value
, zanim zostanie przypisana do niego wartość Common Lisp zgłosi błąd.
Zamiast używać funkcji slot-value
można użyć specjalnej funkcji dostępowej za pomocą parametru kluczowego :accessor
.
(defclass klasa ()
((foo :accessor klasa-foo)))
(setq objekt (make-instance 'klasa))
(setf (klasa-foo objekt) "Lorem Ipsum")
(print (klasa-foo objekt))
=> "Lorem Ipsum"
Można także użyć osobnej funcji do zapisu i odczytu wartości pola za pomocą odpowiednio :writer
i :reader
.
(defclass klasa ()
((foo :reader klasa-get-foo :writer klasa-set-foo)))
(setq objekt (make-instance 'klasa))
(klasa-set-foo "Lorem Ipsum" objekt)
(print (klasa-get-foo objekt))
=> "Lorem Ipsum"
Dodatkowymi opcjami dla pól sią: :documentation
określające dokumentacje pola. :type
określająca typ pola (może być jeden z: real, integer, fixnum, float, bignum, ratinal lub complex
) :initform
określa wartość domyślną dla pola klasy.
Istnieje także możliwość określenia pól statycznych tzn. takich, dla których wartość jest przypisywana dla klasy, wartość dla każdego objektu będzie taka sama. Za pomocą parametru kluczowego :allocation
z przypisaną wartością :class
.
Tak jak w przypadku struktur klaay mogą dziedziczyć pola po swoich rodzicach.
(defclass klasa ()
((foo :accessor klasa-foo)
(bar :accessor klasa-bar)
(baz :accessor klasa-baz)))
(defclass klasa2 (klasa)
((quux :accessor klasa2-quux :initarg :quux)))
(setq objekt (make-instance 'klasa2 :quux "Lorem Ipsum"))
(setf (klasa-foo objekt) "Dolor")
(print (klasa-foo objekt))
=> "Dolor"
W powyższym kodzie użyto opcji dla pola o nazwie :initarg
, dzięki której jest możliwość przypisania polu wartości przy konstruowaniu objektu za pomocą funkcji make-instance
.
Można także użyć opcji dla pola :initform
określającą wartość domyślną dla danego pola.
Istnieje możliwość dziedziczenia po kilku klasach tzw. dziedziczenie wielkorotne.
Funkcje ogólne defiuje się podobnie tak jak zwykłe funkcje za pomocą makra defgeneric
. Funkcje ogólne nie posiadają kodu (ciała). Funkcje ogólne są podobne do funkcji wirtualnych z języka C++.
(defgeneric metoda (parametr1 parametr2))
Funkcji ogólnej opcjionalnie można przypisać dokumentacje.
Metody definuje się za pomocą makra defmethod
.
(defmethod metoda ((self klasa2) (value string))
(setf (slot-value self 'quux) value))
(setq objekt (make-instance 'klasa2))
(metoda objekt "Dolor Sit Amet")
(print (klasa2-quux objekt))
=> "Dolor Sit Amet"
Powyższa metoda przypisuje wartość pola quux
klasa klasa2
.
Metody mogą być "przypisane" do kilku klas na raz, jak w powyższym przypadku do klasy klasa2
i klasy string
.
Metody :before
i :after
są wywoływane odpowiednio przed i wywołaniu metody. Funkcja :around
może być wywoływana zamiast metody, jest wywoływana przed wszystkimi metodami.
(defmethod metoda :before ((self klasa2) (value string))
(print "wywołanie :before metoda"))
(defmethod metoda :after ((self klasa2) (value string))
(print "wywołanie :after metoda"))
(defmethod metoda :around ((self klasa2) (value string))
(print "wywołanie :around metoda")
(call-next-method self value))
(metoda objekt "Foo Bar")
"wywołanie :around metoda"
"wywołanie :before metoda"
"wywołanie :after metoda"
Funkcja :around
musi wywoływać funkcję call-next-method
aby wywołać następną metodę.
Aby usunąc metodę należy użyć poniższego kodu.
(remove-method #'metoda (find-method #'methoda ()
(list (find-class 'klasa2)
(find-class 'sttring))))
Pierwszym argumentem remove-method
jest funkcja ogólna a drugim metoda.
Funkcje definiujemy za pomocą makra defun
. Funkcje istnieją w odzielnej przestrzeni nazw, dzięki czemu możemy tworzyć zmienne i funkcje o takich samych nazwach.
(defun nazwa (parametr1 parametr2)
"Dokumentacja dla funkcji."
wyrażenie
...)
; wywołanie
(nazwa 1 2)
Funkcje mogą mieć parametry opcjinalne:
(defun nazwa (parametr &optional (param2 default param2-p))
wyrażenie
..)
(defun foo (a &optinal (b 0))
(format t "a:~a b:~b~%"))
(foo 1)
"a:1 b:0"
(foo 1 2)
"a:1 b:2"
Wartość default
, w parametrze opcjinalnym, określa wartość jaka zostanie przypisana jesli funkcja zostanie wywołana z jednym parametrem. param2-p
określa czy parametr został przypisany.
Istnieje możliwość aby funkcje przyjmowały różną liczbę argumentów, za pomocą &rest
.
(defun foo (a &rest rest)
(format t "a:~a rest:~a~%" a rest))
(foo 1)
"a:1 rest:NIL"
(foo 1 2)
"a:1 rest:(2)"
(foo 1 2 3)
"a:1 rest:(2 3)"
Aby określić parametry kluczowe dla danej funkcji używamu &key
.
(defun foo (a &key b (c 10 c-p))
(format t "a:~a b:~a c:~a c-p:~a" a b c c-p))
(foo 1)
"a:1 b:nil c:10 c-p:NIL"
(foo 1 :b 20)
"a:1 b:20 c:10 c-p:NIL"
(foo 1 :b 20 :c 30)
"a:1 b :20 c:30 c-p:T"
Zmienna c-p określa czy parametr został przypisany.
Funkcje lokalne tworzymy za pomocą wyrażenia flet
.
(flet ((nazwa1 () wyrażenie ...)
(nazwa2 (param) wyrażenie ...))
wyrażenie
...)
Aby zastosować wywołania rekurenyjne należy użyć wyrażenia labels
.
(labels ((nazwa1 () wyrażenie ...)
(nazwa2 (param) wyrażenie ...))
wyrażenie
...)
(labels ((foo () 2)
(bar () (foo)))
(bar))
W powyższym kodzie funkcja foo
zwraca wartość 2 funkcja bar
wywołuje funkcję foo
. Wewnątrz funkcji labels
wywoływana jest funkcja bar
.
Funkcja rekurencyjna to taka funkcja, w której wnętrzu następuje odwołanie do niej samej tzw. wywołanie rekurencyjne.
Poniżej przedsawiono funkcje rekurencyjną obliczającą silnie.
(defun factorial (n)
(if (zerop n) 1
(* n (factorial (- n 1)))))
Jeżeli w funkcji wywołanie rekurenyjne występuje na końcu wyrażenia, to taka fukcja wykorzystuje tzw. rekurencje ogonową (ang. tail recusion), takie wywołanie rekurencyjne zapisane zostanie jak iteracja za pomocą instrukcji skoku, dzięki czemu funkcja nie będzie wykorzystywać stosu (stanie się bardziej efektywna).
Poniżej przedsawiono funkcję obliczającą silnie z rekurencją ogonową.
(defun factorial (n)
(labels ((f (n product)
(if (zerop n) product
(f (- n 1) (* product n))))) ; wywołanie rekurencyjne ogonowe
(f n 1)))
Funckje anonimowe zapisujemy za pomocą lambda abstrackji.
(lambda (x)
(* x x))
Aby wowołać funkcje anonimową używamy funkcji funcall lub apply.
(funcall (lambda (x) (* x x)) 10)
=> 100
(setq square (lambda (x) (* x x)))
(funcall square 20)
=> 400
(apply #'+ (list 1 2 3 4))
=> 10
Funkcja apply przyjmuje jako drugi argument liste parametrów. Przekazując funkcję jako parametr do innej funkcji należy ją cytować za pomocą znaków #'. Powodem jest to że język Common Lisp ma dwie przestrzenie nazw i w ten sposub system odróżnia funkcje od zmiennych.
W języku Scheme z powodu tego że jest tylko jedna przestrzeń można utworzyć taki kod:
((lambda (x) (display x) (newline)) 10)
=> 10
Jest to anonimowa funkcja jako pierwszy argument. W języku Common Lisp można także użyć alternatywnego zapisu:
(funcall #'(lambda (x) (format t "~S" x)) 10)
=> 10
Funkcje mogą być przekazywane jako parametry, mogą też być zwracane przez funkcję.
(defun complement (fun)
(lambda (x)
(not (funcall fun x))))
Funkcja ta jest zdefiniowana w standardze Common Lisp.
Aby przekazać funkcję jako parametr nalerzy użyć cytowania funkcji - znaków przed nazwą funcji #'.
(mapcar #'sqrt '(1 2 3 4))
Funkcje mapujących używamy jeśli chcemy iterować po jakiejś liście lub sekwencji i przypisać do każdego elementu wartość zwracaną przez funkcje.
(defun square (x)
(* x x))
(mapcar #'square '(1 2 3 4 5 6))
=> (1 4 9 16 25 36)
(mapcar (lambda (x) (* x x)) '(1 2 3 4 5))
=> (1 4 9 16 25)
Ze względu że Common Lips ma dwie przestrzenie nazw, można uruchomić poniższy kod:
(let ((square '(1 2 3 4 5 6)))
(mapcar #'square square))
Funkcje mapujące:
Są to funkcje które operują na listach i przyjmują funkjce jako argument.
mapcar
(mapcar #'funkcja lista1 lista2 ...)
Funkcja przekazywana jako drugi parametr musi mieć tyle argumentów ile przekazano list.
mapcan
(mapcan #'funkcja lista2 lista2 ...)
Destrukcyjna wersja mapcar
.
map
(map typ funkcja lista1 lista2 ...)
Funkcja działa tak jak mapcar z tym że paramter typ
może przyjmować wartości np.: list, array, vector, string.
maplist
(maplist #'funkcja lista1 lista2 ...)
Funkcja działa tak jak mapcar
z tym że do funkcji przekazywane są pary zamiast elementów list.
maphash
(maphash #'(lambda (k v) (format t "~a => ~a~%" k v)) hash-table)
Funkcja iteruje po elementach talibcy haszującej, zwraca wartość nil.
(defun maph (fun hash)
(let (result)
(maphash (lambda (k v) (push (funcall fun k v) result)) hash)
(nreverse result)))
Powyższa funkcja zwraca listę utworzoną z kolejnych wywołań funkcji fun
do elementów tablicy haszującej.
(let ((x 0))
(defun foo ()
(setq x (incf x))
x))
(foo)
=> 1
(foo)
=> 2
(foo)
=> 3
W powyższej funkcji zmienna x jest zamknięta wewnątrz funkcji foo
(funkcja może korzystać ze zmiennej x) chociaż zmienna jest już niedostępna poza konstrukcjią let
.
Makra w odróżnieniu od funkcji nie ewaluują swoich argumntów, zamiast tego do makra przekazywane jest całe s-wyrażenia które nastepnie zostaną przetworzone w celu utworzenia innego s-wyrażenia. Przy wywoływaniu makra początkowe wyrażenie przekazywane jest do makra, które po przetworzeniu zwraca nowe wyrażenie. Takie wyrażenie następnie zostaje wywołane. Makro definujemy za pomocą makra defmacro
. W odróżnieniu od funkcji makr nie możemy przekazyać jako parametry do innych funkcji.
(defmacro def (args &body body)
`(defun ,args ,@body))
Powyższy kod tworzy makro tworzące nową funkcje, używa ono znacznika &body
dla określenia ciała definjowanej funkcji. Powyższe makro używa składni tylnego apostrofu (backquote) `. Tylny apostrof działa podobnie do normalnego apostrofu, który cytuje swój argument. Wszystkie wyrażenia poprzedzone przecinkiem zostaną wywołane, z wyrażeń poprzedzonych znakiem przecinka i małpy ,@
zostaną usunięte nawiasy np.: `(print 1 2 3 ,(+ 1 2 (* 3 4)))
zostanie ewoluowane przez interpreter lispa jako (PRINT 1 2 3 15)
.
Poniżej przedstawiono makro definiujące pętle while znaną z innych języków programowania.
(defmacro while (test &body body)
`(do ()
((not ,test))
,@body))
; użycie powyższego makra
(let ((x 0))
(while (< x 10)
(print x)
(setq x (1+ x))))
0
1
2
3
4
5
6
7
8
9
=> NIL
Poniższa makro definuje pętle for.
(defmacro for ((var start stop &optional return-value) &body body)
(let ((gstop (gensym)))
`(progn
(do ((,var ,start (1+ ,var))
(,gstop ,stop))
((> ,var ,gstop) ,return-value)
,@body))))
(for (i 10 20) (print i))
1
2
3
4
5
6
7
8
9
10
=> NIL
(reverse (let (result)
(for (i 1 10 result)
(push i result))))
=> (1 2 3 4 5 6 7 8 9 10)
Powyższe makro używa funkcji gensym
, która generuje unikalną nazwę dla zmiennej, stosuje się to z powodu tego, że wewnątrz pętli for
użytkownik może zdefiniować taką samą zmienną jaką użyliśmy wewnąrz makra, co doprowadziłoby do błędnego działania. Znacznik &optional
definiuje argument opcjionalny tak jak w przypadku funkcji.
Za pomocą funkcji macroexpand
można zobaczyć jak wygląda wygenerowany przez makro kod. Jest to przydatne w czasie debugowania makr. Funkcja macroexpand
generuje kod rekurencyjnie aż do napotkania tylko funkcji. Aby wyświetlić tylko jeden poziom makra należy użyć funkcji macroexpand-1
.
(macroexpand '(for (i 10 20) (print i)))
(BLOCK NIL
(LET ((I 10) (#:G8442 20))
(TAGBODY #:G8443 (IF (> I #:G8442) (GO #:G8444)) (PRINT I)
(PSETQ I (1+ I)) (GO #:G8443) #:G8444 (RETURN-FROM NIL (PROGN)))))
=> T
(macroexpand-1 '(for (i 10 20) (print i)))
(DO ((I 10 (1+ I)) (#:G8445 20)) ((> I #:G8445)) (PRINT I))
=> T
Nazwa zmiennej #:G8445
zystała wygenerowana przez funkcje gensym
.
format
Funkcja format
działa podobnie do funkcji prinf
języka C. Pierwszy prametr przyjmuje wartość nil, t
lub strumień, jeśli zostanie przekazana wartość nil
wynikowy ciąg znaków zostanie zwrócony przez funkcje format
jeśli przekazana zostanie wartość t
wynikowy ciąg znaków zostanie wypisany na standardowe wyjście, może być także przekazany strumień wyjściowy. Drugim parametr musi być ciąg znaków zawierający odpowiednie znaczniki (poprzdzone znakiem tyldy ~). Kolejne parametry zostaną wstawione w odpowiednie miejca w ciągu znaków.
Poniżej przedstawiono listę znaczników (wielkość liter nie ma znaczenia):
~C
- wyświetla znak
~R
- słowna reprezentacja liczby (w języku angielskim)
~@R
- słowna reprezentacja rzymska
~A
- reprezentacja jako ciąg znaków
~S
- reprezentacja jako ciąg znaków który może być wczytany za pomocą funkcji read
~(znaczniki~)
- wyświetla parametr małymi literami
~:@(znaczniki~)
- wyświetla parametr dużymi literami
~@(znaczniki~)
- pierwszy znak zostanie wyświetlony dużymi titerami
~:(znaczniki~)
- każde słowo wielką literą
~%
- nowa linia
~5%
- 5 nowych lini
~~
- znak tyldy
~D
- liczba dziesiętna
~10D
- liczba dziesiętna wyrównana do 10
~X
- liczba szestnastkowa
~O
- liczba ósemkowa
~B
- liczba binarna
~F
- liczba float z przecinkiem
~,5F
- liczba float z 5 miejscami po przecinku
~{znaczkiki~}
- iteracja - parametr musi być listą
(format t "~{~a ~}~%" (list 1 2 3 4 5))
1 2 3 4 5
=> NIL
~@{znaczniki~}
- iteracja - przetwarza parametry jak listę
(format t "~{~a ~}~%" 1 2 3 4 5)
1 2 3 4 5
=> NIL
~[forma1~;forma2~;..~;N~]
- parametr wybiera forme
(format t "~[zero~;jeden~;dwa~]~%" 1)
"jeden"
=> NIL
~:[forma-fałsz~;forma-prawda~]
- wybiera formę w zalężności czy parametr przyjmuje wartość t
lub nil
(let ((x 10))
(format nil "~:[fałsz~;prawda~]" (zerop x)))
=> "fałsz"
~*
- omija jeden parametr
~:*
- cofa się o jeden parametr (przetwarza jeden paramatr dwa razy)
V
- wstawia wartość parametru
(format nil "~V%" 5)
=> "
"
Aby otworzyć plik należy użyć funkcji open
.
(setq uchwyt (open "nazwa-pliku" :direction :output))
parametr kluczowy :direction
może przyjmować jedną z dwóch wartości :input
lub output
dla których funkcja odpowiednio otwiera plik wejściowy (z którego można czytać) i wyjściowy (do którego można zapisywać).
Funkcji open
można przekazać parametr kluczowy :if-exist
któremu można przekazać wartość :supersede
- zastępującą zawartość, :append
- dodająca zawartość na końcu pliku.
Za pomocą funkcji read
, read-line
, read-char
i write-byte
można odpowiednio przeczytać wyrażenie lispowe ze strumienia, przeczytać linie, przeczytać pojedynczy znak oraz przeczyać bajt danych. Funkcje przyjmują jako drugi opcjionalny paramer strumień wejściowy.
Analogicznie za pomocą funkcji write
, write-line
, write-char
i write-byte
można zapisać wyrażenie do strumienia, zapisać linie, zapisać znak oraz zapisać bajt danych. Funkcja przyjmuje jako drugi opcjionalny parametr strumień wyjściowy. Można użyć także funkcji print
.
(let ((file (open "plik" :direction :input)))
(write-line (read-line file))
(close file))
Po zakończeniu przetwarzania strumienia należy wywołać fukcję close
która zamyka strumień. Strumienie mogą być buforowane, dlatego zapis na dysk może nastąpić dopiero po zamknięciu strumienia.
Funkcja listen
sprawdza czy jest jakiś znak do odczytania ze strumienia.
Makro with-open-file
zamyka plik automatycznie (także gdy nastąpi błąd).
(with-open-file (file "plik" :direction :input)
(write-line (read-line file)))
Poniżej przedstawiono funkcję która zwraca listę lini z pliku.
(defun read-lines (filename)
"Function read file and return list of lines."
(with-open-file (file filename :direction :input)
(loop while (listen file)
collect (read-line file))))
Funkcja file-length
zwraca wielkość pliku.
Funkcja delete-file
usuwa plik.
Funkcja rename-file
zmienia nazwę pliku.
(rename-file "old-name" "new-name")
Strumienie w pamięci.
Za pomocą makra with-input-from-string
można utworzyć strumień z ciągu znaków.
(with-input-from-string (stream "\"Lorem Ipsum Dolor Sit Amet\"")
(write (read stream)))
"Lorem Ipsum Dolor Sit Amet"
=> "Lorem Ipsum Dolor Sit Amet"
Za pomocą makra with-output-to-string
można zapisywać łańcuchy znaków do strumienia.
(with-output-to-string (stream)
(princ "Lorem Ipsum Dolor Sit Amet" stream))
=> "Lorem Ipsum Dolor Sit Amet"
Za pomocą funkcji unwind-protect
można wywołać określony kod nawet w przypadku jeśli wystąpił błąd.
(let (file)
(unwind-protect
(progn
(setq file (open "file" :direction :input))
(loop while (listen file) do
(write-line (read-line file))))
(when file (close file))))
Makro with-open-file
jest zdefinowane właśnie za pomocą unwind-protect
.
Błąd można prosto zasygnalizować za pomocą funkcji error
. Błąd w programie przerywa jego działanie. Jeśli kożystamy z interpretera Lispu w czase wystąpienia błędu nastąpi wejście do Debugera.
(error "Wystąpił błąd w programie!")
Istnieje także możliwość defiowania własnych błędów (ang. conditions) za pomocą makra define-condition
. Błędy działają podobnie do klas, istnieje możliwość dziedziczenia. Wszystkie defiowane błędy powinny dziedziczyć po error
. Błędy działają podobnie do wyjątków w takich językach jak C++, Java czy Python.
(define-condition Foo(error)
((co :initarg :co :initform "Błąd Foo" :reader co))
(:report (lambda (condition stream)
(format stream "Nastąpił błąd: ~@(~A~)." (co condition))))
(:documentation "Podstawowy Błąd Foo"))
W powyższym kodzie użyto znacznika :report
na określenie funkcji za pomocą której zostanie wyświetlona informacja o błędzie. Funkcja ta przyjmuje dwa argumenty - objekt błędu oraz strumień. Znacznik :documentation
określa dokumentacje do błędu. Pola w błędach definiujemy tak jak w przypadku klas.
Aby wywołać błąd należy użyć funkcji error
tak jak w prostym przypadku z ciągiem znaków.
(error 'Foo :co "Jakiś błąd w programie")
*** - Nastąpił błąd: Jakiś błąd w programie.
Aby użyć dziedziczenia błędów należy zastosować poniższy kod.
(define-condition Bar(Foo)
((dlaczego :initarg :dlaczego :reader dlaczego))
(:report (lambda (condition stream)
(format stream "Error: ~(~A~) jest błędny. Dlaczego? ~@(~A~)."
(co condition)
(dlaczego condition)))))
(error 'Bar :co "paskudny błąd" :dlaczego "nieznany")
*** - Error: paskudny błąd jest błędny. Dlaczego? nieznany.
Za pomocą makra ignore-errors
można ignorować błędy np:
(ignore-errors (error "Ten błąd zostanie zignorowany."))
=> NIL
Programy napisane w Common Lispie można zapisywać w plikach. Aby wczytać plik należy użyć funkcji load
.
(load "nazwa_pliku.lisp")
Aby utworzyć nowy pakiet należy użyć makra defpackage
.
(defpackage :Pakiet
(:documentation "To jest dokumentacja tego Pakietu.")
(:use :common-lisp)
(:export :range :factorial))
(provide "Pakiet")
(defun factorial (n)
"Return factorial of n (n!)."
(labels ((f (n product)
(if (zerop n) product
(f (- n 1) (* product n)))))
(f n 1)))
(defun range (n)
"return list of n integers."
(let (result)
(dotimes (i n)
(push i result))
(nreverse result)))
Powyższy pakiet definuje dwie funkcje factorial
oraz range
które są zadeklarowane w pakiecie za pomocą parametru kluczowego :export
. Znacznik :use
określa z których pakietów będziemy korzystać. Znacznik :documentation
określa dokumentacje pakietu. Powyższy kod należy umieścić w pliku Pakiet.lisp
Aby móc używać tak zdefinowanego pakietu należy użyć funkcji require
, należy także określić za pomocą funkcji in-package
, że będziemy kożystać z danego pakietu. W przypadku nie wywołania funkcji in-package
musielibyśmy używać pełnej nazwy każdej z funkcji np. Pakiet:factorial
.
(require :Pakiet)
(in-package :Pakiet)
(factorial 10)
=> 3628800
(reduce #'* (mapcar #'1+ (range 10)))
=> 3628800
Zamiast funkcji require
moglibyśmy także użyć funkcji load.