Trochę praktycznych uwag o uruchamianiu programów.
Najwygodniej robi się to w edytorach Emacsopodobnych, mających tryb uruchamiania rozumiejący zaawansowane języki programowania. Niektóre publiczne IDE mają łaty dla Lispu. Są też oczywiście IDE dla produktów komercyjnych, np. firmy Franz lub Lispworks, wersje okrojone są bezpłatne.
Prawie każda implementacja Common Lispu obejmuje plik inicjalny. Jest on automatycznie ładowany podczas uruchamiania interpretera. Służy do konfigurowania sobie specjalizowanego środowiska, na przykład:
Tak naprawdę to zwykle istnieją dwa pliki inicjalne: wspólny dla wszystkich użytkowników (często nazywany np. ,,site-init'') oraz nasz własny, umieszczany w naszym katalogu domowym.
W różnych implementacjach pliki inicjalne noszą różne nazwy.
Allegro CL | .clinit.cl |
CLISP | .clisprc |
Clozure CL | ccl-init.lisp |
CMUCL | init.lisp |
ECL | .eclrc |
LispWorks | .lispworks |
Dla SBCL plik ten nazywa się .sbclrc
. Co warto w nim
mieć w środowsku uruchomieniowym?
Pliki często zawierają błędy syntaktyczne, uniemożliwiające załadowanie ich. Dostajemy komunikat o błędzie i (ewentualnie) wizytę w debuggerze. Jeśli zamiast wejść w debugger interpreter się wyłączył, to powinniśmy w pliku inicjalnym umieścić linię
(sb-ext:enable-debugger)
(niektóre dystrybucje Linuksa wyłączały go).
Jeśli jest duży, to może być trudno zlokalizować błąd. Pomaga wtedy ustawienie
(setq *load-verbose* t)
Powoduje to wypisywanie wyników obliczania kolejnych wyrażeń. W przypadku definicji funkcji jest to jej nazwa. Ten sam efekt można osiągnąć doraźnie pisząc
(load nazwa-pliku :print t)
Bratnia flaga *compile-verbose*
jest na szczęście
domyślnie ustawiona na t
.
Można śledzić wywołania funkcji. Służą do tego makra trace
i untrace
. Wywołanie
(trace funkcja ...)powoduje rozpoczęcie śledzenia wskazanych funkcji. Śledzenie polega na wypisywaniu argumentów każdego wywołania, a później jego wyniku. Wywołanie bez argumentów po prostu podaj listę śledzonych funkcji.
Wywołanie
(untrace funkcja ...)powoduje zaprzestanie śledzenia wskazanych funkcji. Jeśli nie podano argumentów, dotyczy to wszystkich śledzonych funkcji.
Funkcje zadeklarowane inline
raczej nie dają się śledzić.
Katalog bieżący to ten, z którego załaduje się plik, którego nazwa nie podaje jego położenia
* (load "moj-plik.lisp") TGeneralnie różne implementacje mają własne zdanie na temat tego, gdzie jest katalog bieżący. Najczęściej jest to katalog, z którego uruchomiliśmy interpreter.
Istnieje też pojęcie katalogu domowego użytkownika, jego wartość możemy otrzymać przez
* (user-homedir-pathname) #P"/home/zbyszek/"
W trakcie ładowania pliku loader ustawia zmienną
*load-truename*
na rzeczywistą, kompletną nazwę pliku.
Można z tego korzystać, doładowując pliki znajdujące się w pobliskich
katalogach bez podawania ich pełnych nazw
(load (make-pathname :name "tools" :type "lisp" :defaults *load-pathname*))
Nie należy mylić jej z funkcją truename
* (truename "~/.sbclrc") #P"/home/zbyszek/.sbclrc"
W większości języków dokumentację umieszcza się w komentarzach. Oznacza to, że nie będzie ona dostępna w interpreterze (zresztą zwykle i tak go nie ma, a pracujemy systemem kompiluj-linkuj-testuj).
W Common Lispie też są komentarze, ale używa się ich raczej do wyjaśniania szczegółów implementacyjnych. Właściwa dokumentacja jest wbudowywana w obiekty i dostępna on-line.
Większość standardowych funkcji, zmiennych globalnych czy klas ma dokumentację. Związana jest zawsze z symbolem, dlatego najprościej dobrać się do niej przez
(describe '*)
Dokumentacja podzielona jest na kategorie typów, na przykład
function
, variable
itp. Dostęp niej
uzyskujemy funkcją generyczną
(documentation symbol typ-dokumentacji)Zwraca ona napis dokumentujący (string), bo dokumentacje są napisami.
Dokumentacje można umieszczać we własnych definicjach, takich jak defun, defparameter lub defstruct. W definicjach funkcji napis dokumentujący umieszcza się bezpośrednio po liście parametrów
* (defun make-queue () "Tworzy nową kolejkę FIFO z nagłówkiem" (cons nil nil)) MAKE-QUEUE * (documentation 'make-queue 'function) "Tworzy nową kolejkę FIFO z nagłówkiem" (defun first-elem (queue) "Pobiera pierwszy element podanej kolejki (nie usuwając z niej). Zwraca NIL jeśli kolejka jest pusta" (and (car queue) (first (car queue)))) FIRST-ELEM
Napis dokumentujący można również dodać później przez
* (setf (documentation 'make-queue 'function) "Buduje nową kolejkę FIFO z nagłówkiem") "Buduje nową kolejkę FIFO z nagłówkiem" * (documentation 'make-queue 'function) "Buduje nową kolejkę FIFO z nagłówkiem"
W Common Lispie ciekawie zadbano o przenaszalność programów,
wybór wersji itp. Zmienna *features*
opisuje cechy środowiska.
Jej wartością jest list symboli, zwykle kluczy. Używana jest głównie
przez makra czytania #+ i #-, ale jest to normalna zmienna.
Podstawowe przeznaczenie to stwierdzenie, co umie ,,nasz'' Lisp i jakie cechy obsługuje. U mnie na przykład
* *features* (:THREADS :SB-BSD-SOCKETS-ADDRINFO :ASDF3.3 :ASDF3.2 :ASDF3.1 :ASDF3 :ASDF2 :ASDF :OS-UNIX :NON-BASE-CHARS-EXIST-P :ASDF-UNICODE :64-BIT :64-BIT-REGISTERS :ALIEN-CALLBACKS :ANSI-CL :ASH-RIGHT-VOPS :C-STACK-IS-CONTROL-STACK :CALL-SYMBOL :COMMON-LISP :COMPACT-INSTANCE-HEADER :COMPARE-AND-SWAP-VOPS :COMPLEX-FLOAT-VOPS :CYCLE-COUNTER :ELF :FLOAT-EQL-VOPS :FP-AND-PC-STANDARD-SAVE :GCC-TLS :GENCGC :IEEE-FLOATING-POINT :IMMOBILE-CODE :IMMOBILE-SPACE :INLINE-CONSTANTS :INTEGER-EQL-VOP :LARGEFILE :LINKAGE-TABLE :LINUX :LITTLE-ENDIAN :MEMORY-BARRIER-VOPS :MULTIPLY-HIGH-VOPS :OS-PROVIDES-DLADDR :OS-PROVIDES-DLOPEN :OS-PROVIDES-GETPROTOBY-R :OS-PROVIDES-POLL :OS-PROVIDES-PUTWC :OS-PROVIDES-SUSECONDS-T :PACKAGE-LOCAL-NICKNAMES :RAW-INSTANCE-INIT-VOPS :RAW-SIGNED-WORD :RELOCATABLE-HEAP :SB-DOC :SB-EVAL :SB-FUTEX :SB-LDB :SB-PACKAGE-LOCKS :SB-SIMD-PACK :SB-SOURCE-LOCATIONS :SB-THREAD :SB-UNICODE :SBCL :STACK-ALLOCATABLE-CLOSURES :STACK-ALLOCATABLE-FIXED-OBJECTS :STACK-ALLOCATABLE-LISTS :STACK-ALLOCATABLE-VECTORS :STACK-GROWS-DOWNWARD-NOT-UPWARD :SYMBOL-INFO-VOPS :UNBIND-N-VOP :UNDEFINED-FUN-RESTARTS :UNIX :UNWIND-TO-FRAME-AND-CALL-VOP :X86-64)ale każdy może mieć inaczej.
Jak to czytać? Wszystkiego nie zrozumiemy, bo wymagałoby to głebokich studiów nad implementacją. Najważniejsze rzeczy: :ANSI-CL mówi, że to standardowy Common Lisp. :UNIX, :THREAD każdy wie. :SBCL to konkretna implemetacja. Rzeczy zaczynające się of ":SB-..." to jej specyficzne opcje. ":OS-PROVIDES..." to opinie o systemie operacyjnym. :ASDF mówi, że załadowano moduł/biblioteką ASDF, nawet widać jakie wersje. :64-BIT wiadomo.
Gdybyśmy załadowali kilka modułów, mielibyśmy tu inne opcje. Możemy również umieszczać tam własne cechy
(pushnew :moje *features*)
Jak działają makra czytania #+
i #-
?
Poprzedza się nimi wyrażenia:
#+cecha wyrażenie1 #-cecha wyrażenie2
Interpreter używa ich podczas ładowania kodu. Jeśli po napotakaniu
czytania prefiksu z #+
cecha jest na liście
*features*
, to wyrażenie1 zostanie
wczytane i obliczone. W przeciwnym razie zostanie całkowicie
pominięte. Prefiks #-
działa odwrotnie -- wczytuje
i oblicza wyrażenie2 tylko wtedy, gdy cechy nie ma
na liście *features*
.
* (defparameter temp #+:ciepło "lato" #-:ciepło "zima") TEMP * temp "zima"
W prefiksie zamiast pojedynczej cechy można umieścić wyrażenie logiczne,
używające operatorów and
, or
i not
.
* (defparameter threads? #+(or :threads :sb-thread) t #-(or :threads :sb-thread) nil) THREADS? * threads? T
Do prostej sygnalizacji błędów służy funkcja
(error specyfikator argument ...)Specyfikator może być warunkiem (w innych językach zwanym wyjątkiem), typem warunku lub napisem. Warunek może zostać obsłużony przez konstrukcje obsługi warunków. Jeśli nie, to wywołuje się
(invoke-debugger warunek)Po wywołaniu
invoke-debugger
nie można normalnie wrócić
z funkcji error
. Możliwy jest jednak nielokalny transfer
sterowania.
Jeśli specyfikator jest typem warunku, jako warunku używa się wyniku
(apply #'make-condition specyfikator argumenty)
Jeśli specyfikator jest po prostu napisem, to warunek jest typu
simple-error
i powstaje z
(make-condition 'simple-error :format-string specyfikator :format-arguments argumenty)
Przykład prostego wywołania error
* (error "Pierwszy błąd.") debugger invoked on a SIMPLE-ERROR in thread #: Pierwszy błąd. Type HELP for debugger help, or (SB-EXT:EXIT) to exit from SBCL. restarts (invokable by number or by possibly-abbreviated name): 0: [ABORT] Exit debugger, returning to top level. (SB-INT:SIMPLE-EVAL-IN-LEXENV (ERROR "Pierwszy błąd.") # ) 0]
A tu wywołanie z typem warunku
* (error 'simple-error :format-control "~a" :format-arguments '("Drugi-Błąd")) debugger invoked on a SIMPLE-ERROR in thread #: Drugi-Błąd Type HELP for debugger help, or (SB-EXT:EXIT) to exit from SBCL. restarts (invokable by number or by possibly-abbreviated name): 0: [ABORT] Exit debugger, returning to top level. (SB-INT:SIMPLE-EVAL-IN-LEXENV (ERROR (QUOTE SIMPLE-ERROR) :FORMAT-CONTROL "~a" :FORMAT-ARGUMENTS (QUOTE ("Drugi-Błąd"))) # ) 0]
Informacje o błędach wypisują się na strumień, będący wartością zmiennej
*error-output*
.
Można zmieniać jej wartość, ale należy być ostrożnym. Jeśli próba
wypisania na *error-output*
da błąd (na przykład gdy sieć
przestała być dostępna, a strumień był sieciowy), powoduje to powstanie
kaskady błędów, ponieważ próbuje się wypisać informację o nowym błędzie
itd.
Przykład użycia
;;; Tworzymy plik dziennika błędów. (defparameter *error-log* (open "errors.log" :direction :output :if-does-not-exist :create :if-exists :append)) ;;; Teraz zrobimy tak, żeby informacje o błęðach wypisywały się zarówno ;;; na konsolę, jak i na plik. (when *error-log* (setf *error-output* (make-broadcast-stream *standard-output* *error-log*)))
Ciąg dalszy na pewno nastąpi...