Wprowadzenie do języka Common Lisp
fr3
1 Historia i opis języka
2 Co jest takiego niesamowitego w Common Lispie?
2.1 Skoro Lisp jest taki dobry, dlaczego nikt go nie używa?
3 Kompilator/interpreter oraz IDE
4 Yin i yang, czyli atom i lista - podstawowe typy danych
5 Składnia i podstawowe funkcje
5.2 cons
5.3 list
5.4 car i cdr
5.5 defun - definicja funkcji
5.6 quote
5.7 eval
5.8 print
5.9 if
5.10 cond
5.11 Funkcje matematyczne i relacyjne.
5.12 Równość
5.13 eq
5.14 eql
5.15 =
5.16 equal
5.16.1 Cons
5.16.2 Tablice
5.16.3 Pathnames (ścieżki)
5.16.4 Inne (struktury, hash tablice, instancje, ...
5.17 equalp
5.18 append
5.19 First,second,third, cadar, nth i nthcdr
6 Zmienne
6.20 defparameter - definicja zmiennej dynamicznej
6.21 setf - ustawianie wartości zmiennej
7 Argumenty opcjonalne i słownikowe
7.22 Opcjonalne
7.23 Nieskończona ilość argumentów
7.24 Argumenty słownikowe
8 Dynamiczne i statyczne typowanie oraz Garbage Collector
9 Inne typy danych
9.25 String
9.25.5 format
9.26 Tablica
9.26.6 make-array
9.26.7 array-rank
9.26.8 aref
9.27 Hash tablica
9.27.9 make-hash-table
9.27.10 gethash
9.28 Instancje obiektów
9.29 Funkcje
9.29.11 lambda
9.29.12 function
9.29.13 #'
10 Namespace (środowisko)
10.29.14 apply
10.29.15 funcall
10.30 Generatory
11 Iteracja i mapowanie
11.31 loop
11.32 count-if
11.33 map
11.34 mapcar
11.35 mapc
12 Makra
12.35.16 when
12.36 progn
12.36.17 kiedy
12.37 list*
12.37.18 macroexpand-1
12.38 Backquote
12.38.19 string-case
13 O czym nie powiedziałem i o czym nie jest ten kurs
13.39 O czym nie powiedziałem, a co jest ważne
13.40 Czym nie jest ten kurs
14 Linki
14.40.20 Successfull Common Lisp
14.40.21 Practical Common Lisp
14.40.22 On Lisp
14.40.23 Cliki
Historia i opis języka
Common Lisp jest najpopularniejszym dialektem Lispa, języka stworzonego
w 1958 roku przez Johna McCarthy'ego.
Pierwotnie język powstał jako narzędzie opisu rekursywnych funkcji
wyrażeń symbolicznych ("Recursive Functions of Symbolic Expressions
and Their Computation by Machine, Part I") i w zamyśle nie miał w
ogóle zostać zaimplementowany na komputerze.
Dokładniejszy opis wczesnej historii Lispa i szczegóły jego
implementacji można przeczytać (po angielsku) na stronie Paula
Grahama.
Co jest takiego niesamowitego w Common Lispie?
Makra. Możliwość tworzenia mini-języków najlepszych do danego
problemu. Możliwość abstrakcji najczęściej wpisywanych rzeczy - nie
wszystko da się załatwić klasą, metodą czy funkcją.
Wydajność. Zarówno ta programisty (można znaleźć wiele ludzi relacji,
którzy przepisali program napisany do Lispa - z 100k linii robiło sie
10k) jak i obliczeniową (generalnie Common Lisp ma najlepsze
zarządzanie pamięcią ze wszystkich języków, a CMUCL czy Allegro Common
Lisp bardzo często pokonuje program w C pod względem wydajności
numerycznej).
Spójność. Kod praktycznie wszędzie wygląda tak samo, wszyscy tak samo
indentują kod (brak jakichkolwiek odpowiedników wojen oto, gdzie
stawiać '{' itd) - bardzo dobra czytelność.
Skoro Lisp jest taki dobry, dlaczego nikt go nie używa?
Używa go NASA przy obsłudze misji kosmicznych, AMD przy budowie
procesorów, YahooStore, Boeing, został użyty przy produkcji animacji do Władcy
Pierścieni, ....
Większą listę można znaleźć tu i tu.
Gra MMORPG Vendetta jest napisana z użyciem języka Common Lisp razem z Erlang.
Kompilator/interpreter oraz IDE
Najpopularniejszym IDE do Lispa jest edytor Emacs w połączeniu ze
SLIME.
Kompilatorów i interpreterów jest dość dużo - jednak na sam początek polecam ściągnąć
gotową dystrybucję LispBox z interpreterem CLISP dla twojego systemu.
Uwaga: chociaż można odnieść wrażenie, że Lisp jest jedynie
interpretowanym językiem, ze sprawdzaniem błędów jedynie w runtime, nie
jest to prawdą - większość błędów zostanie wykryta już podczas
kompilacji, przerywając ją - tak samo jak w C/C++, Delphi, ...
Kompilator to np. Allegro Common Lisp (płatny - zdecydowanie najlepszy
ze wszystkich wymienionych), LispWorks, SBCL, CMUCL, Embedded
Common Lisp, Corman Lisp, Gnu Common Lisp, ...
Po uruchomieniu środowiska zobaczysz linię komend REPL (skrót od
"Read-Eval-Print-Loop" czyli "Wczytaj-Wykonaj-Wypisz-Powtórz"), w
której możesz bezpośrednio wpisywać polecenia.
W przykładach dodałem "=>" żeby zaznaczyć wartość zwracaną - nie jest
on rzeczywiście wypisywany w SLIME.
Kilka użytecznych skrótów klawiszowych: (C - ctrl; M (meta) - alt)
- M-p (albo C-strzałka w górę) - poprzednia komenda
- M-n (albo C-strzałka w dół) - następna komenda w historii
- C-M-p (albo C-M-strzałka w górę) - wraca do nawiasu otwierającego '('
- C-M-n (albo C-M-strzałka w dół) - przeskakuje do nawiasu zamykającego ')'
- C-M-@ - podświetla całą forme (kursor musi byc na nawiasie otwierającym)
- C-M-k - wycina całą forme (do schowka)
- C-y (yank) - wkleja zawartość schowka.
(standardowe ctrl-c ctrl-v ctrl-x można uzyskąć w tzw. CUA-mode, w
emacsie 22 dostępnym domyślnie, w emacsie 21 trzeba zainstalować to
samemu)
Dobrym pomysłem jest przeczytanie wprowadzenia do Emacsa.
Yin i yang, czyli atom i lista - podstawowe typy danych
Kod i dane zbudowane są w Lispie z tej samej struktury danych: listy.
Znak '(' oznacza początek listy, znak ')' koniec.
Zawartością listy może być inna lista, albo atom.
Wszystko jest albo listą, albo atomem.
(1 2 3)
Lista złożona z trzech liczb.
(1 (2 3) 4)
Lista złożona z trzech elementów - liczby 1, dwu elementowej listy oraz
liczby 4.
Podstawowym budulcem listy jest struktura zwana cons.
Cons składa się z dwóch pól: car i cdr (są to nazwy dwóch
rejestrów komputera IBM 704, na którym została zaimplementowana
pierwsza wersja Lispa).
Jako struktura zapisana w języku C można to zapisać tak:
struct Cons{
void *car;
void *cdr;
}
Każda komórka listy to jeden cons - w polu car znajduje się
dana, w polu cdr - adres następnej komórki cons.
NULL (NIL) w polu cdr oznacza koniec listy.
Składnia i podstawowe funkcje
Wywołanie funkcji f z argumentem arg jest zapisywane jako:
(f arg)
Jest to odpowiednik zapisu f(arg)
w innych językach programowania.
Znając już zapis, możemy poznać podstawowe funkcje:
cons
(cons car cdr)
Funkcja cons zwraca komórkę cons.
CL-USER> (cons 10 23)
=> (10 . 23)
Komórki cons, które nie są lista (cdr nie jest prawidłowym
wskaźnikiem) są wypisywane z '.'.
list
(list arg1 arg2 arg3 ...)
Funkcja list przyjmuje dowolną liczbę argumentów, i zwraca
listę z nimi.
CL-USER> (list 1 2 3 4 5)
=> (1 2 3 4 5)
Każda lista jest zbudowana z połączonych komórek cons, więc
zapis
(list 1 2 3)
jest równoznaczny
(cons 1 (cons 2 (cons 3 nil)))
Można to łatwo sprawdzić w REPL:
CL-USER> (cons 1 (cons 2 (cons 3 nil)))
=> (1 2 3)
CL-USER> (list 1 2 3)
=> (1 2 3)
car i cdr
(car arg)
i (cdr arg)
Funkcja car zwraca car komórki cons podanej w
argumencie.
CL-USER> (car (cons 2 3))
=> 2
Funkcja cdr przeciwnie - zwraca komórkę cdr.
CL-USER> (cdr (cons 2 3))
=> 3
Pamiętając o tym, że listy są zbudowane z połączonych komórek
cons, możemy użyc car i cdr żeby dostać pierwszy
element listy oraz listę bez pierwszego elementu.
CL-USER> (car (list 1 2 3))
=> 1
CL-USER> (cdr (list 1 2 3))
=> (2 3)
defun - definicja funkcji
(defun nazwa-funcji argumenty kod)
defun jest makrem służacym do definicji funkcji.
CL-USER> (defun dwie-listy (a b)
(list (list a)
(list b)))
=> DWIE-LISTY
Zadeklarowaliśmy właśnie funkcję dwie-listy, zwracającą
dwu-elementową listę z listami zawierającymi argumenty.
Możemy ją wywołać w ten sam sposób jak funkcje wbudowane:
CL-USER> (dwie-listy 10 20)
=> ((10) (20))
quote
(quote argument)
Funkcja quote zwraca nie ewaluowany argument.
CL-USER> (quote (list 1 2 3))
=> (LIST 1 2 3)
(quote cos)
można zapisać w krótszej formie jako
'cos
'
jest to tzw. reader's macro (makro funkcji read)
eval
(eval forma)
Funkcja eval zwraca wynik wykonanej formy.
CL-USER> (list 'cons 50 60)
=> (CONS 50 60)
CL-USER> (eval (list 'cons 50 60))
=> (50 . 60)
(print co)
Funkcja print wypisuje swój argument na standardowe wyjście.
if
(if test jeśli-tak jeśli-nie)
Instrukcja warunkowa. Wykonuje test - jeśli zwróci prawdę, wykonuje
kod jeśli-tak, w przeciwnym wypadku jeśli-nie.
W języku Common Lisp prawdą jest wszystko oprócz symbolu nil (który
jest jednocześnie pustą listą).
Podstawowym symbolem reprezentującym prawdę jest symbol t
CL-USER> (if t
"tak"
"nie")
=> "tak"
CL-USER> (if nil
"tak"
"nie")
=> "nie"
CL-USER> (if '()
"tak"
"nie")
=> "nie"
CL-USER> (if (list)
"tak"
"nie")
=> "nie"
cond
(cond form1 form2 ...)
Cond jest makrem wykorzystującym funkcję if - jest to odpowiednik
if...else if...else w innych językach.
Car każdej formy jest ewaluowany - jeśli wynik jest prawdą, kod
będący cdr jest wykonywany, w przeciwnym cond przechodzi do
następnej formy.
CL-USER> (cond ((= 1 2) (print "1=2"))
((= 2 3) (print "2=3"))
(t (print "t jest prawda")))
"t jest prawda"
=> "t jest prawda"
CL-USER> (cond ((= 1 2) (print "1=2"))
((< 2 3) (print "2<3"))
(t (print "t jest prawda")))
"2<3"
=> "2<3"
Dlaczego dwa razy zostało wypisane to samo? Jedna wartość jest tekstem
wypisanych przez print na standardowe wyjście - druga to wartość
zwrócona, która jest z kolei wypisywana przez REPL.
Funkcje matematyczne i relacyjne.
Wyrażenia matematyczne są w Lispie zapisywane w Notacji Polskiej (prefixowej) -
najpierw jest operator, później argumenty.
Wyrażenie (+ 2 2)
jest równoznaczne "2+2".
CL-USER> (+ 2 3 4)
=> 9
CL-USER> (* 2 3 4)
=> 24
CL-USER> (/ 2 3 4)
=> 1/6
CL-USER> (- 2 3 4)
=> -5
CL-USER> (< 2 3 4)
=> T
CL-USER> (> 2 3 4)
=> NIL
Część ludzi uważa tą notację za lepszą, część woli jednak tradycyjny
zapis.
Jeśli należysz do drugiej grupy, można ściągnąć makro readera do
zapisu infixowego z CMU CL AI Repository.
Po jego załadowaniu można pisać:
CL-USER> #I(2+2)
=> 4
Powstrzymaj się jednak z instalacją na początku nauki.
Równość
Równość w CL niejedno ma imię...
eq
(eq a b)
Zwraca t, jeśli a i b to dokładnie ten sam obiekt.
Na 99% jest to zaimplementowane jako porównanie adresu obiektu.
eql
(eql a b)
Eql zwraca t jeśli:
- (eq a b) zwraca t
- a i b są podtypem typu number, są tego samego typu i mają tą samą wartość.
- a i b są znakami i są tym samym znakiem. (wielkość liter ma znaczenie)
(= a b ...)
=
Wszystkie argumenty muszą być podtypem typu number.
Zwraca t, jeśli a = b (matematycznie)
Przykład:
CL-USER> (eql 1.0 1)
=> NIL
CL-USER> (= 1.0 1)
=> T
CL-USER> (eql #C(1.0 0) 1.0)
NIL
CL-USER> (= #C(1.0 0) 1.0)
T
#C(część_rzeczywista część_urojona) - zapis liczb
zespolonych.
(można także użyc funkcji complex)
equal
(equal a b)
Symbole, numery i znaki
equal zwraca t jeśli argumenty są symbolami które eq (dla których eq zwróci t), liczbami które
eql, albo znakami które eql.
Cons
Dla cons, equal jest zdefiniowany rekursywnie - t jeśli dwa car equal
i dwa cdr equal.
Oznacza to, że można używać equal do porównywania list.
Tablice
Dwie tablice są równe tylko jeśli eq zwróci t, z jednym wyjątkiem -
stringi i bit-wektory są porównywane po kolei dla wszystkich elementów
używając eql. Jeśli jedna z tablic ma fill pointer (wskaźnik
wypełnienia), limituje on maksymalną ilość porównanych elementów.
Podczas porównywania stringów, wielkość liter ma znaczenie.
Pathnames (ścieżki)
Dwie ścieżki equal wtedy i tylko wtedy gdy wszystkie elementy (host,
urządzenie, i tak dalej) są takie same. To, czy wielkość liter w
ścieżce ma znaczenie zależy od implementacji. Jeśli obie ścieżki equal
ich działanie powinno być identyczne.
Inne (struktury, hash tablice, instancje, ...
Dwa obiekty equal tylko wtedy gdy eq zwróci t.
equalp
Typ Zachowanie
number używa =
character używa char-equal - wielkość znaków nie ma znaczenia
cons zdefiniowane rekursywnie
bit vector porównuje elementy
string porównuje elementy
pathname używa equal
struktura struktury muszą miec ten sam typ i każdy musi być equalp
inna tablica porównuje elementy
hash table test i ilość elementów musi być ta sama; jeśli jest, porównuje elementy
Inne obiekty używa eq
append
(append lista1 lista2 lista3 ...)
Append łączy wszystkie listy w jedną i zwraca ją.
CL-USER> (append (list 1 2 3) (list 4 5 6) (list 7 8 9))
=> (1 2 3 4 5 6 7 8 9)
Wszystkie listy są kopiowane, oprócz ostatniej.
Zatrzymaj się w tym miejscu i spróbuj, jako ćwiczenie, napisać funkcję
zwracająca połączone listy bez ich pierwszego elementu.
CL-USER> (polacz-sublisty (list 1 2 3 4) (list 9 8 7 6))
=> (2 3 4 8 7 6)
CL-USER> (polacz-sublisty (list "a" "b" "c") (list "d" "e" "f"))
=> ("b" "c" "e" "f")
First,second,third, cadar, nth i nthcdr
Zamiast pisać car można napisać first. W podobny sposób:
(car(cdr obj)) = (second obj)
(car(cdr(cdr obj)) = (third obj)
Jeśli chcemy dostać n-ty car, najlepiej napisać:
(nth n lista)
Należy pamiętać, że pierwszy element ma indeks 0.
Tak samo dla cdr i nthcdr.
Zamiast pisać (car (car arg)) możemy napisać (caar arg). Literka 'd'
oznacza że w tym miejscu jest cdr, a 'a' - car.
(cadar arg) = (car (cdr (car arg)))
Lepiej jednak unikać tych funkcji i zamiast nich używac first...tenth
oraz nth/nthcdr.
Wyjątkiem jest tutaj caar (i caaar) - jasno widać, że pobieramy wartość z
zagnieżdżonej listy.
Odpowiednikiem funkcji cdr jest rest, jednak nie robi to wielkiej różnicy.
Zmienne
Istnieją dwa oddzielne typy zmiennych - leksykalne i dynamiczne
(nazywane także specjalnymi).
Zmienna dynamiczna zawsze ma tylko jedną wartość - jej zmiana w
jakimkolwiek miejscu w programie zmienia jej wartość globalną.
defparameter - definicja zmiennej dynamicznej
(defparameter zmienna wartosc)
definiuje zmienną
specjalną.
setf - ustawianie wartości zmiennej
(setf zmienna wartosc)
- ustawia wartość zmiennej.
CL-USER> (defparameter a 333)
=> A
CL-USER> (defun funkcja1 () (setf a 90))
=> FUNKCJA1
CL-USER> (defun funkcja (a)
(print a)
(funkcja1)
(print a))
=> FUNKCJA
CL-USER> (funkcja 20)
20
90
=> 90
CL-USER> a
=> 333
CL-USER> (funkcja1)
90
CL-USER> a
90
Zmienna dynamiczna a zawsze istnieje ma tą samą wartość, w
każdym miejscu programu.
Funkcja z zadeklarowanym argumentem o takiej samej nazwie jak zmienna
specjalna zapisuje obecną wartość zmiennej i przywraca ją na końcu, a
następnie zmienia globalną wartość zmiennej a - w całym środowisku.
Zmienna leksykalna istnieje tylko lokalnie - ta sama nazwa zmiennej
może dać inną wartość w zależności od 'położenia'.
Zmienne lokalne tworzą się automatycznie jako argumenty funkcji, albo
przy użyciu let:
CL-USER> (let ((b 50) (d 10))
(print b)
(print d))
50
10
=> 10
CL-USER> (let ((d 10))
(defun f1 ()
(setf d 90)))
=> F1
CL-USER> (defun f2 (d)
(print d)
(f1)
(print d))
=> F2
CL-USER> (f2 55)
55
55
=> 55
Jak widać, zmiana wartości zmiennej d nie zmieniła wartości d w innym
środowisku.
Zmienne leksykalne pozwalają uniknąć wielu błędów, dlatego najlepiej
używać ich jak najczęściej.
Aby 'ostrzec' innych programistów (i samego siebie) że dana zmienna
jest dynamiczna, powinno się otaczać jej nazwę gwiazdkami:
(defparameter *a* 23)
Środowiska leksykalne istnieją tak długo, jak długo istnieje do nich
dostęp (np. przez wywołanie funkcji) - można ich używać do
przechowywania danych:
CL-USER> (let ((zmienna 0))
(defun zwieksz ()
(setf zmienna (+ zmienna 1))))
=> ZWIEKSZ
CL-USER> (zwieksz)
=> 1
CL-USER> (zwieksz)
=> 2
CL-USER> (zwieksz)
=> 3
Zamiast pisać
(setf zmienna (+ zmienna 1))
można napisać
(incf zmienna)
.
Argumenty opcjonalne i słownikowe
Opcjonalne
Argumenty opcjonalne oznacza się używając &optional.
Przykład:
CL-USER> (defun funkcja (&optional (x 666))
(print x))
FUNKCJA
CL-USER> (funkcja)
666
=> 666
CL-USER> (funkcja 20)
20
=> 20
Nie ma ograniczenia (oprócz ograniczeń pamięci itd) na ilość zmiennych
opcjonalnych.
(defun funkcja (&optional (arg1 "arg1") (arg2 40) (arg3 90) arg4))
Zdefiniuje 4 argumenty z wartościa domyślną - w przypadku arg4 jest to
wartość nil.
Nieskończona ilość argumentów
Argumenty tego typu oznacza się poprzez &rest. Wszystkie podane
argumenty zostaną zwinięte w jedną listę.
CL-USER> (defun fun-rest (&rest a)
(print a))
FUN-REST
CL-USER> (fun-rest 1 2 3 4 5 6)
(1 2 3 4 5 6)
=> (1 2 3 4 5 6)
Argumenty słownikowe
Argumenty opcjonalne, podawane jako :index - można je wpisywać w
dowolnej kolejności. Oznaczane poprzez &key.
CL-USER> (defun fun-key (&key (a "a") (b "b") c)
(print a)
(print b)
(print c))
=> FUN-KEY
CL-USER> (fun-key)
"a"
"b"
NIL
=> NIL
CL-USER> (fun-key :a "pawel" :c "gawel")
"pawel"
"b"
"gawel"
=> "gawel"
CL-USER> (fun-key :a "lisp" :b "rzadzi")
"lisp"
"rzadzi"
NIL
=> NIL
Dynamiczne i statyczne typowanie oraz Garbage Collector
Dynamiczne typowanie oznacza, że wartość ma typ, ale zmienna nie -
można przypisać do niej obiekt dowolnego typu.
Dynamiczne typowanie bardzo upraszcza i skraca programowanie.
Statyczne typowanie jest jednak czasami przydatne, jak nie chcemy
jakiegoś typu w danym miejscu.
Można robić to 'ręcznie' - przy pomocy testów typu i ew. wyrzucenia
błędu, ale lepiej powiedzieć jakiego typu oczekujemy:
CL-USER> (defun x (a)
(declare (number a))
(print a))
=> X
CL-USER> (x 23)
23
=> 23
CL-USER> (x "a")
The value "a" is not of type NUMBER.
[Condition of type TYPE-ERROR]
Restarts:
0: [ABORT-REQUEST] Abort handling SLIME request.
1: [ABORT] Exit debugger, returning to top level.
Backtrace:
0: (X "a")
1: (SB-INT:SIMPLE-EVAL-IN-LEXENV (X "a") #<NULL-LEXENV>)
Moim zdaniem domyślne dynamiczne typowanie i statyczne na żądanie jest
najlepszym możliwym wyjściem.
Inne typy danych
Wszystko, co nie jest listą, jest atomem - inne typy danych także.
String
Napis. Podtyp typu vector, który jest podtypem typu array.
Zapisywany przy użyciu " ".
format
(format stream format-string argumenty)
Potężny odpowiednik scanf z języka C. Faktem wartym zauważenia jest
to, że wewnętrzny język format jest kompletny w sensie turinga.
Opis wszystkich możliwości tu.
~A
- wypisuje argument "ładnie".
~%
- znak nowej linii.
strumień t oznacza standardowe wyjście
CL-USER> (format t "~A~%" (list 1 2 3))
(1 2 3)
=> NIL
Tablica
make-array
(make-array dimensions &key element-type initial-element initial-contents adjustable fill-pointer displaced-to displaced-index-offset)
Tworzy tablicę.
CL-USER> (make-array 20 :initial-element "lisp")
=> #("lisp" "lisp" "lisp" "lisp" "lisp" "lisp" "lisp" "lisp" "lisp" "lisp" "lisp"
"lisp" "lisp" "lisp" "lisp" "lisp" "lisp" "lisp" "lisp" "lisp")
Tworzy jedno-wymiarową tablicę z początkową wartością każdego elementu
ustawioną na string "lisp".
CL-USER> (make-array '(5 5) :initial-element "comp.lang.lisp")
=> #2A(("comp.lang.lisp" "comp.lang.lisp" "comp.lang.lisp" "comp.lang.lisp"
"comp.lang.lisp")
("comp.lang.lisp" "comp.lang.lisp" "comp.lang.lisp" "comp.lang.lisp"
"comp.lang.lisp")
("comp.lang.lisp" "comp.lang.lisp" "comp.lang.lisp" "comp.lang.lisp"
"comp.lang.lisp")
("comp.lang.lisp" "comp.lang.lisp" "comp.lang.lisp" "comp.lang.lisp"
"comp.lang.lisp")
("comp.lang.lisp" "comp.lang.lisp" "comp.lang.lisp" "comp.lang.lisp"
"comp.lang.lisp"))
Tworzy dwu-wymiarową tablicę wypełnioną stringiem "comp.lang.lisp".
array-rank
Ilość wymiarów tablicy można sprawdzić przy pomocy funkcji array-rank.
Wielkość tablicy typu adjustable może być dynamicznie zwiększana:
CL-USER> (defparameter *tablica* (make-array 5 :adjustable t))
=> *TABLICA*
CL-USER> (array-total-size *tablica*)
=> 5
CL-USER> (adjust-array *tablica* 20)
=> #(0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
CL-USER> (array-total-size *tablica*)
=> 20
Właściwości fill-pointer można użyć razem z właściwością adjustable do
stworzenia dynamicznego stringa:
CL-USER> (defparameter *napis* (make-array 0 :element-type 'character :adjustable t :fill-pointer t))
=> *NAPIS*
CL-USER> *napis*
=> ""
CL-USER> (format *napis* "ala ma kota")
=> NIL
CL-USER> *napis*
=> "ala ma kota"
CL-USER> (format *napis* "~%~A~%" (list 1 2 3))
=> NIL
CL-USER> *napis*
=> "ala ma kota
(1 2 3)
"
aref
(aref tablica wymiary)
Zwraca element z tablicy.
(aref tablica 23) = tablica[23]
(w innych językach programowania).
Hash tablica
make-hash-table
(make-hash-table &key test size rehash-size
rehash-threshold)
Tworzy hash tablicę. Test oznacza rodzaj testu (eq,eql,equal albo
equalp) jakim porównywane będą elementów.
Niektóre implementacje, np. LispWorks, umożliwiają definicję
całkowicie własnych testów.
Większość implementacji dodaje argument słownikowy weak albo
weakness - jeśli jest ustawiony na t, tworzy to "słabą" hash tablicę - klucze obecne w
niej nie będą liczone przez Garbage Collector jako odnośniki do
obiektu. Pozwala to uniknąć bardzo częstego (w językach z GC) wycieku
pamięci.
gethash
(gethash klucz hash-tablica)
Zwraca wartość elementu klucz z hash-tablica hash-tablica.
Instancje obiektów
Typem instancji jest jej klasa. Są one tworzone bezpośrednio przy użyciu
funkcji allocate-instance albo pośrednio, przy użyciu make-instance.
Opis CLOS (Common Lisp Object System) oraz MOP (Meta Object Protocol)
mógłby zająć dwie grube książki, więc w ogóle pominę ich temat w tym
krótkim wprowadzeniu.
Funkcje
Lisp był pierwszym językiem programowania, w którym funkcje były
obiektami pierwszej klasy - można ja było zwrócić, przekazać w
argumencie itd.
Funkcja jest tworzona przy użyciu specjalnej formy - lambda:
lambda
(lambda (argumenty) kod)
Zwraca anonimową funkcję.
CL-USER> (lambda () (print "x"))
=> #<FUNCTION (LAMBDA ()) {AAD421D}>
function
Specjalna forma
(function funkcja)
Zwraca nazwaną funkcję w środowisku zmiennych (o tym za chwilę):
CL-USER> (function print)
=> #<FUNCTION PRINT>
CL-USER> #'print
=> #<FUNCTION PRINT>
#'
#' jest makrem readera, umożliwiającym krótszy zapis tego samego.
Namespace (środowisko)
Common Lisp, w przeciwieństwie do Scheme, posiada kilka środowisk -
jedno dla funkcji, jedno dla zmiennych, jedno dla bloków (pominę w tym
wprowadzeniu czym to jest).
Symbol będący pierwszym elementem w obecnie ewaluowanej liście oznacza
funkcję istniejącą w środowisku funkcji - dlatego aby wywołać funkcję
istniejącą w środowisku zmiennych, należy użyć innej funkcji
'wywołującej':
apply
(apply funkcja argument1 argument2 lista-argumentow
Wywołuje funkcję ze swoimi argumentami, z tym że ostatni argument musi
być listą - jest on 'rozwijany' na normalne argumenty dla funkcji:
CL-USER> (defun funkcja (a b c d)
(format t "a: ~A b: ~A c: ~A d: ~A~%" a b c d))
=> FUNKCJA
CL-USER> (apply #'funkcja 1 2 3 '(4))
a: 1 b: 2 c: 3 d: 4
=> NIL
CL-USER> (apply #'funkcja '(1 2 3 4))
a: 1 b: 2 c: 3 d: 4
=> NIL
CL-USER> (apply #'funkcja 1 2 3 4)
attempt to use VALUES-LIST on a dotted list: 4
[Condition of type SIMPLE-TYPE-ERROR]
Restarts:
0: [ABORT-REQUEST] Abort handling SLIME request.
1: [ABORT] Exit debugger, returning to top level.
Backtrace:
0: (APPLY #<FUNCTION FUNKCJA> 1)
1: (SB-INT:SIMPLE-EVAL-IN-LEXENV (APPLY (FUNCTION FUNKCJA) 1 2 3 4) #<NULL-LEXENV>)
funcall
(funcall funkcja argument1 argument2 argument3
Wywołuje funkcję z swoimi argumentami.
CL-USER> (funcall #'funkcja 1 2 3 4)
a: 1 b: 2 c: 3 d: 4
=> NIL
Możemy w ten sposób wykonać anonimową funkcję:
CL-USER> (funcall (lambda (arg) (print arg))
"ala ma kota")
"ala ma kota"
=> "ala ma kota"
Generatory
Używając lambdy, możemy bardzo łatwo zaimplementować prosty generator:
CL-USER> (defun generator-inc (start end)
(lambda () (if (> end start)
(incf start)
end)))
GENERATOR-INC
CL-USER> (defparameter *gen* (generator-inc 2 6))
*GEN*
CL-USER> (funcall *gen*)
3
CL-USER> (funcall *gen*)
4
CL-USER> (funcall *gen*)
5
CL-USER> (funcall *gen*)
6
CL-USER> (funcall *gen*)
6
Każde wywołanie funkcji generator-inc tworzy nową
closure - o ile nie zadeklarujemy zmiennej start i end jako
specjalne, możemy mieć na raz kilkanaście generatorów i wszystko
będzie działać.
Iteracja i mapowanie
Common Lisp ma naprawdę ogromną liczbę funkcji/makr do zrealizowania
pętli. Opiszę tylko kilka (moim zdaniem) najużyteczniejszych i
najczęściej używanych.
loop
Loop jest kolejnym przykładem kompletnego w sensie Turinga mini-języka
w cl (obok format).
Jak nazwa wskazuje, służy do pętli.
Jego składnia jest dość intuicyjna - przedstawię ją przez przykłady.
CL-USER> (defparameter *lista* '(1 2 3 4 5 6 7))
=> *LISTA*
CL-USER> (loop for obj in *lista* collecting obj)
=> (1 2 3 4 5 6 7)
CL-USER> (loop for obj in *lista* summing obj)
=> 28
CL-USER> (loop
for i from 0 to 10
and j from 0
collect (list i j) into wynik
finally (return wynik))
=> ((0 0) (1 1) (2 2) (3 3) (4 4) (5 5) (6 6) (7 7) (8 8) (9 9) (10
10))
CL-USER> (loop
for i from 5 downto 0
when (oddp i) collect (1+ i))
=> (6 4 2)
Ten przykład policzy ilość liczb nieparzytnych - counting oznacza
'zwiększ licznik o 1 jeśli test zwróci prawdę'.
Funkcja oddp zwraca prawdę jeśli liczba jest nieparzysta, w przeciwnym
wypadku nil.
CL-USER> (loop for obj in *lista* counting (oddp obj))
4
CL-USER> (loop
for i from 0 to 5 do
(print i))
0
1
2
3
4
5
=> NIL
count-if
Powyższy kod można również zapisać jako:
CL-USER> (count-if #'oddp *lista*)
=> 4
map
(map result-value-type function sequence1 sequence2 ...)
Funkcja map wywołuje funkcję podaną w drugim argumencie po kolei dla wszystkich
elementów sekwencji podanych jako następne argumenty.
Element ze sekwencji pierwszej to pierwszy argument, drugiej drugi,
itd.
Gdy funkcja powróci, map dodaje element zwrócony do wyniku o żądanym typie.
Gdy jakakolwiek sekwencja się skończy, map zwróci dotychczas zebrane
obiekty.
CL-USER> (map 'string
(lambda (arg) (char-downcase arg))
"QWERTYUIOP")
=> "qwertyuiop"
CL-USER> (map 'vector #'+ '(1 2 3 4) '(98 034 92340 93))
=> #(99 36 92343 97)
mapcar
(mapcar funkcja sequence1 sequence2 ...) = (map 'list funkcja
sequence1 sequence2 ...)
mapc
(mapc function sequence1 sequence2 ...)
Mapc zwraca sequence1 - nie zbiera wyników. Użycie tej funkcji ma
sens, gdy funkcja podana w pierwszym argumencie ma efekty poboczne -
modyfikuje listę albo jakąś inną strukturę.
Makra
Aby zdefiniować makro, należy użyć makra defmacro:
(defmacro argumenty kod)
Na pierwszy rzut oka nie widać wielkiej różnicy między deklaracją
funkcji (defun).
Różnice są jednak ogromne:
- Makra są wykonywane tylko raz, podczas kompilacji.
- Argumenty do makra nie są ewaluowane.
- Makra zwracają formę - czyli po prostu normalny kod źródłowy.
Zdefiniujmy na początek proste makro - zaimplementujemy makro
when.
when
(when test kod)
Po prostu - kiedy. Kiedy test zwróci prawdę, wykonaj kod, w przeciwnym
wypadku zwróć nil.
Takie makro już istnieje w standardzie, nazwiemy więc nasze makro
kiedy.
Najpierw pomyślmy, jak napisać to ręcznie.
Najlepiej użyć if i progn.
progn
progn wykonuje wszystkie formy zawarte w niej, i zwraca wynik
ostatniej.
CL-USER> (progn 23 1 45)
45
CL-USER> (prog1 23 1 45)
23
CL-USER> (prog2 23 1 45)
1
prog1 i prog2 zwracają, odpowiednio, pierwszą i drugą
wartość.
Progn jest nam potrzebne, gdyż if przyjmuje tylko jedną formę do
wykonania.
Kiedy (< 2 4)
(2 jest mniejsze od 4) chcemy aby został
wykonany kod
(print "2") (print "jest") (print "mniejsze") (print "od") (print
"4")
CL-USER> (if (< 2 4)
(progn (print "2")
(print "jest")
(print "mniejsze")
(print "od")
(print "4")))
"2"
"jest"
"mniejsze"
"od"
"4"
=> "4"
Działa.
kiedy
Nasz zapis używając kiedy ma wyglądać tak:
(kiedy (< 2 4)
(print "2")
(print "jest")
(print "mniejsze")
(print "od")
(print "4"))
Piszemy makro:
CL-USER> (defmacro kiedy (test &rest kod)
(list 'if test (list* 'progn
kod)))
=> KIEDY
list*
Tworzy listę z wszystkich argumentów i dołącza do ostatniego
argumentu.
(list* a b) = (cons a b)
macroexpand-1
Możemy sprawdzić wynik wykonania samego makra, używając funkcji
macroexpand-1:
CL-USER> (macroexpand-1 '(kiedy (< 2 4) (print "tak")))
=> (IF (< 2 4) (PROGN (PRINT "tak")))
=> T
Tak - forma wygląda tak, jak chcieliśmy.
Druga wartość zwrócona przez macroexpand-1 (funkcja może zwracać
dowolną ilość wartości, nie tylko jedną) oznacza że jakieś makro
zostało wykonane.
Funkcja macroexpand wykonuje całkowite rozwinięcie makra,
korzystając z funkcji macroexpand-1, która rozwija tylko jeden raz.
(jeśli korzystasz z SLIME, możesz uzyć C-c enter do wywołania
macroexpand-1, i C-c M-m do wywołania macroexpand. Kursor musi być na
otwierającym nawiasie formy z makrem).
Sprawdźmy nasze makro na kodzie powyżej:
CL-USER> (kiedy (< 2 4)
(print "2")
(print "jest")
(print "mniejsze")
(print "od")
(print "4"))
"2"
"jest"
"mniejsze"
"od"
"4"
=> "4"
Backquote
Tworząc makra trzeba selektywnie ewaluowac formy - używanie ' (quote)
i funkcji list szybko staje się męczące.
` - działa tak samo jak ', ale "," ma dla takiej formy specjalne
znaczenie
W środku formy :
"," wyewaluuj formę po przecinku `
",@" wyewaluuj formę po przecinku i przyłącz ją bezpośrednio do
listy
Jako przykład, makro kiedy można zapisać w ten sposób:
(defmacro kiedy (test &rest kod)
`(if ,test (progn ,@kod)))
test jest ewaluowany (inaczej byłby w wynikowej formie zostałby symbol
"test" a nie wartość argumentu!), kod jest ewaluowany i przyłączany do
listy.
Na koniec: makro string-case, do przeanalizowania.
(string-case co
(a1 kod)
(a2 kod)
(a3 kod)
(otherwise kod))
Jeśli (equal co a_n) = t), wykonuje kod. W przeciwnym
przypadku idzie dalej. Otherwise - w przeciwnym przypadku - jest
wykonywane, gdy żaden test nie powiódł się. Gdy nie ma warunku
otherwise, a żaden test się nie powiódł, zwraca nil.
string-case
(defmacro string-case (co &rest forms)
(let* ((otherwise nil)
(wynik (mapcan (lambda (form)
(cond ((eq (car form) 'otherwise)
(setf otherwise (cdr form))
'())
(t
`(((equal ,co ,(car form))
,@(cdr form))))))
forms)))
`(cond ,@(append wynik (when otherwise `((t
,@otherwise)))))))
let* tym się różni od let, że binduje symbole po kolei, podczas
gdy let wszystkie na raz.
O czym nie powiedziałem i o czym nie jest ten kurs
O czym nie powiedziałem, a co jest ważne
Nie powiedziałem o CLOS, systemie wyjątków, MOP, strukturach, listach asocjacyjnych, strumieniach,
modułach, asdf i asdf-install, pathnames i series.
Jeśli zacząłeś/masz zamiar uczyć się CL, są to tematy którymi
powinienieś się (ale dopiero po pewnym czasie) zainteresować.
Czym nie jest ten kurs
Przeprowadzeniem kogoś kto nigdy nie widział języka na oczy do kodera
CL. Do tego są książki - 20x dłuższe (adresy poniżej).
Ten kurs ma na celu zapoznanie jedynie z podstawami języka i nie
powinien być używany jako podstawa nauki.
Linki
Successfull Common Lisp
Successfull Common Lisp - Bardzo dobra i rozległa książka
Practical Common Lisp
Practical Common Lisp - podejście praktyczne. Moim zdaniem dobra książka razem z poprzednią
On Lisp
On Lisp - książka o zaawansowanych technikach programowania funkcyjnego w Common Lispie
IMO świetne ;-D B. dobry art
No, fr3m3n dobra robota :)
Zawsze jest ctrl+p lub ctrl+s, jesli chcesz do pdf to zainstaluj drukarkę wirtualną pdf :)
Hmmm... szkoda, że na 4p nie ma wersji do druku, ani wersji w pdf. Ale cóż, tak czy siak trzeba to przeczytać ;]
Słyszałem od znajomych wiele dobrych słów o tym języku, może sam w końcu go poznam ;]