Uruchamianie programów

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.

Plik inicjalny

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 CLccl-init.lisp
CMUCLinit.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.

Śledzenie wywołań

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żacy

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")
T
Generalnie 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"

Dokumentacja

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"

Cechy implementacji

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

Obsługa błędów

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...