Przewodnik po języku Common Lisp V 1.01 z komentarzami

PL
Data dodania: 2011-09-17, Autor: Paweł Cichosz, Dodał: Karol, Wyświetleń: 274

Komentarz:

Witam. Ucieszyła mnie duża poczytność tutorialu pana Pawła Cichosza — licznik wskazuje ponad 4800 osób — nie spodziewałem się takiego zainteresowania Lispem.

Niestety tutorial zawiera trochę błędów, poza tym pomija wiele ważnych zagadnień (nie twierdzę że powinny być tu omówione (to tutorial a nie podręcznik) — uważam jednak że warto o nich wspomnieć np. o tym że Lisp jest językiem obiektowym). Przyszedł mi do głowy pomysł że dobrze by było część rzeczy skomentować — rezultat poniżej :)

Postanowiłem nic w oryginale nie zmieniać (no prawie nic..)¹ — komentarze umieściłem w oddzielnych ramkach — łącznie około 40 komentarzy. Większość z tego co zrobiłem z tym artykułem (w formie poprawek, komentarzy i rozbudowy layoutu) zrobiłem w 2006 roku, w 2008 naniosłem nieco poprawek i dopisałem trochę o makrach. Makra są nieco zbyt ambitne dla tego tutoriala ale uznałem że wspomnieć mogę, do czytania nie namawiam ;)

Życzę miłej lektury — Szymon.
Kontakt: ssbm2 (at) o2 (dot) pl


[1] ograniczyłem się do poprawienia kilku literówek i błędów w kodzie powstałych prawdopodobnie na etapie formatowania tutorialu; interweniowałem 2 razy (tzn. zmieniłem treść) ale wyraźnie zaznaczyłem co, gdzie i dlaczego (autor uparcie próbuje wykonywać destruktywne operacje na literałach — co jest zabronione). Przeformatowałem HTML.

Wprowadzenie

Język Lisp jest obok wykorzystywanego w obliczeniach numerycznych języka Fortran najstarszym językiem programowania wciąż aktywnie używanym, przy czym — w przeciwieństwie do Fortranu — pod względem oferowanej siły wyrazu nie ustępuje najnowocześniejszym współczesnym językom. Będąc językiem ogólnego przeznaczenia, posiada szereg cech, które czynią go szczególnie dogodnym narzędziem implementacji oprogramowania o charakterze badawczym lub dydaktycznym oraz prototypowania. Jeśli uwzględni się ponadto występujące w nim mechanizmy wspierające przetwarzanie danych symbolicznych, nie budzi zdziwienia jego popularność w środowisku naukowców i praktyków zajmujących się sztuczną inteligencją. Język ten wciąż nie starzeje się, a po wprowadzeniu standardu ANSI dla dialektu Common Lisp pozwala na pisanie programów przenośnych, które przy tym wcale nie muszą być — przy umiejętnej implementacji — tak bardzo nieefektywne, jak się tradycyjnie przyjęło sądzić.

Wypada dodać że poszczególne implementacje mogą znacznie różnić się wydajnością. Najszybszy kod (styczeń 2006) — mam na myśli implementacje na 'wolnej' licencji — produkuje SBCL. Najszybciej kompiluje CLISP. CLISP kompiluje do bajtkodu, SBCL do kodu natywnego.

Poniżej tabelka z (częściowym) zestawieniem z benchmarkowni: 'The Great Computer Language Shootout' z 2003/2004 r. Tabelka zawiera porównanie Lispu, Javy, Perla, Pythona do C++. Podane wyniki mają charakter orientacyjny, tym bardziej że teraz mamy rok 2006 i sporo mogło się zmienić (benchmarkownia zmieniła podejście (i sajta), w tej chwili prawie nic tam nie ma — więc podaję stare rezultaty).

Legenda test Lisp Java Perl Python C++
<0.8 array access 1.8 6.8 127 141 1.0
0.8-1.2 hash access 1.1 3.2 1.9 4.0 1.0
1.2-3.0 list processing 0.9 20 11 20 1.0
3-10 object instantiation 1.2 2.4 89 49 1.0
10-50 exception handling 0.02 0.9 1.7 1.5 1.0
>50 sum numbers from file 5.0 2.6 2.5 8.3 1.0
word count 0.7 4.6 1.6 2.6 1.0
heapsort 1.7 7.0 76 85 1.0
matrix multiplication 2.8 8.9 225 278 1.0
regular expressions 0.7 1.9 0.9 1.2 1.0

Jak widać Lisp nie wypada źle (o czym autor wspomina, nie podaje jednak przykładów) — szczególnie w kategorii wyjątków (w terminologii lispowej: conditions), operacji na listach i wyrażeń regularnych. Dobry wynik w kategorii 'word count', był AFAIK spowodowany użyciem symboli (czasem lepiej jest użyć symboli zamiast napisów — część operacji jest wtedy szybsza (np. wyszukiwanie w tablicy haszującej)).

Ciekawy wątek na temat 'warunków' można znaleźć na grupie comp.lang.lisp w wątku 'Comparing Lisp conditions to Java Exceptions'. Dobre wprowadzenie do 'warunków' można znaleźć w książce 'Practical Common Lisp' (rozdział: Beyond Exception Handling: Conditions and Restarts).

Standard ANSI Lispu nie zawiera wyrażeń regularnych, istnieje kilka przenośnych bibliotek (są napisane w Lispie w odróżnieniu od np. Perla gdzie są zaimplementowane w C). Najpopularniejsza z nich to CL-PPCRE kompatybilna z wyrażeniami języka Perl. CL-PPCRE jest bardzo szybka, benchmarki można znaleźć na stronie Ediego Weitza.

W powyższej tabelce widnieje pozycja 'object instantiation' — choć komentowany przeze mnie tutorial (tzn. ten który właśnie czytasz) o tym nie wspomina to Common Lisp jest językiem na wskroś obiektowym, o ile w ciągu zeszłego (2005) roku nic się nie zmieniło to ANSI Common Lisp jest jedynym językiem obiektowym spełniającym wymagania organizacji 'Object Managment Group' (tej od CORBY i UML-a).

Celem tego tekstu jest praktyczne przygotowanie czytelnika do rozumienia w podstawowym zakresie kodu programów napisanych w języku Lisp i dokonywania w nich prostych modyfikacji. Nie będzie to w żadnym razie systematyczny wykład tego języka, lecz raczej seria krótko komentowanych przykładów użycia jego najważniejszych konstrukcji. Najwłaściwsze wydaje się wobec tego prowadzenie lektury równolegle z weryfikacją tych przykładów we własnym środowisku języka Lisp.

Pierwszy kontakt

Po uruchomieniu interpretera języka Lisp rozpoczyna się interaktywna sesja, w trakcie której użytkownik może wpisywać różne konstrukcje lispowe, nazywane formami, i natychmiast obserwować ich wyniki. W poniższych przykładach zakładać będziemy, że interpreter zgłasza swoją gotowość znakiem zachęty >, chociaż w konkretnych środowiskach może być to inny znak. Jeśli wówczas użytkownik wprowadzi dowolną formę zakończoną znakiem Enter nastąpi jej interpretacja i wyznaczenie wartości (czyli wartościowanie), po czym zostanie zapisany uzyskany wynik i ponownie pojawi się znak zachęty.

'Interaktywna sesja'/'interpreter' o której autor wspomina nosi nazwę 'read-eval-print loop' (CLHS 25.1.1), w skrócie REPL. Słowo 'interpreter' jest w tym przypadku mylące, lepiej już 'powłoka'. Celem uprzyjemnienia sobie pracy z REPL-em proponuję używać edytora EMACS z pakietem SLIME. Jeśli nie lubisz EMACSa to wypróbuj OS IDE jabberwocky lub wtyczkę do Eclipse (pod koniec 2008 r. są dwie). Komercyjne implementacje lispu mają dobre IDE — w celach edukacyjnych można sobie zassać wersję bezpłatną (z różnymi ograniczeniami, ale na początek powinna wystarczyć).

Wprowadzanie i wartościowanie form

Najprostszym rodzajem form są stałe. Ich wartościowanie jest trywialne: wartością stałej jest ta sama stała. Poniższy przykład ilustruje wartościowanie stałych liczbowych (liczby całkowitej, liczby zmiennopozycyjnej i liczby ułamkowej):

> 2 ==> 2 > 3.6 ==> 3.6 > 7/13 ==> 7/13

Dla większej czytelności w tym i kolejnych przykładach drukowane przez interpreter wyniki wartościowanych form poprzedzone zostały napisem ==>, chociaż faktycznie nie jest on wypisywany.

Proste jest również wartościowanie form będących zmiennymi — polega na odtworzeniu zapamiętanej w zmiennej wartości. W następującym przykładzie zmiennej najpierw nadaje się wartość, a następnie ją odtwarza.

> (setq v 10) ==> 10 > v ==> 10

Wartościowanie form będących wywołaniami funkcji jest bardziej złożone, gdyż jako argumenty wywołania mogą być przekazane formy będące stałymi, zmiennymi lub także wywołaniami funkcji. W pierwszej kolejności następuje więc wartościowanie wszystkich form będących argumentami wywołania, a potem faktyczne wywołanie funkcji z uzyskanymi wartościami argumentów. Wartość zwrócona przez funkcję jest wynikiem wartościowanej formy. Jako ilustrację można wziąć pod uwagę następujący przykład obliczania wartości wyrażenia, które w standardowej notacji matematycznej zostałoby zapisane jako (a+b)(a-b):

> (setq a 4) ==> 4 > (setq b 3) ==> 3 > (* (+ a b) (- a b)) ==> 7

Jak widać lisp ma składnie prefiksową (przedrostkową), (tzn. 'formy' (wyrażenia symboliczne) zapisujemy w notacji polskiej). Taki zapis ma wiele zalet, niemniej problem stanowi translacja wyrażeń arytmetycznych które już mamy zapisane w notacji infiksowej. Rozwiązaniem jest użycie makra #I które można pobrać z CMU Artificial Intelligence Repository. Przykład:

> '#I"2+3*(a+c*x^^(y+1)+z)"
==> (+ 2 (* 3 (+ A (* C (EXPT X (+ Y 1))) Z)))

Wywoływane są tu funkcje *, + i - realizujące w języku Lisp operatory arytmetyczne mnożenia, dodawania i odejmowania.

W języku Lisp występują również formy, które w zapisie przypominają wywołania funkcji, lecz nimi nie są. Mogą to być operatory specjalne, służące głównie do deklarowania funkcji i zmiennych oraz określania sterowania wykonywaniem programu, a także makrodefinicje, rozszerzające podstawowy język o dodatkowe konstrukcje ułatwiające pisanie programów. Łącznie funkcje, operatory specjalne i makrodefinicje nazywać będziemy operatorami. Formy zapisywane przy użyciu operatorów mają wspólną ogólną postać:

(op forma1 ...)

W parze nawiasów znajduje się nazwa operatora, a następnie pewna liczba form (w szczególności 0) stanowiących jego argumenty. Sposób wartościowania argumentów zależy od rodzaju operatora. Tylko dla funkcji można powiedzieć, że wartościowane są wszystkie argumenty, które następnie przekazuje się do wywołania tej funkcji.

Pliki

Chociaż ilustrując dalej różne konstrukcje języka Lisp przykładami przyjęto, że odpowiednie formy są wprowadzane interaktywnie podczas sesji z interpreterem, w praktyce tekst programów — poza co najwyżej prostymi formami pisanymi doraźnie na potrzeby bieżących obliczeń lub manipulacji na danych — redagowane są za pomocą edytorów tekstu i przechowywane są w plikach, których nazwy najczęściej wyróżnione są przyrostkiem .lsp. Mogą się w nich znajdować dowolne formy, których wartościowanie następuje w trakcie wczytywania (ładowania) pliku. Do inicjowania tego procesu służy funkcja load, której argumentem jest nazwa pliku (w razie potrzeby wraz ze ścieżką dostępu) ujęta w cudzysłowy, tak jak w następujących przykładach:

> (load "plik1.lsp") ==> T > (load "projekt/plik2.lsp") ==> T

W przypadku powodzenia operacji wczytania pliku i wartościowania znajdujących się w nim form funkcja load zwraca wartość t oznaczającą logiczną prawdę. Jeśli w trakcie wczytywania lub wartościowania wystąpi błąd, zwracana jest wartość nil oznaczająca logiczny fałsz. W obu przypadkach mogą być wypisywane różne informacje diagnostyczne, w tym komunikaty o ewentualnych błędach.

Oprócz plików w postaci źródłowej, zawierających tekst napisany w języku Lisp, mogą być wczytywane także pliki w specjalnej skompilowanej postaci, utworzonej z plików źródłowych przez ich kompilację. Znajdujące się w nich formy są wówczas wartościowane w taki sam sposób, lecz może to być nawet od kilku do kilkunastu razy szybsze. Ponieważ zazwyczaj pliki źródłowe zawierają definicje funkcji, ich przy ich późniejszym wywoływaniu czas obliczeń znacznie się skraca. Do kompilacji plików źródłowych służy funkcja compile-file, której jako argument przekazuje się nazwę pliku. Pokazuje to poniższy przykład:

> (compile-file "plik.lsp") ==> T

Pliki skompilowane mają nazwy, w których przyrostek .lsp zastąpiony jest innym, zależnym od konkretnego środowiska języka Lisp. Ich ładowanie przeprowadza się jednak za pomocą tej samej funkcji load, która służy do ładowania plików źródłowych. Możliwa jest też kompilacja form znajdujących się w pliku źródłowym w trakcie ich wczytywania, co wydłuża proces ładowania, ale przyspiesza wartościowanie form i późniejszy czas wykonywania definiowanych funkcji. Aby spowodować kompilowanie w trakcie ładowania, należy funkcji load przekazać dodatkowo specjalny argument, tak jak w następującym przykładzie:

> (load "plik.lsp" :compiling t) ==> T

Argument takiej postaci jest tzw. argumentem kluczowym o nazwie compiling i wartości t. O rodzajach argumentów funkcji będzie dokładniej mowa dalej.

Jednym z argumentów funkcji COMPILE-FILE jest :EXTERNAL-FORMAT — to o tyle ważne że w przypadku lispów oferujących napisy w UTF-8 (i nie tylko) może być konieczne jej użycie. Dla przykładu: jeśli w kodzie programu (przykładowo w implementacji: SBCL) używamy literałów napisowych z polskimi znakami to by uniknąć rozbicia tychże znaków na oktety należy użyć COMPILE-FILE w nast. postaci:

(COMPILE-FILE desygnator :EXTERNAL-FORMAT :UTF-8)

Btw, słowa kluczowego 'compiling' o którym jest mowa nie ma w standardzie (CLHS: Function LOAD). Aby 'za jednym zamachem' załadować i skompilować plik można napisać:

(LOAD (COMPILE-FILE desygnator opcje...))

Dane

W języku Lisp występują typy danych i odpowiadające im mechanizmy przetwarzania znane z innych języków programowania wysokiego poziomu, co pozwala poprzestać na ich pobieżnym omówieniu. Istotnym wyjątkiem jest specyficzny dla tego języka typ listowy, który dzięki swej elastyczności stanowi podstawę struktur danych definiowanych w większości napisanych w tym języku programów.

Myślę że na dzień dzisiejszy listy (jako typ danych wbudowany w język) nie są czymś nadzwyczajnym. Lisp natomiast wyróżnia się tym że kod lispowych programów jest zbudowany z list¹ (taka własność zwie się homoikonicznością) co pozwala wygodne metaprogramowanie.

Listy są bardzo elastycznym typem danych — ale rzadko optymalnym z punktu widzenia wydajności (czas dostępu O(n)) — i z tego też powodu nie tak znowu często używanym. W użyciu są (między innymi) symbole, pakiety, tablice, struktury, obiekty, tablice haszujące tudzież funkcje.

Najważniejszym zastosowaniem list jest to że są budulcem kodu lispowego a więc metaprogramowanie odbywa się (a przynajmniej jego końcowy etap) właśnie przez operacje na listach (jak już zostało po widziane Lisp jest językiem homoikonicznym) najczęściej odbywa się to przy użyciu makr, dużo rzadziej funkcji EVAL/COMPILE.


[1] bardziej ogólnie — struktur zbudowanych z komórek elementarnych (CLHS: System Class CONS), tzn. drzew i grafów (Common Lisp umożliwia zapis cyklicznych literałów), przykłady można znaleźć np. na blogu Billa Clementson-a we wpisie Using Circular Structures in CL. Operator to porównywania cyklicznych struktur jest dostępny na http://paste.lisp.org/display/14361)).

ta ramka została rozbudowana 20 listopada 2008, dotyczy makr które choć bardzo w Lispie ważne nie są w tutorialu omawiane. Zawartość tej ramki należy potraktować jako zbiór ciekawostek i pominąć w pierwszym czytaniu.

Makra to też funkcje tyle że ich wykonanie (etap "podstawienia" makr) następuje przed etapem kompilacji właściwego kodu. Makra (czyli "specjalny rodzaj funkcji") jako wynik zwracają kod właśnie — który dla kompilatora jest nieodróżnialny od kodu bezpośrednio wprowadzonego przez programistę. Oczywiście cały proces jest rekurencyjny, tzn. żeby rozwinąć makro (które zwraca kod przeznaczony dla kompilatora) jest/może być potrzebny kompilator... Common Lisp jest tak skonstruowany że w dowolnym momencie może być uruchomiony parser, kompilator czy debugger (przez programistę bądź automatycznie przez system lispowy). Makra dowolnego typu mogą być zagnieżdżone, mogą być nawet lokalne, etc. Skolko ugodno.

W Common Lispie mamy do dyspozycji 4 typy makr (ewentualnie 5 — czasem rozbudowa operatora SETF jest traktowana jako osobny typ makr). Wykorzystaniu makr jest poświęcona niezła książka "On Lisp" dostępna online za darmo.

Najogólniejsze i wykonywane w pierwszej kolejności — już podczas działania parsera (w terminologii lispowej to "reader") są 'reader macros'. Naiwnie można powiedzieć że ów reader (parser) zamienia "tekstową" reprezentację kodu znajdującą się w pliku / łańcuchu / strumieniu na listy. Kilka makr tego rodzaju jest dostępnych od razu, najczęściej spotykanym jest ' (apostrof) (tzn. ów znak ma przypisaną funkcję która jest wykonywana przez wbudowany parser) — 'X jest rozwijane do (QUOTE X), '(A B C) do (QUOTE (A B C)).

"Makra parsera" (reader macros) nie są najczęściej używanymi, choć teoretycznie można za ich pomocą dodać nową warstwę syntaktyczną języka, tzn. przerobić Lisp na coś Pascal-podobnego, Ruby-podobnego, et cetera. Takie rozwiązania się nie przyjęły — mimo że pierwotnie — już w fazie projektowania LISPu 50+ lat temu miał się on składać z dwu warstw syntaktycznych: S-wyrażeń (Symbolicznych) i M-wyrażeń (Meta). S-wyrażenia okazały się na tyle wygodne że M-wyrażenia nigdy się na dłużej nie przyjęły choć powstało wiele rozwiązań (na przykład CGOL) a współczesny Lisp (tzn. Common Lisp) ma programowalny parser tak że nie trzeba zmieniać języka żeby dodać nową warstwę syntaktyczną. W praktyce "Makra parsera" służą głównie do drobnych narzutów składniowych zwanych potocznie lukrem syntaktycznym (jeśli chodzi o #' to można się dopatrzyć analogii z sigilami znanymi z języka Perl). Dlaczego nie są często używane ? Dobrym wyjaśnieniem jest znane powiedzenie "lukier syntaktyczny powoduje raka średnika" — używany w nadmiarze więcej szkodzi niż pomaga.

Najczęściej można spotkać "makra właściwe" w postaci S-wyrażeń (operator ...argumenty...) przekształcanych do innych S-wyrażeń — widać pewną analogię z XML/XSLT. Makra właściwe (to mój termin wymyślony na poczekaniu) służą do konstrukcji abstrakcji składniowych¹ podobnych do form specjalnych — zarówno makra właściwe jak i formy specjalne mogą mieć reguły ewaluacji odmienne od zwykłych funkcji. Argumenty funkcji w Common Lispie są kolejno ewaluowane od lewej do prawej. Formy specjalne mają własne reguły ewaluacji, na przykład operator warunkowy IF — (IF warunek then-coś else-coś-innego) — który spodziewa się 2 lub 3 argumentów zawsze ewaluuje pierwszy a w zależności od wyniku argument drugi lub trzeci. Gdyby operator IF był funkcją zawsze ewaluował by wszystkie swoje argumenty. Programista lispowy (współcześnie, w starych wcieleniach Lispu dostępny był operator NLAMBDA) nie może tworzyć form specjalnych ale może makra które nieźle "udają" formy specjalne. Btw, standard dopuszcza implementację wbudowanych w język makr (OR, AND, SETF, DOLIST, LOOP, ...) jako formy specjalne. Do definiowania "makr właściwych" używa się operatorów DEFMACRO i MACROLET.

Dużo rzadziej spotyka się makra symboliczne (rozwijany jest konkretny symbol), podstaw można dowiedzieć się z lektury opisu operatorów DEFINE-SYMBOL-MACRO i SYMBOL-MACROLET.

Makra kompilatora w odróżnieniu od reszty nie służą do definiowania nowych abstrakcji — służą do sterowania optymalizacją, tzn. oferują kompilatorowi "podpowiedzi" z których może (ale nie musi) skorzystać.


[1] nawet tak potężnych jak makro LOOP swojej rozbudowanej formie, ciekawskich odsyłam do odpowiedniego rozdziału książki "Practical Common Lisp". Jakkolwiek LOOP robi wrażenie to są znacznie mocniejsze przykłady, na przykład SERIES (autor Richard C. Waters).

Zmienne

Tak jak w programach w każdym innym języku, dane przetwarzanie w programach w języku Lisp mogą być zachowywane w zmiennych. Nie ma obowiązku deklarowania zmiennych i określania ich typu. Taką niedeklarowaną zmienną powołuje do życia pierwsza operacja nadania jej wartości, do czego służy operator specjalny setq, przykłady użycia której były już podane wyżej. Jego kolejne użycie dla tej samej zmiennej zmienia jej wartość, lecz może także zmienić jej typ, jeśli nowa wartość jest innego typu niż stara, tak jak w następującym przykładzie, w którym pierwsza wartość zmiennej x jest liczbą, a druga napisem:

W lispie to wartości mają typ¹, nie zmienne (pomijając kwestię opcjonalnych deklaracji). Tak więc zdanie: "Jego kolejne użycie dla tej samej zmiennej zmienia jej wartość, lecz może także zmienić jej typ..." nie ma sensu.


[1] Lisp należy do rodziny języków z silną typizacją (oczywiście termin ten oznacza różne rzeczy w różnych językach ;).

> (setq x 10) ==> 10 > x ==> 10 > (setq x "hello") ==> "hello" > x ==> "hello"

Zmienne globalne

Zmienne tworzone za pomocą formy setqglobalne, czyli istnieją i można się do nich odwoływać z dowolnego miejsca programu od chwili wykonania tej formy. Takie zmienne można również — i jest to bardziej elegancka praktyka — jawnie zadeklarować za pomocą makrodefinicji defvar:

> (defvar *x1*) ==> *x1* > (defvar *x2* 10) ==> *x2*

Zwyczaj nakazuje, aby pierwszym i ostatnim znakiem nazwy takiej globalnej zmiennej był znak *, co ułatwia ich identyfikację w tekście programu. Podanie początkowej wartości definiowanej zmiennej jest opcjonalne. Do zmiany wartości można następnie używać w zwykły sposób formy setq. Wielokrotne wykonanie formy defvar dla tej samej zmiennej ma skutek równoważny jej pierwszemu wykonaniu. Oznacza to w szczególności, że powtórne wykonanie tej formy z inną wartością początkową nie zmienia wartości zmiennej, co widać w poniższym przykładzie.

> (defvar *z* 10) ==> *Z* > *z* ==> 10 > (defvar *z* 20) ==> *Z* > *z* ==> 10

Inaczej zachowuje się makrodefinicja defparameter, która pod innymi względami jest równoważna makrodefinicji defvar: tutaj efekt jej wielokrotnego wykonania dla tej samej zmiennej jest taki sam, jak efekt ostatniego wykonania, co w szczególności daje możliwość zmiany wartości. Poprzedni przykład przybiera dla formy defparamater następującą postać:

> (defparameter *z* 10) ==> *Z* > *z* ==> 10 > (defparameter *z* 20) ==> *Z* > *z* ==> 20

Na definiowanie jeszcze innego rodzaju zmiennych globalnych pozwala makrodefinicja defconstant. Zdefiniowana za jej pomocą zmienna jest ustalona, co oznacza, że jej początkowej wartości nie można zmieniać. Zwyczajowo nazwy zmiennych ustalonych wyróżnia się rozpoczynając i kończąc je znakiem +, tak jaj w poniższym przykładzie.

> (defconstant +g+ 9.81) ==> +G+

Wartości nadawane zmiennym za pomocą operatorów setq, defvar, defparameter i defconstant nie muszą być stałymi, jak w podanych przykładach — mogą to być dowolne poprawne formy, które są obliczane w zwykły sposób w celu określenia wartości zmiennej.

Zmienne lokalne

Znane z innych języków programowania zmienne lokalne występują również w języku Common Lisp i ich użycie można zdecydowanie polecić wszędzie tam, gdzie nie jest konieczne przechowywanie danych dostępnych przez cały czas dla różnych funkcji. Do definiowania zmiennych lokalnych i określania ich zakresu służy operator specjalny let. Ogólna postać zapisanej za jego pomocą formy jest następująca:

(let ((zmienna forma) (zmienna forma) ... (zmienna forma)) forma forma ... forma)

W pierwszej części formy let występują lista zmiennych wraz z formami określającymi ich wartości początkowe, a w drugiej części lista dowolnych form, wartościowanych w kolejności, w jakiej zostały zapisane, w których mogą występować te zmienne. Mogą być tam one w szczególności także zmieniane za pomocą formy setq — jej użycie nie powoduje wówczas powołania zmiennej globalnej, lecz tylko nadaje nową wartość zmiennej lokalnej. Wartością formy let jest wartość ostatniej formy z jej drugiej części. Po zakończeniu jej wartościowania definiowane w niej zmienne lokalne przestają istnieć. Oto prosty przykład użycia formy let:

> (let ((x 10) (y (+ 10 20))) (setq y (- y x)) (* x y)) ==> 200

W formach nadających początkowe wartości zmiennym definiowanych w ramach formy let nie można używać wartości zmiennych zdefiniowanych w tej samej formie wcześniej: przyjmuje się, że wartości początkowe wszystkich zmiennych są obliczane jednocześnie i dopiero potem zmienne te mogą być używane. W formie let* obliczanie i nadawanie wartości początkowych przebiega sekwencyjnie (w kolejności zapisu), a każda definiowana zmienna może być używana natychmiast po nadaniu jej wartości początkowej, czyli także w formach określających wartości początkowe kolejnych zmiennych. Ilustruje to następujący przykład.

> (let* ((x 10) (y (+ x 20))) (setq y (- y x)) (* x y)) ==> 200

Typy proste

Repertuar prostych typów danych w języku Lisp zawiera występujące w większości innych języków typy liczbowe, typ znakowy, lecz także specyficzny dla tego języka typ symboliczny. Omówiony tu będzie także przy okazji typ napisowy, choć jest on w istocie typem złożonym.

Liczby

Liczby zapisywane są w zwykły sposób, przy czym w obliczeniach rozróżnia się liczby całkowite i zmiennoprzecinkowe, a także dodatkowo ułamkowe. Te ostatnie zapisywane są przez oddzielenie licznika i mianownika ułamka (oba muszą być liczbami całkowitymi) znakiem / (bez odstępów), tak jak w ułamkach 3/7 i 21/13. Występowanie typu ułamkowego pozwala na dokładne przeprowadzanie obliczeń na liczbach wymiernych. Dostępna w języku Common Lisp obejmuje także operacje na liczbach zespolonych, te jednak wykorzystywane są głównie w specjalnych dziedzinach zastosowań i nie będzie o nich dalej mowy.

Znaki i napisy

Typy znakowy i napisowy są stosunkowo rzadko wykorzystywane w programach w języku Lisp. Ich użycie ogranicza się w zasadzie do programów zajmujących się przetwarzaniem tekstu, co nie jest najbardziej typowym zastosowaniem tego języka. Stałe znakowe zapisuje się poprzedzając właściwy znak znakami #\, tak jak w przypadku stałych #\a i #\Z. Napisy ujmuje się w cudzysłowy, tak jak w stałych "Ala" i "kot". Oczywiście istnieje zestaw standardowych funkcji umożliwiających wykonywanie podstawowych manipulacji na znakach i napisach.

Nie zgadzam się ze stwierdzeniem że typ znakowy i napisowy są rzadko używane. IMO jest wręcz przeciwnie.

Symbole

Jeśli nie zależy nam na przetwarzaniu tekstu, lecz na zapisie i przetwarzaniu pewnej informacji symbolicznej, wygodnie jest korzystać z typu symbolicznego. Symbolem w języku Lisp jest dowolny ciąg znaków złożony z liter, cyfr i niektórych innych znaków (m.in. _, -, * i +). Symbolami są w szczególności nazwy zmiennych i funkcji. Podczas wartościowania form symbole są normalnie wartościowane właśnie jako zmienne lub funkcje, zależnie od kontekstu ich wystąpienia. Jeśli interesuje nas sam symbol, a nie jego wartościowanie jako zmiennej lub funkcji, należy go zacytować za pomocą znaku '. W poniższym przykładzie zmiennej x przypisuje się symbol xyz:

W nazwie symbolu mogą występować dowolne znaki (w tym białe). Te które mają specjalne znaczenie np. kropka, spacja, nawiasy okrągłe, itp. należy bądź poprzedzić backslashem, bądź jeśli jest ich więcej, ograniczyć (bądź całą nazwę symbolu) znakiem | (vertical bar).

> (setq x 'xyz) ==> XYZ

Wszystkie symbole są wewnętrznie przekształcane przez interpreter do postaci zawierającej wyłącznie wielkie litery. Oznacza to w szczególności, że wielkość liter nie jest w symbolach rozróżniana.

Owszem — dopóki ich nie ujmiemy pomiędzy znaki | | o czym pisałem przed chwilą. Domyślne zachowanie parsera można zmienić, co nie jest zalecane.

Wartości logiczne

W języku Lisp nie występuje samodzielny typ logiczny, lecz oczywiście mogą być sprawdzane różne warunki i ich kombinacje stanowiące pewne wyrażenia logiczne. Przyjmuje się wówczas, że logiczny fałsz oznacza specjalna stała nil, która — o czym będzie jeszcze mowa — jest utożsamiana także z pustą listą. Logiczną prawdę może oznaczać dowolna wartość dowolnego typu różna od nil (czyli listy pustej), co pozwala w oprócz informacji o prawdziwości pewnego warunku przekazać dodatkowe informacje. W sytuacjach, gdy nie jest to konieczne, do oznaczenia logicznej prawdy używa się specjalnej stałej t.

W języku Lisp występuje typ logiczny, i zgodnie z intuicją nazywa go (ów typ) symbol BOOLEAN. Sprawdźmy:

> (TYPE-OF T) ==> BOOLEAN
> (TYPE-OF NIL) ==> NULL
> (SUBTYPEP 'NULL 'BOOLEAN) ==> T

Typ NULL jest podtypem (klasą) typu BOOLEAN, jego jedynym elementem jest stała NIL. Typ BOOLEAN zawiera dwa elementy — T i NIL.

> (TYPEP T 'BOOLEAN) ==> T
> (TYPEP NIL 'BOOLEAN) ==> T

Symbol NIL nazywa nie tylko stałą reprezentującą fałsz logiczny i koniec listy. Nazywa również typ pusty (CLHS: Type NIL). Uwaga: typ pusty i typ NULL to nie to samo!

Listy

Najbardziej charakterystyczny dla języka Lisp obiekt, jakim jest lista, to uporządkowany ciąg elementów dowolnego typu. Każdy element może mieć inny typ. Lista zapisywana jest przez ujęcie ciągu jej elementów (oddzielonych znakami odstępu, tabulacji lub nowego wiersza) w parę nawiasów okrągłych. Poniżej przedstawione są przykładowe listy liczb całkowitych oraz symboli:

(3 8 17 33 2) (sloneczna pochmurna deszczowa)

Nic też nie stoi na przeszkodzie, aby elementem listy była inna lista, tak jak w przypadku poniższej listy:

(a b (1 2 3) d e (4 5))

Lista występująca w formie jest podczas normalnego wartościowania traktowana jak wywołanie funkcji, której nazwą jest pierwszy element listy, a jej pozostałe elementy stanowią argumenty wywołania. Aby takiemu wartościowaniu zapobiec, należy listę zacytować, podobnie jak w przypadku symboli, poprzedzając ją znakiem ':

> (setq lst '(a b c d e)) ==> (A B C D E)

Lista pusta '() może być zapisana równoważnie za pomocą specjalnej stałej nil. Do sprawdzania, czy lista jest pusta, można wykorzystać funkcję null, dającą w wyniku t dla listy pustej i nil dla niepustej. W każdej liście, która nie jest pusta, może być wyodrębniony pierwszy element ("głowa" listy) oraz (być może pusta) lista pozostałych elementów ("ogon" listy). Takie wyodrębnienie jest jedną z podstawowych operacji przy przetwarzaniu list. Realizuje je para funkcji first i rest, które dla argumentu będącego listą zwracają odpowiednio pierwszy element i listę pozostałych elementów. Działanie tych funkcji, znanych także pod historycznymi nazwami car i cdr, ilustruje następujący przykład:

> (setq lst '(a b c)) ==> (A B C) > (setq f (first lst)) ==> A > (setq r (rest lst)) ==> (B C) > (setq rr (rest r)) ==> (C) > (setq frr (first rr)) ==> C > (setq rrr (rest rr)) ==> NIL

Działanie funkcji first i rest można w pewnym sensie odwrócić. Funkcja cons tworzy listę, której pierwszym elementem jest jej pierwszy argument, a pozostałe elementy pochodzą z listy podanej jako drugi argument. Ta lista nie jest kopiowana do wyniku, lecz tylko odpowiednio dołączana. Działanie tej funkcji ilustruje kolejny przykład:

> (setq lst1 (cons 'c nil)) ==> (C) > (setq lst2 (cons 'b lst1)) ==> (B C) > (setq lst (cons 'a lst2)) ==> (A B C) > (cons (first lst) (rest lst)) ==> (A B C)

Listę można także utworzyć podając wprost jej wszystkie elementy. Służy do tego funkcja list, przyjmująca dowolną liczbę argumentów i zwracająca listę zawierającą te argumenty jako elementy. Oto przykłady jej użycia:

> (list 'a) ==> (A) > (list 'a 'b 'c) ==> (A B C) > (list 'a '(b c) 'd) ==> (A (B C) D)

Istotną cechą listy jest to, że dostęp do jej elementów jest sekwencyjny: nie można bezpośrednio odwołać się do elementu ze środka listy. Dostęp ten może być realizowany przez wyodrębnienie najpierw pierwszego elementu, potem pierwszego z pozostałych itd. Istnieją wprawdzie funkcje oferujące dostęp do elementu o podanym numerze, ale ich implementacja może wyglądać właśnie w ten sposób, co w przypadku elementów o dużych numerach oznacza znaczący koszt obliczeniowy. Więcej informacji o podstawowych funkcjach do przetwarzania list znajdzie się w dalszej części tego tekstu.

Chociaż lista jest jednym z podstawowych typów danych w języku Lisp, z punktu widzenia wewnętrznej reprezentacji stanowi ona szczególny rodzaj tzw. konstrukcji, czyli pary dwóch (być może złożonych) elementów. Zasadniczo wszystkie obiekty reprezentujące dane w języku Lisp dzielą się na konstrukcje i obiekty nie będące konstrukcjami, czyli atomy. Działanie funkcji cons polega właśnie na tworzeniu takiej konstrukcji przez połączenie swoich dwóch argumentów. Dla argumentów będących atomami powstaje para, zapisywana przez ujęcie ich w nawiasy i oddzielenie kropką:

> (cons 'a 'b) ==> (A . B)

Lista jest parą, której pierwszym elementem jest pierwszy element listy, a drugim elementem jest lista pozostałych elementów listy. Ilustrują to poniższe przykłady.

> (cons 1 nil) ==> (1) > (cons 1 '(2)) ==> (1 2)

Funkcja consp zwraca t wtedy i tylko wtedy, gdy jej argumentem jest konstrukcja.

Wektory

Wektor w języku Common Lisp jest również uporządkowanym ciągiem elementów dowolnego typu (być może różnych typów). Od listy wektor różni się tym, że jest indeksowany i pozwala na bezpośredni swobodny dostęp do dowolnego elementu w stałym czasie. Wektor zapisuje się tak samo jak listę, dodatkowo poprzedzając otwierający nawias znakiem #, np.:

#(3 8 17 33 2) #(sloneczna pochmurna deszczowa)

Używając wektorów w formach także musimy je cytować, czyli umieszczać na początku (przed #) znak ':

(setq v '#(a b c d e))

Nie, nie musimy cytować obiektów które są wartościowane do samych siebie (CLHS: self-evaluating object). Tzn. nie trzeba cytować literalnie zapisanych wektorów (np. #(1 2 3)) tak samo jak nie musimy cytować napisów (np. "foo") czy znaków (np. #\A). Powyższy przykład można zapisać bez cytowania:

(SETQ V #(A B C D E))

UWAGA: taki zapis (tzn. #(a b), '(a b), "ab") stosujemy tylko wtedy jeśli nie zamierzamy destruktywnie zmieniać wektora/listy/napisu — w przeciwnym wypadku do konstrukcji danych należy użyć funkcji VECTOR, LIST, COPY-LIST lub COPY-SEQ.

wektory:

(VECTOR 'A 'B 'C 'D)

lub:

(COPY-SEQ #(A B C D))

listy:

(LIST 'A 'B 'C 'D)

lub:

(COPY-SEQ '(A B C D))

pary:

(CONS 'A 'B)

lub:

(COPY-LIST '(A . B))

napisy:

(COPY-SEQ "Daj, ać ja pobruszę, a ty poczywaj.")

lub:

(MAKE-ARRAY (LENGTH
#0="Daj, ać ja pobruszę, a ty poczywaj.")
:ELEMENT-TYPE 'CHARACTER
:INITIAL-CONTENTS #0#)

Ostatni przykład jest przekombinowany, zapodałem go by nie było nudno ;) Jak można zauważyć napisy są po prostu wektorami (tablicami) znaków:

> (ARRAY-ELEMENT-TYPE "foo")
==> CHARACTER

> (TYPE-OF "Daj, ać ja pobruszę, a ty poczywaj.")
==> (SIMPLE-ARRAY CHARACTER (35))

W przeciwieństwie do list, które na ogół rosną powiększając się o kolejne elementy (zazwyczaj dołączane na początku za pomocą funkcji cons), wektory często tworzone są od razu w pożądanym rozmiarze, choć może on być później zmieniany. Do utworzenia wektora o żądanym rozmiarze służy funkcja make-array, której ten rozmiar dostarcza się jako argument. Ilustruje to poniższy przykład:

> (setq v (make-array 5)) ==> #(NIL NIL NIL NIL NIL)

Z przykładu tego widać, że początkową wartością wszystkich elementów wektora jest nil. Można podać inną wartość początkową elementów wektora korzystając z dodatkowego argumentu kluczowego funkcji make-array. Argumenty kluczowe funkcji mają ustalone nazwy, przy czym w wywołaniu podaje się (w dowolnej kolejności) nazwy wybranych argumentów kluczowych poprzedzone dwukropkiem, a po nich wartości tych argumentów. W naszym przypadku wykorzystany będzie argument :initial-element w sposób, który ilustruje przykład:

> (setq v (make-array 5 :initial-element 1)) ==> #(1 1 1 1 1)

Dostęp do dowolnego elementu wektora jest możliwy za pomocą funkcji svref. Jej pierwszym argumentem jest wektor, a drugim indeks elementu, przy czym numeracja zaczyna się od 0. Demonstruje to przykład:

> (setq v '#(a b c d e f)) ==> #(A B C D E F) > (svref v 3) ==> D

Znak ' jest tu niepotrzebny. Wystarczy napisać (SETQ V #(A B C D E F)).

Poniżej autor modyfikuje wektor V: (SETF (SVREF V 3) 'DDD). Nie powinien był tego robić jeśli wcześniej stworzył ów wektor jako literał — inicjalizacja zmiennej V powinna wyglądać np. tak: (SETQ V (COPY-SEQ #(A B C D E F))).

Każdy element tablicy można także zmieniać. W tym celu należy zastosować operator specjalny setf, który służy do realizacji operacji przypisania, tak jak setq, lecz może być używany nie tylko do zmiennych prostych, lecz także (między innymi) elementów wektorów. Kontynuując poprzedni przykład możemy zapisać:

> (setf (svref v 3) 'ddd) ==> DDD > v ==> #(A B C DDD E F)

Zmienne oraz wyniki niektórych funkcji dostępu do elementów list, wektorów i struktur, które mogą być argumentami funkcji setf, nazywać będziemy uogólnionymi zmiennymi.

Struktury

Oprócz list i rzadziej od nich używanych wektorów język Common Lisp udostępnia także struktury. Struktura jest typem danych agregującym ustaloną liczbę nazwanych składowych dowolnych typów. Liczba i nazwy składowych struktury są elementami określenia jej typu i wymagana jest ich deklaracja. Ma ona w podstawowym przypadku następującą postać:

(defstruct nazwa-struktury (nazwa-skladowej forma) (nazwa-skladowej forma) ... (nazwa-skladowej forma))

Nazwa struktury i nazwy składowych, jak wszystkie inne nazwy w języku Lisp, są dowolnymi symbolami. Formy występujące dla każdej składowej określają jej domyślną początkową wartość w nowo tworzonej strukturze. Ich pominięcie powoduje przyjęcie wartości nil. Przykładowa definicja struktury może być następująca:

> (defstruct czas (godzina 0) (minuta 0) (sekunda 0)) ==> CZAS

Zdefiniowanie struktury o nazwie X powoduje, że zdefiniowana zostaje także automatycznie funkcja o nazwie make-X oraz dla każdej jej składowej y funkcja o nazwie X-y. Pierwsza z nich tworzy i zwraca nową strukturę określonego typu, a druga realizuje dostęp do składowej. Użycie funkcji make-X bez argumentów powoduje, że składowe otrzymają początkowe wartości określone w definicji struktury. Można także podać inne początkowe wartości wybranych składowych za pomocą argumentów kluczowych o nazwach takich jak nazwy tych składowych. Ilustruje to poniższy przykład, w którym zakłada się dostępność podanej wyżej definicji struktury czas:

> (setq c1 (make-czas)) ==> #S(CZAS :GODZINA 0 :MINUTA 0 :SEKUNDA 0) > (setq c2 (make-czas :godzina 23 :minuta 15)) ==> #S(CZAS :GODZINA 23 :MINUTA 15 :SEKUNDA 0)

Funkcje dostępu do składowych, które jako argument przyjmują strukturę odpowiedniego typu, pozwalają na pobieranie ich wartości oraz ich zmienianie przez użycie formy setf. Kontynuując poprzedni przykład możemy zapisać:

> (czas-godzina c2) ==> 23 > (setf (czas-minuta c2) 20) ==> 20 > (czas-minuta c2) ==> 20

Wyrażenia arytmetyczne i logiczne

Jedynymi wyrażeniami występującymi w języku Lisp są formy, których podstawowe rodzaje były już omawiane. Pełnią one rolę występujących w innych językach programowania wyrażeń, ale także instrukcji i deklaracji. W szczególności wyrażeniom arytmetycznym i logicznym odpowiadają formy będące liczbowymi lub logicznymi stałymi, zmiennymi oraz wywołaniami funkcji zwracających takie wartości, które mogą być funkcjami standardowymi lub definiowanymi przez programistę. Wśród tych pierwszych są funkcje realizujące podstawowe operacje arytmetyczne i logiczne, albowiem operatory arytmetyczne i logiczne nie występują wprost w języku Lisp jako jego specjalne elementy, lecz są traktowane jak funkcje. Daje to jednolitość zapisu i interpretacji wszystkich form.

Operatory arytmetyczne

Funkcje realizujące podstawowe operacje arytmetyczne mają dla wygody krótkie nazwy będące symbolami odpowiednich operatorów. Dotyczy to operacji dodawania, odejmowania, mnożenia i dzielenia. Ich działanie ilustrują następujące proste przykłady.

> (+ 2 3) ==> 5 > (- 3 4) ==> -1 > (- 3 4 5) ==> -6 > (* 2 3 4) ==> 24 > (/ 3 4) ==> 3/4 > (/ 3.0 4.0) ==> 0.75 > (/ 3 4 5) ==> 3/20

Każda z funkcji realizujących cztery podstawowe działania arytmetyczne może przyjmować dowolnie wiele argumentów dowolnego typu liczbowego. W przypadku dodawania i mnożenia wszystkie argumenty są odpowiednio dodawane lub mnożone. W przypadku odejmowania od pierwszego argumentu odejmowane są wszystkie pozostałe. Przy dzieleniu pierwszy argument dzielony jest przez wszystkie pozostałe. Dla argumentów całkowitych lub ułamkowych każda operacja arytmetyczna daje zawsze dokładny wynik całkowity lub ułamkowy. Jeśli co najmniej jeden z argumentów jest zmiennopozycyjny, taki jest również wynik (i może być w związku z tym zaokrąglony).

Przy okazji warto wspomnieć o jednoargumentowych operatorach 1+ i 1-, których wynikiem jest wartość argumentu odpowiednio zwiększona i zmniejszona o 1. Sam argument nie jest przez nie modyfikowany. Ponadto dostępne są funkcje incf i decf, które także zwracają wartość argumentu odpowiednio zwiększoną i zmniejszoną o 1, przy czym sam ten argument musi być uogólnioną zmienną i jest także modyfikowany. Opcjonalny drugi argument określa wartość, o którą pierwszy jest zwiększany lub zmniejszany.

Operatory relacyjne

Również operatory relacyjne (porównywania) realizowane są przez funkcje, których nazwy są symbolami tych operatorów. Każda z nich daje wynik t, jeśli odpowiednia równość lub nierówność jest spełniona, oraz wynik nil, jeśli nie jest ona spełniona. W przypadku, gdy dowolna z funkcji relacyjnych otrzyma więcej niż dwa argumenty, jest ona równoważna koniunkcji warunków powstałych przez zastosowanie odpowiedniej operacji porównywania do każdej pary dwóch kolejnych argumentów. Przykłady działania tych funkcji przedstawione są poniżej.

> (= 3 4) ==> NIL > (= 3 3 3) ==> T > (< 1 3 3) ==> NIL > (<= 1 3 3) ==> T > (> 4 3 3) ==> NIL > (>= 4 3 3) ==> T

Testowanie równości

Omówione wyżej funkcje relacyjne mogą być używane do porównywania wartości typów liczbowych. W języku Lisp możliwe jest jednak porównywanie obiektów dowolnych typów. Dokładniej, można sprawdzać równość dwóch obiektów dowolnych typów, przy czym możliwe są także różne sposoby rozumienia równości, którym odpowiadają różne funkcje służące do jej testowania.

Dostępne są cztery podstawowe funkcje do testowania równości obiektów dowolnych typów: eq, eql, equal i equalp, z których każda testuje równość nieco inaczej rozumianą. Każda z tych funkcji przyjmuje dwa argumenty dowolnego typu i zwraca t jeśli są one równe oraz nil w przeciwnym przypadku, przy czym równość jest rozumiana następująco:

  • dla funkcji eq równość oznacza identyczność — dwa obiekty są równe wtedy, gdy są w istocie tym samym (a nie tylko takim samym) obiektem,
  • dla funkcji eql równość oznacza zwykłą równość wartości w przypadku liczb tego samego typu, zwykłą równość w przypadku znaków oraz identyczność w pozostałych przypadkach,
  • dla funkcji equal równość oznacza to samo co dla funkcji eql w przypadku liczb, znaków i symboli oraz tzw. strukturalną równoważność dla napisów i list,
  • dla funkcji equalp równość oznacza strukturalną równoważność nie tylko w przypadku list, lecz również w przypadku wektorów i struktur.

Obiekty o identycznym zapisie (a więc intuicyjnie równe) niekoniecznie są identyczne dla funkcji eq. Identyczność ta oznacza, że porównywane obiekty są wewnętrznie jednym i tym samym obiektem, choć może on być dostępny za pomocą różnych zmiennych. Identyczne są zawsze takie same symbole, czasem liczby całkowite i znaki (zależnie od implementacji), ale na ogół już nie liczby ułamkowe i zmiennopozycyjne oraz napisy. Listy o takiej samej zawartości są identyczne tylko jeśli są obie jedną i tą samą listą. Działanie funkcji eq ilustrują następujące przykłady.

> (eq 'a 'a) ==> T > (eq 1.2 1.2) ==> NIL > (setq x 1.2) ==> 1.2 > (setq y x) ==> 1.2 > (eq x y) ==> T > (eq "abc" "abc") ==> NIL > (setq s1 "abc") ==> "abc" > (setq s2 s1) ==> "abc" > (eq s1 s2) ==> T > (eq '(a b) '(a b)) ==> NIL > (setq l1 '(a b)) ==> (A B) > (setq l2 l1) ==> (A B) > (eq l1 l2) ==> T

Działanie funkcji eql różni się od działania funkcji eq tylko dla liczb i znaków. Według niej liczby takich samych typów o takich samych wartościach są równe. Równe są również takie same znaki. Obiekty innych typów uznawane są za równe tylko jeśli są identyczne, tak jak to było wyjaśnione dla funkcji eq. Funkcja eql jest najczęściej używanym testem równości w języku Lisp. Kilka przykładów jej użycia znajduje się poniżej:

> (eql 2 2) ==> T > (eql 2.0 2) ==> NIL > (eql 2.5 2.5) ==> T > (eql #\a #\a) ==> T > (eql #\a #\A) ==> NIL > (eql "abc" "abc") ==> NIL

Interpretacja równości realizowana przez funkcję equal różni się od tej przyjętej w funkcji eql tylko dla napisów i list. W przypadku obiektów tych typów równość nie wymaga już identyczności, a tylko równości ich poszczególnych elementów składowych. Napisy są równe według tej funkcji, jeśli równe są ich wszystkie odpowiednie znaki (porównywane tak jak przez eql). Równość list jest z kolei definiowana rekurencyjnie: dwie listy są równe w sensie funkcji equal, jeśli równe w sensie tej funkcji są ich pierwsze elementy ("głowy") oraz równe w sensie tej funkcji są listy ich pozostałych elementów ("ogony"). To właśnie jest strukturalna równoważność, która oznacza, że aby dwie listy były równe, muszą mieć taką samą strukturę (w sensie zagnieżdżenia list w listach), a odpowiadające sobie elementy na najniższym poziomie (nie będące listami) muszą być równe. Dla obiektów innych typów złożonych — wektorów i struktur — równość także w przypadku funkcji equal jest równoważna identyczności. lustrację działania funkcji equal stanowi następująca seria prostych przykładów:

> (equal "abc" "abc") ==> T > (equal "abc" "ABC") ==> NIL > (equal '(a b c) '(a b c)) ==> T > (equal '(a (b c) d) '(a (b c) d)) ==> T > (equal '(a (b c) d) '(a b c d)) ==> NIL > (equal '((a b) "bcd") '((a b) "bcd")) ==> T

W przypadku funkcji equalp interpretacja równości jako strukturalnej równoważności obejmuje nie tylko napisy i listy, lecz także wektory i struktury. Dwa wektory są równe w sensie tej funkcji, jeśli mają tyle samo elementów i ich wszystkie odpowiednie elementy są równe w sensie tej funkcji. Dwie struktury tego samego typu są równe, jeśli odpowiednio równe są ich składowe. Dla innych obiektów funkcja ta działa tak samo jak funkcja equal.

Funkcja EQUALP (ale nie EQUAL) służy także do porównywania tablic haszujących. W przypadku porównywania napisów EQUALP traktuje znaki małe i duże tak jakby były równe (EQUALP do porównywania znaków w napisach używa funkcji CHAR-EQUAL, w przeciwieństwie do funkcji EQUAL która używa EQL).

> (EQUALP "FOO" "foo")
==> T
> (EQUAL "FOO" "foo")
==> NIL

Operatory logiczne

Operacje logicznej alternatywy, koniunkcji i negacji są realizowane odpowiednio przez operatory or, and i not. Pierwsze dwa mogą przyjmować dowolnie wiele argumentów. Dla operatora or wynikiem jest wartość jego pierwszego argumentu różnego od nil albo nil, jeśli taka jest wartość wszystkich argumentów. Wynikiem operatora and jest wartość jego ostatniego argumentu, jeśli wszystkie argumenty są różne od nil, albo nil w przeciwnym przypadku. Oba te operatory wartościują kolejno swoje argumenty tylko do momentu, w którym możliwe jest ustalenie ich wyniku i tym w szczególności różnią się od funkcji, dla których wartościowane są wszystkie argumenty. Operator not daje wartość nil dla argumentu różnego od nil oraz wartość t dla argumentu nil. Ilustrują to następujące proste przykłady:

> (or '() '(1 2) '(3 4)) ==> (1 2) > (or nil '()) ==> NIL > (and '() '(1 2) '(3 4)) ==> NIL > (and 1 2 3) ==> T > (not 0) ==> nil > (not '()) ==> T

Sterowanie wartościowaniem

Sterowanie przebiegiem wykonania programu w języku Lisp sprowadza się do określenia kolejności wykonywania poszczególnych form. Normalnie formy wartościowane są w kolejności ich zapisu. Aby zmienić tę kolejność w trakcie wykonywania programu w zależności od pewnych warunków, wykorzystuje się formy sterujące, do których realizacji służą odpowiednie operatory specjalne lub makrodefinicje. Można je podzielić na formy warunkowe i formy iteracji.

Formy warunkowe

Formy warunkowe pozwalają na uzależnienie wartościowania jednej lub większej liczby form od spełnienia pewnych warunków. Rolę tych warunków mogą pełnić dowolne formy, których wynik interpretuje się jako logiczną prawdę (jeśli jest różny od nil) lub fałsz (jeśli jest równy nil).

Forma if

Podstawową formą warunkową jest forma realizowana za pomocą operatora specjalnego if, która ma następującą postać:

(if forma1 forma2 forma3)

Jeśli wartością formy forma1 nie jest nil, wartościowana jest forma forma2 i jej wynik staje się wynikiem formy if. W przeciwnym przypadku wartościowana jest forma forma3, której wynik jest wynikiem formy if. Formy if można więc użyć na przykład w następujący sposób:

> (setq x 2) ==> 2 > (setq y (* 3 x x)) ==> 12 > (if (> x y) (setq z (- x y)) (setq z (- y x))) ==> 10 > z ==> 10

Bardziej popularną formą zapisu wyrażenia

(IF (> X Y) (SETQ Z (- X Y)) (SETQ Z (- Y X)))

jest

(SETQ Z (IF (> X Y) (- X Y) (- Y X))

Formy when i unless

Forma if wymaga określenia akcji podejmowanych zarówno w przypadku, gdy występujący w niej warunek jest spełniony, jak i w przeciwnym przypadku. Niekiedy interesuje nas tylko jedna z tych dwóch sytuacji. Wówczas wygodniej jest użyć formy when lub unless, które są realizowane przez makrodefinicje i mają następującą postać:

(when forma1 forma2 ...) (unless forma1 forma2 ...)

W obu przypadkach forma forma1 pełni rolę warunku. Po niej może wystąpić dowolna liczba form, które są wszystkie wartościowane kolejno w przypadku, gdy warunek jest odpowiednio spełniony dla formy when i niespełniony dla formy unless. Wówczas wynikiem całej formy warunkowej jest wynik ostatniej z wartościowanych form. W przeciwnym przypadku daje ona w wyniku nil. Przykłady użycia obu omawianych konstrukcji znajdują się poniżej.

> (setq l1 '(a b c)) ==> (A B C) > (setq l2 (cons 'a '(b c))) ==> (A B C) > (setq l3 l1) ==> (A B C) > (when (equal l1 l2) (setq x (first l1)) (setq y (rest l2))) ==> (B C) > (unless (eql l1 l3) (setq x (rest l1)) (setq y (first l3))) ==> NIL > x ==> A > y ==> (B C)

Formy cond i case

Formy when i unless są prostsze od formy if w tym sensie, że określa się w nich akcje tylko dla przypadku, gdy warunek jest odpowiednio spełniony albo niespełniony, a nie dla obu tych przypadków. Jednocześnie można w nich zapisać więcej niż jedną formę do wykonania w takiej sytuacji. Z kolei makrodefinicja cond służy do zapisu formy warunkowej bardziej złożonej od formy if, gdyż umożliwia sprawdzanie dowolnej ustalonej liczby warunków i określanie akcji do wykonania w przypadku spełnienia każdego z nich, przy czym tu również można podać więcej niż jedną formę do wartościowania. Ogólna postać formy cond jest następująca:

(cond (forma1 forma1-1 ...) (forma2 forma2-1 ...) ...)

Może tu wystąpić jedna lub kolejnych więcej par nawiasów, wewnątrz każdej z których znajdują się dwie lub więcej form. Pierwsza z tych form określa warunek, a wszystkie następne są formami, które mają być wartościowane w przypadku, gdy warunek ten jest spełniony. Przy wartościowaniu formy cond wartościuje się kolejne formy-warunki aż do znalezienia pierwszego spełnionego warunku, następnie wartościowane są wszystkie następujące po nim formy, a wynikiem staje się wynik ostatniej z nich. Żaden następny warunek ani następujące po nim formy nie są wartościowane. Jeśli żaden warunek nie jest spełniony, wynikiem formy cond jest nil. Aby sytuacji takiej uniknąć, często jako ostatni warunek umieszcza się warunek zawsze spełniony t, a po nim formy, które mają być wartościowane w przypadku, gdy nie był spełniony żaden z wcześniejszych warunków. Ilustracją użycia formy cond jest następujący prosty przykład:

> (setq l '(a b c d)) ==> (A B C D) > (setq i 2) ==> 2 > (cond ((= i 0) nil) ((= i 1) (first l)) ((= i 2) (first (rest l))) (t l)) ==> B

Warunki dla formy cond mogą być dowolnymi formami, których wynik jest traktowany jako logiczna prawda lub fałsz. W szczególnym przypadku, kiedy formy te mają postać porównania wartości pewnej formy z ustalonymi wartościami, można użyć w zamian formy case. Jej ogólna postać jest następująca:

(case forma (wart1 forma1-1 ...) (wart2 forma2-1 ...) ...)

Występująca bezpośrednio po słowie case forma jest wartościowana, a następnie wynik porównywany jest za pomocą testu równości eql kolejno ze stałymi wartościami otwierającymi kolejne pary nawiasów. W przypadku uzyskania równości dla pewnej wartości wartościowane są wszystkie umieszczone po niej formy, a wynikiem formy case stanie się wynik ostatniej z nich. Jako ostatnią wartość można także umieścić słowo otherwise — występujące po niej formy będą wartościowane w przypadku, gdy dla żadnej wartości nie wystąpi równość.

Ponieważ dla liczb całkowitych porównywanie za pomocą funkcji = jest równoważne porównywaniu za pomocą eql, powyższy przykład użycia formy cond może być przepisany z użyciem formy case następująco:

> (setq l '(a b c d)) ==> (A B C D) > (setq i 2) ==> 2 > (case i (0 nil) (1 (first l)) (2 (first (rest l))) (otherwise l)) ==> B

Formy iteracji

Chociaż jedną z charakterystycznych cech tradycyjnego stylu programowania w języku Lisp jest częste użycie rekurencji również jako substytutu iteracji (co dzięki odpowiedniej implementacji interpreterów nie musi oznaczać spadku efektywności), o czym będzie jeszcze mowa dalej, język Common Lisp jest wyposażony w operatory służące do iteracyjnego wartościowania form. Tu omówione zostaną dwie najprostsze z nich, które w wielu sytuacjach są wystarczające: dotimes i dolist, a następnie najbardziej elastyczną, ale i znacznie bardziej złożoną formę do. Makrodefinicja loop dostępna w standardzie ANSI Common Lisp, nie będzie prezentowana.

Makro LOOP w swojej podstawowej formie było także dostępne przed standaryzacją ANSI, a rozszerzone wersje w poszczególnych dialektach niejednokrotnie przewyższały możliwościami to co jest zdefiniowane w ANSI.

LOOP (co do tego praktycznie wszyscy się zgadzają) nie jest dobrze zaprojektowane (delikatnie mówiąc) — niemniej jest bardzo często używane (bądź nadużywane).

Alternatywy dla LOOPa to ITERATE i SERIES.

Iteracja z ustaloną liczbą powtórzeń

Forma dotimes służy do realizacji iteracji polegającej na wartościowaniu podanych form określoną, ustaloną na początku liczbę razy. Jej ogólna postać jest następująca:

(dotimes (licznik liczba-powtórzeń wynik) forma ...)

W nawiasach po słowie dotimes występują kolejno:
  • licznik — zmienna, której będą przypisywane kolejne numery iteracji, zaczynając od 0,
  • liczba-powtórzeń — forma, która jest wartościowana jednokrotnie na początku i której wartość (liczba całkowita) określa liczbę powtórzeń iteracji,
  • wynik — forma, która jest wartościowana jednokrotnie na końcu i której wartość jest wynikiem formy dotimes (jeśli zostanie pominięta, wynikiem jest nil.
Następnie może wystąpić dowolna liczba form, wartościowanych w kolejności zapisu odpowiednią liczbę razy. Może być w nich używana zmienna licznik, która w kolejnych iteracjach przyjmuje wartości będące kolejnymi liczbami całkowitymi od 0 do n-1, gdzie n jest liczbą powtórzeń wyznaczoną przez wartościowanie formy liczba-powtórzeń. Następnie wartościowana jest, o ile występuje, forma wynik. Oto prosty przykład użycia formy dotimes:

> (setq l nil) ==> NIL > (dotimes (i 10 l) (setq l (cons i l))) ==> (9 8 7 6 5 4 3 2 1 0)

Iteracja względem listy

Forma dolist pozwala na realizację iteracji polegającej na powtarzaniu tych samych akcji dla każdego elementu listy. Ma ona następującą postać:

(dolist (elem lst wynik) forma ...)

W nawiasach po słowie dolist występują:
  • elem — zmienna, której będą przypisywane kolejne elementy listy, względem której przeprowadzana jest iteracja, zaczynając od pierwszego,
  • lst — forma, która jest wartościowana jednokrotnie na początku i której wartość musi być listą,
  • wynik — forma, która jest wartościowana jednokrotnie na końcu i której wartość jest wynikiem formy dolist (jeśli zostanie pominięta, wynikiem jest nil.
Następnie może wystąpić dowolna liczba form, wartościowanych w kolejności zapisu tyle razy, ile elementów liczy lista otrzymana z wartościowania formy lst. Może być w nich używana zmienna elem, która w kolejnych iteracjach przyjmuje wartości będące kolejnymi elementami tej listy. Następnie wartościowana jest, o ile występuje, forma wynik. Oto prosty przykład użycia formy dolist:

> (setq l nil) ==> NIL > (dolist (e '(a b c d e f g h) l) (setq l (cons e l))) ==> (H G F E D C B A)

Iteracja uniwersalna

Forma do pozwala na zapisanie dowolnej iteracji: mogą w niej być określone dowolne zmienne sterujące, dowolny sposób ich inicjalizacji i modyfikacji, oraz dowolny warunek kontynuacji. Ogólna postać tej formy jest następująca:

(do ((z1 ini1 mod1) (z2 ini2 mod2) ...) (kont wynik) forma ...)

Pierwszą część formy do stanowią deklaracje używanych w niej zmiennych. Zadeklarowana może zostać ich dowolna liczba. W powyższym zapisie z1, z2,... oznaczają zmienne, które mogą być używane w powtarzanych iteracyjnie formach, a także w formach określających warunek kontynuacji i wynik iteracji. Po każdej zmiennej następuje forma, która jest wartościowana jednokrotnie na początku w celu wyznaczenia jej wartości początkowej (ini1, ini2,...), a następnie forma wartościowana przed każdym kolejnym powtórzeniem iteracji określająca nową wartość tej zmiennej (mod1, mod2,...). W formach określających sposób modyfikacji dowolnej zmiennej może być używana poprzednia wartość tej lub innych zmiennych — jest to zawsze wartość z poprzedniej iteracji. Oznacza to założenie, że formy inicjalizacji oraz modyfikacji są dla wszystkich zmiennych zawsze wartościowane równocześnie, a nie sekwencyjnie. Pominięcie formy modyfikacji dla zmiennej powoduje, że nie będzie ona modyfikowana po każdej iteracji (ale oczywiście może być modyfikowana jawnie przez jedną lub więcej z powtarzanych iteracyjnie form). Pominięcie także formy inicjalizacji powoduje nadanie zmiennej wartości początkowej nil.

W drugiej części formy do znajduje się para nawiasów obejmująca dwie formy: kont, która jest wartościowana przed każdym powtórzeniem iteracji i stanowi warunek jej zakończenia oraz wynik, która jest wartościowana po ostatniej iteracji i której wynik staje się wynikiem formy do. Następnie występuje dowolna liczba form, które są iteracyjnie wartościowane tak długo, jak długo warunek zakończenia pozostaje niespełniony.

Podobnie jak forma let ma swój nieco odmienny wariant let*, dla formy do istnieje również alternatywna wersja do*, różniąca się od niej sposobem wartościowania form inicjalizacji i modyfikacji zmiennych. Zakłada się w niej wartościowanie sekwencyjne w kolejności zapisu. Ma to dwie konsekwencje: po pierwsze, w formach inicjalizacji można używać zmiennych zadeklarowanych w formie do* wcześniej, a po drugie, w formach modyfikacji użycie zmiennych zadeklarowanych wcześniej powoduje odwołanie się do ich nowych, już wcześniej zmodyfikowanych wartości. Zatem zawsze zmienne używane w tych formach mają swoje "najbardziej aktualne" wartości, czyli uwzględniają także ostatnio dokonaną modyfikację.

Przykładem użycia form iteracji do i do* mogą być następujące fragmenty kodu:

> (do ((l nil) (i 0 (1+ i)) (j 0 (+ j i))) ((= i 5) l) (setq l (cons i l)) (setq l (cons j l))) ==> (6 4 3 3 1 2 0 1 0 0) > (do* ((l nil) (i 0 (1+ i)) (j 0 (+ j i))) ((= i 5) l) (setq l (cons i l)) (setq l (cons j l))) ==> (10 4 6 3 3 2 1 1 0 0)

Pierwszy z przykładów można zapisać inaczej:

(DO ((I 0 (1+ I))
(J 0 (+ J I))
(L NIL (LIST* J I L)))
((= I 5) L))

Funkcja LIST* przydaje się jeśli chcemy dodać do początku listy więcej niż jeden element. Zamiast:

> (LET ((LIST (LIST 'A 'B)))
(CONS 1 (CONS 2 LIST)))
==> (1 2 A B)

można napisać:

> (LET ((LIST (LIST 'A 'B)))
(LIST* 1 2 LIST))
==> (1 2 A B)

Przerywanie iteracji

Możliwe jest przerwanie powtarzania iteracji przed spełnieniem warunku zakończenia. Dzięki temu w niektórych przypadkach warunek ten może być sformułowany w prostszy sposób i nie uwzględniać wszystkich okoliczności, w których kontynuowanie iteracji powinno zostać zaprzestanę. Do przedterminowego opuszczenia formy iteracyjnej służy makrodefinicja return. Skutkiem jej wartościowania jest opuszczenie formy iteracyjnej, w ramach której występuje (w przypadku iteracji zagnieżdżonych chodzi o iterację maksymalnie wewnętrzną). Wartość przekazana jako argument formy return staje się wynikiem opuszczanej formy iteracyjnej. Przerywanie iteracji ilustruje poniższy przykład.

> (setq sum 0) ==> 0 > (setq lst '(1 2 3 4 5 6 7 8 9 10)) ==> (1 2 3 4 5 6 7 8 9 10) > (dolist (e lst sum) (if (< sum 20) (incf sum e) (return e))) ==> 7

Formy grupujące

Do form używanych do sterowania wartościowaniem można również zaliczyć jako ich najbardziej trywialny rodzaj formy grupujące. Pozwalają one potraktować serię wartościowanych kolejno form jako jedną formę. Może to być przydatne do zapisu skomplikowanego przetwarzania tam, gdzie wymagane jest podanie pojedynczej formy. Dwie formy grupujące występujące w języku Lisp to prog1 i progn, które nie różnią się sposobem wartościowania występujących w ich obrębie form, a tylko wyznaczania wyniku, który jest wynikiem pierwszej formy dla prog1 i ostatniej formy dla progn. Formy te mają postać następującą:

(prog1 forma1 forma2 ...) (progn forma1 forma2 ...)

Ich wykorzystanie ilustrują poniższe przykłady:

> (setq x 4) ==> 4 > (setq y 7) ==> 7 > (if (> x y) (prog1 (setq x (- x y)) (setq l (list x y))) (prog1 (setq y (- y x)) (setq l (list y x)))) ==> 3 > l ==> (3 4)
> (setq x 3) ==> 3 > (setq y 4) ==> 4 > (setq z 5) ==> 5 > (when (progn (setq x (* x x)) (setq y (* y y)) (setq z (* z z)) (= (+ x y) z)) (list x y z)) ==> (9 16 25)

Funkcje

Tak jak w każdym współczesnym języku programowania, podstawowymi elementami programu w języku Lisp są definiowane przez programistę funkcje. Dzięki nim dekompozycja rozwiązywanego problemu ułatwiająca sformułowanie algorytmu może mieć bezpośrednie odzwierciedlenie w kodzie programu, zwiększając przy tym jego czytelność, uniwersalność i dając szanse na powtórne wykorzystanie niektórych fragmentów. Niektóre ważne funkcje standardowo dostępne w języku Common Lisp już występowały we wcześniejszej dyskusji i przykładach, a nieco większy ich repertuar zostanie przedstawiony dalej. Obecnie będzie mowa o funkcjach definiowanych przez programistę, które występują praktycznie w każdym programie w języku Lisp.

Definiowanie funkcji

Definicja funkcji formułowana jest za pomocą makrodefinicji defun. W podstawowym przypadku definicja ma ona następującą postać:

(defun nazwa-f (nazwa-a1 nazwa-a2 ...) forma ...)

Bezpośrednio po słowie defun występuje nazwa funkcji (czyli dowolny symbol), a następnie lista nazw jej argumentów (nazywanych argumentami formalnymi). Może być to w szczególności lista pusta w przypadku funkcji nie przyjmujących argumentów oraz lista jednoelementowa w przypadku funkcji jednoargumentowych. Po liście nazw argumentów może być zapisana kolejno dowolna liczba form składających się na treść funkcji. W formach tych mogą być używane nazwy argumentów funkcji, traktowane tak jak nazwy zmiennych lokalnych. Jako przykład można wziąć pod uwagę następującą definicję funkcji:

> (defun potega (m n) (let ((p 1)) (dotimes (i n p) (setq p (* p m))))) ==> POTEGA

Treść dwuargumentowej funkcji potega stanowi jedna forma let, wprowadzająca zmienną lokalną p, w ramach której znajduje się iteracja dotimes.

Wywoływanie funkcji

Zdefiniowana funkcji może być wywoływana w omawiany już wcześniej sposób, przy czym liczba form podanych jako argumenty wywołania (są to tzw. argumenty aktualne) powinna odpowiadać liczbie nazw argumentów podanych w definicji, czyli argumentów formalnych. Skutkiem wywołania funkcji jest wartościowanie kolejno form składających się na jej treść, przy czym argumenty formalne traktowane są jak zmienne lokalna, których początkowe wartości ustalane są przez wartościowanie argumentów aktualnych. Wynikiem zwracanym przez funkcję jest wynik jej ostatniej formy. W szczególności funkcja potega o definicji podanej wyżej może być wywołana następująco:

> (potega 2 10) ==> 1024

Z analizy treści tej funkcji wynika, że zwraca ona wartość argumentu m podniesioną do potęgi będącej wartością argumentu n, co potwierdza powyższy przykład.

Przy wywoływaniu funkcji przekazywanie argumentów odbywa się przez wartość, co zostało wyżej przedstawione jako inicjalizacja zmiennych będących argumentami formalnymi za pomocą wartości argumentów aktualnych. Oznacza to, że ewentualne przypisywanie wewnątrz funkcji argumentom formalnym innych wartości nie ma wpływu na wartość argumentów aktualnych również wtedy, gdy one także są zmiennymi. Następująca funkcja:

(defun zwieksz (x) (setq x (1+ x)))

zwraca wartość o jeden większą niż wartość argumentu, ale tego argumentu nie zmienia:

> (setq a 10) ==> 10 > (zwieksz a) ==> 11 > a ==> 10

Jednak w przypadku, gdy argumentem jest lista lub wektor, może być zmieniana jego zawartość, w szczególności przez wykonanie operacji przypisania dla odpowiednich zmiennych uogólnionych. Po zakończeniu wywołania argument pozostaje tym samym obiektem typu złożonego, ale jego poszczególne elementy mogą się różnić. Dotyczy to również znaków w napisach i składowych struktur. Jako ilustracja niech posłuży następujący przykład:

> (defun zamien (l1 l2) (let ((f1 (first l1)) (f2 (first l2))) (setf (first l1) f2) (setf (first l2) f1))) ==> ZAMIEN > (setq lst1 '(1 2 3)) ==> (1 2 3) > (setq lst2 '(a b c)) ==> (A B C) > (zamien lst1 lst2) ==> 1 > lst1 ==> (A 2 3) > lst2 ==> (1 B C)

Tak nie wolno (dlaczego nie wolno napisałem przy innej okazji). Powinno być:

(SETQ LST1 (LIST 1 2 3) LST2 (LIST 'A 'B 'C))

Btw, do zamiany główek list można użyć operatora ROTATEF:

> (DEFUN ZAMIEŃ-GŁÓWKI (L1 L2)
(ROTATEF (FIRST L1) (FIRST L2))
(VALUES L1 L2))
==> ZAMIEŃ-GŁÓWKI

> (ZAMIEŃ-GŁÓWKI (LIST 1 2 3) (LIST 'A 'B 'C))
==> (A 2 3)
==> (1 B C)

Specjalne rodzaje argumentów

Większą elastyczność wykorzystywania funkcji mogą niekiedy zapewnić dwa specjalne rodzaje argumentów: argumenty opcjonalne i kluczowe. W obu przypadkach chodzi o to, aby nie było konieczne przy każdym wywoływaniu przekazywanie wszystkich argumentów funkcji, jeśli jest ich wiele, ale nie zawsze wszystkie są potrzebne lub część z nich ma szczególnie często występujące wartości, które można uznać za domyślne.

Argumenty opcjonalne

Umieszczenie na liście argumentów formalnych funkcji słowa &optional powoduje, że następujące po nim argumenty są opcjonalne, co oznacza, że przy wywoływaniu funkcji mogą zostać pominięte. Wywołanie musi zawierać argumenty aktualne dla wszystkich nieopcjonalnych argumentów formalnych oraz może zawierać argumenty aktualne dla pewnej liczby początkowych opcjonalnych argumentów formalnych. Przypisywanie wartości argumentom następuje w kolejności ich występowania. Argumenty opcjonalne, dla których w wywołaniu nie zostanie przekazany argument aktualny, otrzymują wartość nil. Inną domyślną wartość argumentu opcjonalnego można określić zastępując w definicji funkcji jego nazwę listą, której pierwszym elementem jest ta nazwa, a drugim forma, wartościowana na początku wywołania w celu jego inicjalizacji. Deklarowanie i przekazywanie argumentów opcjonalnych ilustruje następujący zmodyfikowany wariant funkcji potega, w którym jej wywołanie z jednym argumentem powoduje jego podniesienie do kwadratu:

> (defun potega (m &optional (n 2)) (let ((p 1)) (dotimes (i n p) (setq p (* p m))))) ==> POTEGA > (potega 3) ==> 9

Argumenty kluczowe

W przypadku argumentów opcjonalnych możliwe jest pominięcie wartości dla wszystkich z nich, przekazanie wartości dla wszystkich z nich albo przekazanie wartości dla pewnej liczby początkowych. Nie ma więc dowolności w określaniu, dla którego argumentu wartość będzie podana, a dla którego nie — decyduje o tym kolejność na liście argumentów formalnych w definicji funkcji. Argumenty kluczowe charakteryzują się tym, że przy wywoływaniu funkcji są identyfikowane nie przez swoją pozycję na liście argumentów, lecz przez nazwę: możliwe jest dowolne przekazywanie lub pomijanie wartości dla każdego z nich.

Argumentami kluczowymi są argumenty formalne umieszczone w definicji funkcji po słowie &key. Tak jak dla argumentów opcjonalnych, można dla nich określić wartości domyślne — wówczas zamiast samej nazwy argumentu podaje się listę złożoną z nazwy i formy wyznaczającej wartość domyślną, jeśli odpowiedni argument aktualny nie zostanie przekazany. Brak jawnie podanej wartości domyślnej powoduje przyjęcie wartości nil.

W wywołaniu funkcji z argumentami kluczowymi odpowiadające im argumenty aktualne mogą się pojawić po argumentach aktualnych dla argumentów niekluczowych. Poszczególne aktualne argumenty kluczowe mogą występować w dowolnej kolejności. Każdy z nich ma postać poprzedzonej dwukropkiem nazwy argumentu, a następnie formy określającej jego wartość. Dla ilustracji funkcja potega zostanie zdefiniowana w wersji, w której wykładnik jest argumentem kluczowym:

> (defun potega (m &key (wykladnik 2)) (let ((p 1)) (dotimes (i wykladnik p) (setq p (* p m))))) ==> POTEGA > (potega 3) ==> 9 > (potega 3 :wykladnik 3) ==> 27

Argumenty kluczowe nie muszą należeć do pakietu KEYWORD (zapis :WYKLADNIK jest skrótem od KEYWORD:WYKLADNIK) — aczkolwiek takie podejście jest najpopularniejsze¹.

> (DEFUN POTĘGA (M &KEY ((WYKŁADNIK X) 2))
(DO ((R 1 (* M R))
(C 0 (1+ C)))
((= C X) R)))
==> POTĘGA

> (POTĘGA 3 'WYKŁADNIK 4)
==> 81

[1] jeśli używamy symboli z różnych pakietów mogą wystąpić problemy (konflikty nazw) — dlatego prawie zawsze do nazywania argumentów kluczowych używa się symboli z pakietu KEYWORD. Oczywiście pakietów używa się właśnie po to coby konfliktów nazw uniknąć — miałem na myśli sytuację w której nie kwalifikujemy symboli — o czym będzie powiedziane pod koniec tutorialu. Często lepszym wyjściem niż kwalifikowanie symboli jest użycie funkcji SHADOW / SHADOWING-IMPORT. (ten tutorial nie omawia pakietów dokładnie — warto zajrzeć do (CLHS 11.1 Package Concepts), przyzwoite wprowadzenie napisał Ron Garret znany także jako Erann Gat: 'The Complete Idiot's Guide to Common Lisp Packages') Podsumowując — jeśli nie mamy konkretnego powodu by zrobić inaczej — używajmy symboli z pakietu KEYWORD jako słów kluczowych.

Zarówno dla argumentów opcjonalnych jak i kluczowych można podać dodatkową zmienną — 'supplied-p-parameter' — której przypisywana jest logiczna prawda jeśli argument został podany lub fałsz (NIL) w przeciwnym wypadku. Jeśli opcjonalny/kluczowy argument nie zostanie podany w wywołaniu funkcji wtedy domyślnie przypisywana mu jest wartość NIL — a jeśli w wywołaniu została podana wartość NIL ? Jak rozróżnić pierwszego NILa od drugiego ? (podpowiedź: po to właśnie można podać tę dodatkową zmienną ;). Przykład:

> (DEFUN FOO (&OPTIONAL (ARGUMENT NIL ARGUMENT?))
(IF ARGUMENT?
(FORMAT T "Dostałem argument: ~A." ARGUMENT)
(FORMAT T "Nie dostałem argumentu."))
(VALUES))

> (FOO 997)
Dostałem argument: 997.
> (FOO NIL)
Dostałem argument: NIL.
> (FOO)
Nie dostałem argumentu.

Potrzeba obsłużenia takiej sytuacji występuje rzadko, ale 'supplied-p-parameter' można także używać celem poprawienia czytelności kodu:

> (DEFUN POTĘGA (M &KEY ((WYKŁADNIK X) 2 WYKŁADNIK?))
(UNLESS WYKŁADNIK?
(WARN "Nie dostałem wykładnika, ~
podnoszę do kwadratu."))
(DO ((R 1 (* M R))
(C 0 (1+ C)))
((= C X) R)))

> (POTĘGA 2)
WARNING: Nie dostałem wykładnika, podnoszę do kwadratu.
==> 4

UWAGA: autor pominął dwie klasy parametrów: REST i AUX. Z tą drugą nie trzeba się koniecznie zapoznawać, ale z pierwszą tak.

Funkcje jako dane

W języku Lisp istnieje możliwość potraktowania funkcji jako danych, będących wartościami zmiennych lub argumentów funkcji. Ten drugi przypadek stanowi właśnie najczęstsze zastosowanie tej możliwości. Dzięki niemu do funkcji jako jej argument można przekazać wpływającą na jej działanie inną funkcję, co ma na celu zwiększenie jej uniwersalności. Funkcje traktowane jako dane będziemy nazywać obiektami funkcyjnymi.

Aby potraktować funkcję jako daną należy jej nazwę poprzedzić przedrostkiem #'. W tej postaci może być ona przypisana zmiennej lub przekazana jako argument, tak jak w poniższym przykładzie dla funkcji potega (dla jej pierwszej wersji bez argumentów opcjonalnych i kluczowych):

> (setq obl #'potega) ==> #< CLOSURE POTEGA (M N) (DECLARE (SYSTEM::IN-DEFUN POTEGA)) (BLOCK POTEGA (LET ((P 1)) (DOTIMES (I N P) (SETQ P (* P M)))))>

Dokładny sposób zapisu obiektów funkcyjnych przez interpreter jest zależny od konkretnego środowiska.

W celu wywołania funkcji jako obiektu funkcyjnego wykorzystuje się funkcje standardowe funcall lub apply. Pierwszym argumentem funkcji funcall jest obiekt funkcyjny, a pozostałe argumenty stanowią argumenty dla jego wywołania. Funkcja apply różni się tym, że jej drugim (i ostatnim) argumentem jest lista argumentów tego wywołania. Jedna z tych dwóch możliwości wywoływania obiektów funkcyjnych może być bardziej wygodna od drugiej w konkretnych sytuacjach. Kontynuując poprzedni przykład można zapisać:

> (funcall obl 2 (+ 3 7)) ==> 1024 > (setq mn '(3 5)) ==> (3 5) > (apply obl mn) ==> 243

Poniżej przedstawiony jest przykład funkcji, która do każdego elementu listy stanowiącej jej pierwszy argument stosuje funkcję przekazaną jako drugi argument oraz zwraca sumę uzyskanych wyników (zakładając, że są to liczby).

(defun lsum (lst fun) (let ((s 0)) (dolist (e lst s) (incf s (funcall fun e)))))

Jeśli teraz założymy dostępność funkcji kwadrat zwracającej argument podniesiony do kwadratu o następującej definicji:

(defun kwadrat (x) (* x x))

to funkcji lsum można użyć do obliczenia sumy kwadratów elementów listy:

> (lsum '(1 2 3) #'kwadrat) ==> 14

Funkcje anonimowe

W bardziej złożonych programach w języku Lisp może wystąpić potrzeba używania wielu obiektów funkcyjnych, gdyż często są one argumentami funkcji standardowych, o czym będzie w odpowiednim miejscu mowa. Aby uniknąć definiowania wielu funkcji tylko w celu przekazania ich (być może jednokrotnego) jako argumentów, wykorzystuje się funkcje anonimowe, definiowane ad hoc w miejscu użycia. Funkcja anonimowa nie ma nazwy i jest dostępna tylko jako obiekt funkcyjny. Jej definicja ma następująca ogólną postać:

#'(lambda (nazwa-a1 nazwa-a2 ...) forma ...)

Po słowie lambda występuje lista argumentów aktualnych, a następnie dowolna liczba form stanowiących treść funkcji. Korzystając z funkcji anonimowej można poprzedni przykład obliczania sumy kwadratów elementów listy zapisać następująco:

> (lsum '(1 2 3) #'(lambda (x) (* x x))) ==> 14

Wiele wartości funkcji

Funkcje w języku Common Lisp zawsze zwracają co najmniej jedną wartość, choć w przypadku funkcji używanych wyłącznie ze względu na efekty uboczne może być ona ignorowana. Jednak istnieje także możliwość zwracania wielu wartości, co niekiedy okazuje się bardzo wygodne. Zwrócenie wielu wartości oraz dostęp do wielu wartości zwróconych przez wywołanie funkcji wymagają specjalnych konstrukcji.

Nieprawda, mogą zwracać 0 (zero) wartości ;) Nie będę się rozpisywał ponieważ jest to fakt o niewielkim praktycznym znaczeniu — jeśli jesteś ciekaw zajrzyj na comp.lang.lisp. Wątek jest zatytułowany (values)?.

Jeśli funkcja zwracająca wiele wartości używana jest w zwykły sposób, bez zastosowania specjalnych mechanizmów dostępu do tych wartości, jej wywołanie udostępnia tylko pierwszą zwracaną wartość. Uzasadnia to definiowanie funkcji w taki sposób, aby pierwsza zwracana wartość była wartością najczęściej używaną, podstawową, a pozostałe wartości przekazywały pewne informacje uzupełniające, które nie zawsze są wykorzystywane.

Zwracanie wielu wartości

Podstawową metodą używaną do zwracania wielu wartości jest wykorzystanie operatora values, który zwraca wszystkie przekazane mu argumenty jako wiele wartości. Aby uzyskać funkcję zwracającą wiele wartości, należy więc jako formę określającą jej wynik wykorzystać formę values, przekazując jej jako argumenty wszystkie wartości, jakie mają być zwrócone. Ilustruje to poniższy przykład, w którym zdefiniowana jest funkcja zwracająca minimalny element listy przekazanej jej jako argument listy jako pierwszą wartość i jej maksymalny element jako drugą wartość (używając do ich wyznaczenia standardowych funkcji min i max):

> (defun min-max (l) (values (apply #'min l) (apply #'max l))) ==> min-max > (min-max '(3 5 1 2 4)) ==> 1 ; 5

Jak pokazuje przykład, po interaktywnym wartościowaniu formy zwracającej wiele wartości interpreter zapisuje wszystkie te wartości.

Dostęp do zwracanych wartości

Używając funkcji o wielu wartościach w zwykły sposób uzyskuje się dostęp tylko pierwszej ze zwracanych wartości. Dla funkcji zdefiniowanej w poprzednim przykładzie można to zilustrować następująco:

> (setq m (min-max '(3 5 1 2 4))) ==> 1 > m ==> 1

Prostą metodą uzyskania dostępu do wszystkich zwracanych wartości jest użycie makrodefinicji multiple-value-list. Zwraca ona listę wartości zwracanych przez formę podaną jako jej argument. Następnie do elementów tej listy można odwoływać się w zwykły sposób. Dla funkcji z poprzednich przykładów można to zapisać następująco:

> (setq mm (multiple-value-list (min-max '(3 5 1 2 4)))) ==> (1 5) > mm ==> (1 5)

Alternatywne i czasem wygodniejsze rozwiązanie polega na użyciu makrodefinicji multiple-value-bind, która wprowadza zmienne lokalne analogicznie jak forma let, przypisując im wartości zwracane przez podaną formę. Ogólny sposób użycia tej makrodefinicji można przedstawić następująco:

(multiple-value-bind (zmienna1 zmienna2 ...) forma1 forma2 ...)

W parze nawiasów bezpośrednio po słowie multiple-value-bind znajdują się nazwy dowolnej liczby zmiennych, które będą traktowane jak zmienne lokalne. Pierwsza z występujących następnie form jest wartościowana, a zwracane przez nią wartości przypisywane są kolejno tym zmiennym. Jeśli liczba wartości przekracza liczbę zmiennych, nadmiarowe wartości są ignorowane. Jeśli liczba zmiennych przekracza liczbę wartości, nadmiarowe zmienne otrzymują wartość nil. Potem wartościowane są kolejne formy, w których mogą być używane wprowadzone zmienne. Wynik ostatniej z nich jest wynikiem całej formy multiple-value-bind. Konkretny przykład jej użycia dla funkcji z poprzednich przykładów przedstawiony jest poniżej.

> (multiple-value-bind (m1 m2) (min-max '(3 5 1 2 4)) (- m2 m1)) ==> 4

W wolnej chwili warto poczytać o MULTIPLE-VALUE-CALL, MULTIPLE-VALUE-SETQ i VALUES-LIST.

Rekurencja

Rekurencja jako technika programowania może być stosowana we wszystkich nowoczesnych językach programowania, jednak w przypadku języka Lisp jej rola jest szczególnie duża. Jest to po części konsekwencją historii rozwoju języka, którego wczesne wersje nie zawierały konstrukcji iteracyjnych, a po części natury problemów najczęściej rozwiązywanych za pomocą programów w tym języku, dla których w wielu przypadkach algorytmy rekurencyjne są najbardziej naturalne.

Wywołania rekurencyjne

Tak jak w każdym języku, w którym rekurencja jest dostępna, rekurencyjne wywołania funkcji nie różnią się niczym od pozostałych. Programista musi zadbać o to, aby algorytm rekurencyjny sformułowany był właściwie, a w szczególności aby rekurencja zawsze była skończona. Poniżej przedstawiona jest funkcja rekurencyjna zwracająca listę wszystkich podlist listy podanej jako argument, czyli list złożonych z dowolnych podzbiorów zbioru jej elementów. Nie jest to najbardziej zwięzła i elegancka z jej możliwych implementacji, ale nie wymaga szczególnej pomysłowości.

> (defun podlisty (lst) (if (null lst) '(nil) (let ((pdl (podlisty (rest lst)))) (dolist (p pdl pdl) (setq pdl (cons (cons (first lst) p) pdl)))))) ==> PODLISTY > (podlisty '(1 2 3 4)) ==> ((1) (1 4) (1 3 4) (1 3) (1 2 3) (1 2 3 4) (1 2 4) (1 2) (2) (2 4) (2 3 4) (2 3) (3) (3 4) (4) NIL)

Przedstawiona wersja funkcji zwraca listę złożoną z listy pustej, jeśli jej argumentem jest lista pusta. W przeciwnym przypadku znajduje rekurencyjnie wszystkie podlisty listy powstałej przez pominięcie pierwszego elementu, a następnie tworzy dodatkowe podlisty dodając do każdej z nich ten pominięty pierwszy element.

Rekurencja końcowa

Tradycyjny styl programowania w języku Lisp, ukształtowany częściowo przez niedostępność iteracji we wczesnych wersjach języka, obejmuje preferencje dla rozwiązań iteracyjnych również dla problemów, dla których istnieją proste i eleganckie rozwiązania iteracyjne. Na ogół nie ma problemu z rekurencyjnym zapisem powtarzanych wielokrotnie operacji. Prostym przykładem może być następująca rekurencyjna wersja funkcji potega, wcześniej definiowanej iteracyjnie:

(defun potega (m n) (if (= n 0) 1 (* m (potega m (1- n)))))

Na ogół narzut związany z wywoływaniem funkcji powoduje, że rekurencyjna realizacja obliczeń o charakterze w istocie iteracyjnym jest mniej efektywna. Jednak nowoczesne interpretery i kompilatory języka Lisp są w stanie optymalizować proces wartościowania funkcji rekurencyjnych w sytuacji, gdy rekurencyjne wywołanie stanowi ostatnią formę treści funkcji, której wynik jest także zwracany jako jej wynik. Mamy wówczas do czynienia z rekurencją końcową. W wielu przypadkach można nadać rekurencji realizującej iterację właśnie taką postać, choć czasem wymaga to pewnej pomysłowości. Przez wielu programistów od dawna związanych z językiem Lisp takie właśnie końcowo-rekurencyjne zapisy funkcji uważane są za najbardziej eleganckie.

Na ogół aby umożliwić umieszczenie wywołania rekurencyjnego na końcu funkcji stosuje się "akumulację" częściowych wyników obliczeń generowanych na kolejnych poziomach rekurencji. Aby były one przekazywane między kolejnymi poziomami, do ich zachowania wykorzystuje się dodatkowy argument funkcji, zazwyczaj deklarowany jako opcjonalny, gdyż zbędny przy pierwszym wywołaniu. Technikę tę ilustruje kolejna wersja funkcji potega, tym razem z rekurencją końcową. Wykorzystywany w niej dodatkowy argument opcjonalny p ma za zadanie akumulowanie dotychczasowych wyników (częściowo obliczonej potęgi):

(defun potega (m n &optional (p 1)) (if (= n 0) p (potega m (1- n) (* p m))))

W praktyce zapisuje się to inaczej (przy użyciu operatora LABELS).

(DEFUN POTĘGA (M N)
(LABELS ((REC (N P)
(IF (ZEROP N)
P
(REC (1- N) (* P M)))))
(REC N 1)))

Funkcje zaprogramowane z wykorzystaniem rekurencji końcowej są dla dobrych interpreterów języka Lisp równie efektywne, co równoważne funkcje iteracyjne. Jednak rekurencję końcową można zastosować także do implementacji algorytmów, które nie mają trywialnych odpowiedników iteracyjnych. W każdym przypadku można oczekiwać, że wersja z końcową rekurencją będzie bardziej efektywna niż z rekurencją "wewnętrzną" (kiedy po wywołaniu rekurencyjnym musi nastąpić powrót i wartościowanie co najmniej jednej formy przed uzyskaniem wyniku funkcji). Poniżej przedstawiona jest zmodyfikowana wersja funkcji podlisty, w której wykorzystano rekurencję końcową i akumulację częściowych wyników za pomocą argumentu opcjonalnego pdl, oznaczającego listę dotychczas wygenerowanych podlist.

> (defun podlisty (lst &optional (pdl '(nil))) (if (endp lst) pdl (podlisty (rest lst) (dolist (p pdl pdl) (setq pdl (cons (cons (first lst) p) pdl)))))) ==> PODLISTY > (podlisty '(1 2 3 4)) ==> ((4) (4 1) (4 2 1) (4 2) (4 3 2) (4 3 2 1) (4 3 1) (4 3) (3) (3 1) (3 2 1) (3 2) (2) (2 1) (1) NIL)

Wersja iteracyjna 'podlist' nie jest dłuższa ani brzydsza ;) od tej rekurencyjnej zaproponowanej przez autora:

(DEFUN PODLISTY (LIST &AUX (ACC (LIST NIL)))
(DOLIST (I LIST ACC)
(DOLIST (J ACC) (PUSH (CONS I J) ACC))))

Ani moje wersja iteracyjna, ani, *ehem* rekurencyjna (co w takim razie robi tam DOLIST ?) pana Cichosza nie podobają mi się. Jako że o gustach się nie dyskutuje po prostu podam swoją:

(DEFUN PODLISTY (LIST)
(LABELS ((REC (LIST ACC)
(IF (ENDP LIST)
ACC
(DESTRUCTURING-BIND (HEAD . TAIL) LIST
(REC TAIL
(MAPCAN #'(LAMBDA (X)
(LIST X (CONS HEAD X)))
ACC))))))
(REC LIST (LIST NIL))))

Program jako zbiór funkcji

Na ogół nietrywialne programy w języku Lisp mogą być traktowane jako pewne zbiory funkcji. Ładowanie do środowiska pliku w postaci źródłowej lub skompilowanej powoduje wartościowanie wszystkich zawartych w nim form, lecz w większości przypadków nie wykonują one żadnego faktycznego przetwarzania danych czy obliczeń, a są tylko definicjami funkcji (także zmiennych globalnych, struktur itd.). Korzystanie z programu polega wówczas na wywoływaniu odpowiednich funkcji, często jednej lub najwyżej kilku realizujących dostęp do różnych możliwości programu.

Przetwarzanie sekwencji

Szczególna rola list jako podstawowych struktur danych używanych w programach napisanych w języku Lisp była już podkreślana wyżej. Są one używane do celów często znacznie wykraczających poza typowe wykorzystanie list w innych językach programowania, w których ich obsługa nie jest wbudowana. Jednym z najważniejszych powodów tego stanu rzeczy jest specyficzna dla języka Lisp możliwość literalnego zapisywania list jako stałych (a nie tylko ich konstruowania przez dodawanie kolejnych elementów) oraz dostępność wielu funkcji pozwalających na niezwykle elastyczne przetwarzanie list, odpowiednie dla ich wielu różnych zastosowań. Charakterystyczne dla języka Lisp jest także używanie list zagnieżdżonych (niekiedy wielokrotnie), czyli takich, których elementami są inne listy. Taka lista staje się strukturą rekurencyjną, która może być wygodną reprezentacją nie tylko prostych ciągów elementów, lecz również obiektów takich jak drzewa i grafy.

Mniej uniwersalne i elastyczne od list, lecz pod pewnymi względami podobne do nich są wektory, które pozwalają na efektywny swobodny dostęp do dowolnego elementu. Wiele podstawowych operacji na ciągach elementów można realizować (czasem z niejednakową efektywnością) zarówno na listach, jak i na wektorach. Stąd są one łącznie określane mianem sekwencji. Sekwencjami są także napisy, które w zasadzie traktowane są jak wektory o elementach będących znakami. Obecnie zostaną podstawowe funkcje służące do przetwarzania sekwencji: najpierw wspólne dla list i wektorów, a następnie specyficzne dla każdego z tych typów.

Listy i wektory

Ujednolicone operacje dla obu rodzajów sekwencji obejmują między innymi dostęp do elementów, łączenie, odwzorowania iteracyjne, redukcje iteracyjne oraz wyszukiwanie, zliczanie i usuwanie elementów według podanych kryteriów.

Dostęp do elementów

Niezależnie od tego, czy dana sekwencja jest listą czy wektorem, można uzyskać dostęp do jej elementu o podanym numerze porządkowym. Służy do tego funkcja elt, której pierwszym elementem jest sekwencja, a drugim indeks elementu, zawsze liczony zaczynając od 0. Oczywiście realizacja takiego dostępu będzie zazwyczaj bardziej efektywna dla wektorów, dla których jest to zwykłe indeksowanie, niż dla list, dla których może oznaczać konieczność przejścia do żądanego elementu od początku listy. Proste przykłady użycia funkcji elt przedstawione są poniżej.

> (setq lst '(1 2 3 4 5)) ==> (1 2 3 4 5) > (setq vec #(1 2 3 4 5)) ==> #(1 2 3 4 5) > (elt lst 2) ==> 3 > (elt vec 3) ==> 4 > (setf (elt lst 0) 0) ==> 0 > lst ==> (0 2 3 4 5) > (setf (elt vec 4) 4) ==> 4 > vec ==> #(1 2 3 4 4)

Jak pokazuje przedstawiony przykład, stosując do wyniku funkcji elt operator specjalny setf można dokonać modyfikacji elementu sekwencji.

W powyższym przykładzie autor robi rzecz niedozwoloną. Omówiłem to w przy innej okazji. Porządnie napisany kod powinien wyglądać tak:

> (DEFPARAMETER *LST* (LIST 1 2 3 4 5)) ==> *LST*
> (DEFPARAMETER *VEC* (VECTOR 1 2 3 4 5)) ==> *VEC*
> (ELT *LST* 2) ==> 3
> (ELT *VEC* 3) ==> 4
> (SETF (ELT *LST* 0) 0) ==> 0
> *LST* ==> (0 2 3 4 5)
> (SETF (ELT *VEC* 4) 4) ==> 4
> *VEC* ==> #(1 2 3 4 4)

Aby odwoływać się do elementów sekwencji za pomocą ich indeksów dobrze jest znać długość sekwencji, gdyż użycie jako argumentu funkcji elt indeksu wykraczającemu poza jej koniec powoduje błąd. Liczbę elementów sekwencji można uzyskać za pomocą funkcji length. Maksymalny dozwolony indeks jest o 1 mniejszy od wartości zwróconej przez tę funkcję. Jej użycie demonstruje przykład:

> (length '(1 2 3 4 5)) ==> 5

Trzeba sobie zdawać sprawę, że dla sekwencji będących listami funkcja length może w przypadku niektórych interpreterów być dość kosztowna, jeśli jej implementacja polega na liczeniu elementów listy.

Nie wiem czemu autor tak się uparł na te 'interpretery', interesuję się lispem od 3 lat i nigdy¹ o żadnym (występującym samodzielnie) nie słyszałem. Tzn. niektóre implementacje mają coś w tym rodzaju ale to jako dodatek do kompilatora.


[1] Owszem LISP w latach 1958-1962 był językiem li tylko interpretowanym... tyle że do było dawno.

Łączenie sekwencji

Dowolna liczba sekwencji dowolnego typu może zostać połączona w jedną sekwencję wynikową. Służy do tego celu funkcja concatenate, której pierwszy argument określa typ sekwencji wynikowej — w najprostszym przypadku może to być symbol list lub vector (jeśli podany jako stała, to oczywiście poprzedzony znakiem cytowania '). Pozostałe argumenty są sekwencjami do połączenia. Wynikiem funkcji jest sekwencja typu określonego przez pierwszy argument, której elementami są kolejne elementy wszystkich sekwencji wejściowych, umieszczone w takiej kolejności, w jakiej zostały one podane w wywołaniu. Sekwencje te nie są przy tym w żaden sposób modyfikowane, a sekwencja wynikowa powstaje raczej przez kopiowanie ich elementów niż faktyczne łączenie. Działanie funkcji concatenate ilustruje przykład przedstawiony poniżej.

> (setq lst1 '(1 2 3)) ==> (1 2 3) > (setq vec '(4 5 6 7)) ==> (4 5 6 7) > (setq lst2 '(8 9 10)) ==> (8 9 10) > (concatenate 'vector lst1 vec lst2) ==> #(1 2 3 4 5 6 7 8 9 10) > lst1 ==> (1 2 3) > vec ==> #(4 5 6 7) > lst2 ==> (8 9 10)

Odwzorowania iteracyjne

Odwzorowania iteracyjne są bardzo silnym mechanizmem przetwarzania sekwencji, pozwalającym w wielu sytuacjach na ich znacznie prostszą implementację niż za pomocą zwykłych form iteracyjnych. Istotą odwzorowania iteracyjnego jest przekształcenie sekwencji w nową sekwencję, powstałą przez zastosowanie tej samej funkcji do każdego z elementów sekwencji oryginalnej. Wyniki uzyskane z wywołań tej funkcji składają się na nową sekwencję. W bardziej ogólnej wersji odwzorowanie iteracyjne może przekształcać wiele sekwencji w jedną sekwencję wynikową za pomocą funkcji wieloargumentowej, wywoływanej iteracyjnie dla kolejnych odpowiednich elementów sekwencji oryginalnych.

Do realizacji odwzorowania iteracyjnego dla sekwencji dowolnych typów służy funkcja map, której wywołanie ma postać:

(map typ fun sekw1 ...)

Jej pierwszy argument określa typ sekwencji wynikowej. Jego możliwe wartości to w najprostszym przypadku symbole list i vector oraz nil. Drugim argumentem jest obiekt funkcyjny służący do realizacji odwzorowania, czyli określający funkcję odwzorowującą. W praktyce często bywa to funkcja anonimowa, jeśli operacje wykonywane w ramach odwzorowania są proste i niepotrzebne w innych miejscach programu. Pozostałe argumenty są sekwencjami dowolnego typu i dowolnej długości, których liczba musi być równa liczbie argumentów funkcji przekazanej jako drugi argument. Jeśli argument typ jest różny od nil, wynikiem jest nowo utworzona sekwencja odpowiedniego typu o liczbie elementów takiej, jak w najkrótszej sekwencji przekazanej do funkcji. Jej każdy element jest wynikiem zastosowania funkcji fun do odpowiednich elementów sekwencji wejściowych. Jeśli argument typ ma wartość nil, sekwencja wynikowa nie jest tworzona i funkcja map zwraca nil, ale przekazana do niej funkcja jest wywoływana dla kolejnych elementów sekwencji w zwykły sposób, choć tylko dla ewentualnych efektów ubocznych. W każdym przypadku sekwencje przekazane jako argumenty nie są zmieniane (o ile nie zmienia ich dostarczona funkcja). Kilka prostych przykładów odwzorowań iteracyjnych realizowanych za pomocą funkcji map przedstawiono poniżej.

W tym miejscu zdecydowałem się zastąpić oryginalny przykład (dopisałem też co nieco) — coby ciągle nie poprawiać tego samego (autor uparcie stosuje operator QUOTE (w skrócie ') na literałach — niepotrzebnie).

> (MAP 'LIST #'(LAMBDA (X) (* X X)) #(1 2 3 4 5))
==> (1 4 9 16 25)

> (MAP 'VECTOR #'+ '(1 2 3 4 5 6 7 8 9 10) #(6 7 8 9 10))
==> #(7 9 11 13 15)

> (LET ((SUM 0))
(MAP NIL #'(LAMBDA (X) (INCF SUM X)) '(1 2 3 4 5))
SUM)
==> 15

Ostatni przykład można zapisać dużo prościej przy użyciu operatora REDUCE (będzie o nim za chwilkę).

(REDUCE (FUNCTION +) (QUOTE (1 2 3 4 5)))

W celach demonstracyjnych nie użyłem tzw. reader macros. Przy ich użyciu przykład z REDUCE wyglądał by tak:

(REDUCE #'+ '(1 2 3 4 5))

W nieznacznie odmienny sposób realizowanie jest odwzorowanie iteracyjne za pomocą funkcji map-into. W jej przypadku pierwszy argument nie jest określeniem typu sekwencji wynikowej, lecz sam jest sekwencją dowolnego typu, w której będą umieszczane wyniki kolejnych wywołań funkcji odwzorowującej przez zmianę jej elementów. Tym razem iteracja wykonywana jest tyle razy, ile elementów liczy najkrótsza z wszystkich sekwencji, z uwzględnieniem sekwencji wejściowych i sekwencji wynikowej. Jeśli sekwencja wynikowa jest dłuższa, jej dodatkowe elementy pozostają bez zmian. Użycie funkcji map-into ilustruje następujący przykład:

> (setq lst '(1 2 3 4 5 6 7 8 9 10)) ==> (1 2 3 4 5 6 7 8 9 10) > (setq vec #(a b c d e f)) ==> #(A B C D E F) > (map-into lst #'(lambda (x y) (list x y)) lst vec) ==> ((1 A) (2 B) (3 C) (4 D) (5 E) (6 F) 7 8 9 10)

Jak pokazuje ten przykład, sekwencją wynikową może być także jedna z sekwencji wejściowych.

The consequences are undefined if literal objects (including quoted objects) are destructively modified.



Ostrzeżenie w ramce powyżej to cytat z definicji Lispu (tzn. standardu ANSI). Sprawa jest poważna — każda implementacja ma prawo obsłużyć taką operację po swojemu, co gorsza może zwrócić błąd albo po prostu umrzeć (popularne implementacje są na szczęście kuloodporne ale standard w odniesieniu do destruktywnych operacji na literałach tego nie wymaga). Tak więc 'destruktywne operacje na literałach' (CLHS: 3.7.1) są poważnym błędem¹.

Co takiego autor robi źle? Najpierw przyporządkowuje symbolowi 'lst' literał (konkretniej — quoted object)

(SETQ LST (QUOTE (1 2 3 4 5 6 7 8 9 10)))

w skróconym zapisie

(SETQ LST '(1 2 3 4 5 6 7 8 9 10))

a potem destruktywnie go modyfikuje funkcją MAP-INTO. Przykład z MAP-INTO powinien wyglądać następująco (opuściłem lambdę bo jest zbędna, przypisanie wektora #(A B C D E F) pozostawiłem bez zmian bo nie jest modyfikowany):

> (SETQ LST (LIST 1 2 3 4 5 6 7 8 9 10))
==> (1 2 3 4 5 6 7 8 9 10)

> (SETQ VEC #(A B C D E F))
==> #(A B C D E F)

> (MAP-INTO LST #'LIST LST VEC)
==> ((1 A) (2 B) (3 C) (4 D) (5 E) (6 F) 7 8 9 10)

Podsumowując — jeśli używamy w swoich programach literałów to nie przekazujemy ich w charakterze argumentów dla funkcji które destruktywnie oddziałują na owe argumenty (w razie konieczności przekazujemy kopię argumentu). Te funkcje to między innymi DELETE, MAP-INTO, (SETF ELT), (SETF AREF), (SETF SVREF), (SETF CAR), (SETF CDR), NREVERSE, SORT, NCONC i wiele innych. Standardowe destruktywne funkcje łatwo rozpoznać — mają one postać albo (SETF cośtam)², albo ich nazwa zaczyna się na literę N³. Wyjątkiem od tej reguły (nazwa na N) jest 11 funkcji: DELETE, DELETE-IF, DELETE-DUPLICATES, FILL, MAP-INTO, MERGE, REPLACE, RPLACA, RPLACD, SORT i STABLE-SORT. Specjalnym przypadkiem jest funkcja APPEND która kopiuje wszystkie swoje elementy prócz ostatniego (oczywiście sama nic nie modyfikuje, ale jeśli jej ostatni argument będzie literałem to później, kiedy będziemy modyfikować wynik możemy wpędzić się w kłopoty). Niektóre makra także modyfikują swoje argumenty: SETF, PSETF, INCF, DECF, ROTATEF, SHIFTF.


[1] Jeśli implementacja nie zgłasza błędu (czy chociażby ostrzeżenia) to modyfikacja literałów może prowadzić do błędów które są bardzo trudne do znalezienia. Rozpatrzmy taki przypadek:

(DEFVAR L1 '(1 2 3 4))
(DEFVAR L2 '(1 2))

Ponieważ literałów nie modyfikuje się, kompilator może tak zoptymalizować kod że pierwsze dwa elementy listy L2 będą także należały do listy L1! Jeśli zmienimy coś w jednej z list to zmienimy także w drugiej!

[2] Nazwą funkcji w Lispie może być symbol lub dwuelementowa lista postaci (SETF cośtam). Funkcje (SETF ...) są powiązane z makrem SETF. Rozważmy następujący przykład — stwórzmy wektor o długości 1 i ustawmy wartość pierwszego (i jedynego) elementu tego wektora na 10.

> (DEFPARAMETER *VEC* (MAKE-ARRAY 1))
==> *VEC*
> (SETF (SVREF *VEC* 0) 10)
==> 10
> *VEC*
==> #(10)

Można to zrobić w inny sposób:

> (DEFPARAMETER *VEC* (MAKE-ARRAY 1))
==> *VEC*
> (FUNCALL #'(SETF SVREF) 10 *VEC* 0)
==> 10
> *VEC*
==> #(10)

[3]
Funkcje destruktywne z nazwą rozpoczynającą się od litery 'N' — dotyczące operacji na konstukcjach (listach/drzewach/zbiorach), sekwencjach i napisach oraz ich nie-destruktywne odpowiedniki:
Operacje na listach:
NBUTLAST BUTLAST
NCONC APPEND
NRECONC REVAPPEND
Operacje na napisach:
NSTRING-CAPITALIZE STRING-CAPITALIZE
NSTRING-DOWNCASE STRING-DOWNCASE
NSTRING-UPCASE STRING-UPCASE
Operacje na drzewach:
NSUBLIS SUBLIS
NSUBST SUBST
NSUBST-IF SUBST-IF
NSUBST-IF-NOT SUBST-IF-NOT
Operacje na sekwencjach:
NREVERSE REVERSE
NSUBSTITUTE SUBSTITUTE
NSUBSTITUTE-IF SUBSTITUTE-IF
NSUBSTITUTE-IF-NOT SUBSTITUTE-IF-NOT
Operacje na zbiorach:
NINTERSECTION INTERSECTION
NSET-DIFFERENCE SET-DIFFERENCE
NSET-EXCLUSIVE-OR SET-EXCLUSIVE-OR
NUNION UNION

Odwzorowania redukcyjne

Odwzorowania redukcyjne przypominają odwzorowania iteracyjne w tym, że ich istota także polega na wywoływaniu pewnej funkcji dla wszystkich elementów sekwencji. Różnica polega na tym, że funkcja ta musi być dwuargumentowa i używana jest do łączenia kolejnych elementów jednej tylko sekwencji w pojedynczy wynik. Najpierw wywołuje się funkcję dla pierwszych dwóch elementów, potem dla wyniku tego wywołania i trzeciego elementu, i tak aż do końca sekwencji, po czym ostatni wynik jest wynikiem całego odwzorowania redukcyjnego.

Do realizacji odwzorowań redukcyjnych służy funkcja reduce, której wywołanie ma następującą postać:

(reduce fun sekw)

Pierwszy argument jest funkcją odwzorowującą, a drugi sekwencją, do której kolejnych elementów funkcja ta stosowana jest w sposób opisany wyżej. Jeśli sekwencja liczy tylko jeden element, wynikiem jest właśnie ten element. W przeciwnym przypadku pierwsze wywołanie funkcji następuje dla pierwszych dwóch elementów sekwencji, a każde następne — dla wyniku poprzedniego wywołania i następnego elementu.

Sposób działania funkcji reduce może być modyfikowany za pomocą dodatkowych argumentów kluczowych. Najbardziej przydatny z nich jest argument :key, którego wartością może być jednoargumentowy obiekt funkcyjny. Jeśli zostanie podany, ten obiekt funkcyjny określa tzw. funkcję klucza i stosowany jest do każdego elementu sekwencji przed wywołaniem dla niego funkcji odwzorowującej, której argumentem staje się nie bezpośrednio element listy, lecz wynik uzyskany z wywołania funkcji klucza. Przykłady użycia odwzorowania redukcyjnego podane są niżej.

> (reduce #'* #(1 2 3 4 5)) ==> 120 > (reduce #'+ '((1 a) (2 b) (3 c) (4 d) (5 e)) :key #'first) ==> 15

Wyszukiwanie, zliczanie i usuwanie

Trzeci najważniejszy rodzaj operacji, jaki może być przeprowadzony w jednolity sposób dla list i wektorów, obejmuje wyszukiwanie w nich elementów, zliczanie oraz usuwanie elementów według pewnych kryteriów. W podstawowym przypadku chodzi po prostu o wyszukiwanie, liczenie lub usuwanie elementów równych (w sensie wybranej funkcji testującej równość) pewnemu podanemu elementowi. Pozwalają na to funkcje find, position, count i remove, dla których wywołania mają następującą ogólną postać:

(find elem sekw) (position elem sekw) (count elem sekw) (remove elem sekw)

Funkcja find poszukuje w sekwencji podanej jako drugi argument elementu równego pierwszemu argumentowi i zwraca pierwszy znaleziony element spełniający ten warunek. Działanie funkcji position jest podobne, ale zwracany jest nie znaleziony element, lecz jego numer porządkowy (indeks), licząc od 0, a w przypadku braku poszukiwanego elementu nil. Funkcja count zwraca liczbę takich elementów, a funkcja remove kopię sekwencji wejściowej z pominięciem wszystkich takich elementów. Warto zauważyć, że usuwanie nie oznacza tu modyfikacji pierwotnej sekwencji — jest to usuwanie z kopii. Każda z tych funkcji może przyjmować dodatkowe argumenty kluczowe, spośród których najczęściej wykorzystywany jest argument :test, którego wartością może być obiekt funkcyjny określający używany test równości. Domyślnie jego wartością jest #'eql, lecz w pewnych przypadkach potrzebne może być użycie innej funkcji porównującej. Przydatny jest także argument :key, przez który można przekazać funkcję klucza — wówczas przy wyszukiwaniu, zliczaniu lub usuwaniu porównuje się nie same elementy, lecz wyniki zwracane dla nich przez tę funkcję. Specyficzny dla funkcji find i position jest argument argument :from-end, który — jeśli podana jest dla niego wartość różna od nil — powoduje wyszukiwanie od końca sekwencji. Poniżej przedstawione zostały proste demonstracje użycia trzech omawianych funkcji.

> (find '(3 4) '((1 2) (3 4) (5 6))) ==> nil > (find '(3 4) '((1 2) (3 4) (5 6)) :test #'equal) ==> (3 4) > (find 3 '((1 a) (2 b) (3 c) (3 d) (4 e) (5 f)) :key #'first) ==> (3 C) > (find 3 '((1 a) (2 b) (3 c) (3 d) (4 e) (5 f)) :key #'first :from-end t) ==> (3 D) > (position 3 '((1 a) (2 b) (3 c) (3 d) (4 e) (5 f)) :key #'first) ==> 2 > (position 3 '((1 a) (2 b) (3 c) (3 d) (4 e) (5 f)) :key #'first :from-end t) ==> 3 > (count 1 '((1 a) (2 b) (3 c) (1 d) (2 e) (1 f)) :key #'first) ==> 3 > (setq lst '((1 a) (2 b) (3 c) (1 d) (2 e) (1 f))) ==> ((1 A) (2 B) (3 C) (1 D) (2 E) (1 F)) > (remove 1 lst :key #'first) ==> ((2 B) (3 C) (2 E)) > lst ==> ((1 A) (2 B) (3 C) (1 D) (2 E) (1 F))

Funkcje find, position, count i remove mają swoje nieco uogólnione wersje o nazwach find-if, position-if, count-if i remove-if, w których rolę kryterium wyszukiwania, zliczania lub usuwania nie pełni równość (jakkolwiek byłaby określana) danemu elementowi, lecz dowolny warunek określony przez obiekt funkcyjny. Wywołania tych funkcji mają postać:

(find-if fun sekw) (position-if fun sekw) (count-if fun sekw) (remove-if fun sekw)

Ich działanie i wartości zwracane są w pełni analogiczne do odpowiednich funkcji omawianych wcześniej, z tym że w ich przypadku wyszukiwanie, zliczanie i usuwanie dotyczy elementów sekwencji, dla których funkcja określona przez pierwszy argument daje w wyniku logiczną prawdę, czyli wartość różną od nil. Argument kluczowy :test nie ma wobec tego dla nich zastosowania, ale może być używany argument :key. Jeśli będzie za jego pomocą podana funkcja klucza, funkcja fun będzie stosowana dopiero do zwracanych przez nią wyników, a nie bezpośrednio do elementów listy. Dla funkcji find-if pozostaje ważny argument kluczowy from-end. Przykłady użycia omawianych funkcji przedstawione są poniżej.

> (find-if #'(lambda (x) (> x 5)) '(2 4 6 8 10)) ==> 6 > (position-if #'(lambda (x) (> x 5)) '(2 4 6 8 10)) ==> 2 > (count-if #'(lambda (x) (and (> x 0.3) (< x 0.7))) #(0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9)) ==> 3 > (remove-if #'null '(1 2 nil 3 nil 4 5 nil 6 nil 7 8)) ==> (1 2 3 4 5 6 7 8)

Tylko listy

Ze względu na specyfikę sekwencji będących listami repertuar funkcji służących do ich przetwarzania jest szerszy niż wspólny zestaw operacji dostępnych dla wszystkich sekwencji. Specyficzny jest także sposób tworzenia list przez łączenie elementów, dostęp do przyrostków (końcowych podlist) list, oraz łączenie list. Są dla nich również określone dodatkowe funkcje realizujące odwzorowania iteracyjne.

Tworzenie list

O wykorzystywanych do tworzenia list funkcjach cons i list była już mowa wyżej. Oprócz nich dostępna jest także funkcja make-list, która tworzy listę o podanej jako jej argument długości. Kluczowy argument :initial-element pozwala na podanie elementu, którym tworzona lista będzie wypełniona. Przy jego braku jest to wartość nil. Przykład tworzenia listy w opisany sposób może być następujący:

> (make-list 5) ==> (NIL NIL NIL NIL NIL) > (make-list 5 :initial-element 0) ==> (0 0 0 0 0) > (make-list 5 :initial-element '(1 2)) ==> ((1 2) (1 2) (1 2) (1 2) (1 2))

Dostęp do elementów

Dostęp do pierwszego elementu listy zapewnia omawiana już funkcja first. Ponieważ funkcja rest udostępnia z kolei podlistę złożoną z jej pozostałych elementów, jej wielokrotne stosowanie pozwala w zasadzie na uzyskanie dostępu do dowolnego elementu listy. Ilustruje to poniższy przykład.

> (setq lst '(a b c d e f)) ==> (A B C D E F) > (first (rest (rest (rest lst)))) ==> D

Wygodniejszy jest dostęp do elementu listy o podanym numerze porządkowym realizowany przez funkcję nth, chociaż jego faktyczna realizacja może sprowadzać się do przesuwania się po liście tak jak przy wielokrotnym stosowaniu funkcji rest, co oznacza, że jest to operacja kosztowna dla elementów odległych od początku listy. Pierwszym argumentem funkcji nth jest liczony od 0 numer elementu, a drugim lista, tak jak demonstruje to poniższy przykład.

> (nth 1 '(1 2 3 4 5)) ==> 2

Ponadto istnieją funkcje jednoargumentowe dające dostęp do pierwszych dziesięciu elementów listy, z których pierwszą jest funkcja first, a pozostałe mają nazwy second, third, fourth, fifth, sixth, seventh, eighth, ninth i tenth. Elementy listy udostępniane przez funkcje first, nth oraz funkcje second, third itd. mogą być zmieniane za pomocą operatora specjalnego setf, czyli są uogólnionymi zmiennymi.

Dostęp do podlist

Wielokrotnie już omawiana funkcja rest daje w wyniku podlistę listy przekazanej jej jako argument z pominięciem pierwszego elementu. Należy zwrócić uwagę, że nie jest to skopiowany, lecz rzeczywisty fragment listy oryginalnej, więc zmiana elementów takiej podlisty skutkuje odpowiednią zmianą także dla całej lisy. Ilustruje to następujący przykład.

> (setq l1 '(1 2 3 4 5)) ==> (1 2 3 4 5) > (setq l2 (rest l1)) ==> (2 3 4 5) > (setf (first l2) 'b) ==> B > l1 ==> (1 B 3 4 5)

Tu znowu jest modyfikowany literał. Pisałem już o tym. Powinno być: (SETQ L1 (LIST 1 2 3 4 5). Ponieważ ten błąd powtarza się jeszcze kilka razy nie będę go już osobno komentował tylko wprowadzę poprawki w oryginalnym tekście.

Uogólnioną wersją funkcji rest jest funkcja nthcdr, która zwraca podlistę listy otrzymaną przez pominięcie określonej przez pierwszy argument liczby początkowych jej elementów. Z kolei funkcja last pozwala uzyskać podlistę złożoną z podanej liczby ostatnich elementów. Jej pierwszym argumentem jest lista, a drugim liczba elementów, jaką ma liczyć podlista. Jest to argument opcjonalny o domyślnej wartości 1. Poniższe przykłady ilustrują sposób użycia tych funkcji.

> (nthcdr 3 '(1 2 3 4 5)) ==> (4 5) > (last '(1 2 3 4 5) 3) ==> (3 4 5) > (last '(1 2 3 4 5) 3) ==> (5)

O ile końcowa podlista dowolnej długości może być potraktowana jako fragment listy oryginalnej i przez to jej wyznaczenie jest stosunkowo efektywne (chociaż może być konieczne "dotarcie" do odpowiedniego miejsca tej listy od początku), nie jest możliwe wyodrębnienie w ten sposób początkowej podlisty bez niszczenia listy oryginalnej. Służąca do tego celu funkcja butlast jest więc mniej efektywna, gdyż jej działanie polega na skopiowaniu odpowiedniej liczby elementów z początku listy. Jej pierwszym argumentem jest lista, a drugim opcjonalna (domyślnie 1) liczba końcowych elementów tej listy, które mają być pominięte przy tworzeniu jej kopii, zwracanej jako wynik. Funkcja ta ma także swoją efektywną destrukcyjną wersję o nazwie nbutlast, w której lista pierwotna nie jest kopiowana, lecz "przycinana" do odpowiedniej długości - tym samym modyfikowana. Ilustrują to poniższe przykłady.

> (setq lst (copy-seq '(1 2 3 4 5))) ==> (1 2 3 4 5) > (butlast lst) ==> (1 2 3 4) > (butlast lst 2) ==> (1 2 3) > lst ==> (1 2 3 4 5) > (nbutlast lst 3) ==> (1 2 3) > lst ==> (1 2 3)

Powiększanie i łączenie list

Podczas przetwarzania list często występuje potrzeba ich powiększania przez dołączanie nowych elementów lub łączenia list wcześniej istniejących. Podstawowa funkcja do tworzenia list, cons, w gruncie rzeczy jest funkcją dołączającą do istniejącej listy nowy element, umieszczany na jej początku. Jednak w wywołaniu:

(cons e lst)

wartość zmiennej lst nie ulega zmianie — jest to w dalszym ciągu lista pierwotna, natomiast lista powiększona o element e jest wynikiem podanej formy. Aby taka zmodyfikowana lista stała się nowa wartością zmiennej, należałoby użyć jawnego przypisania:

(setq lst (cons e lst))

Tę samą operację można zapisać krócej w następujący sposób:

(push e lst)

Funkcja push (ściśle rzecz biorąc, jest to makrodefinicja, lecz dla poniższej dyskusji nie ma to znaczenia) umieszcza element podany jako pierwszy argument na początku listy podanej jako drugi argument, który musi być (uogólnioną) zmienną, zwracając nową listę i jednocześnie przypisując ją tej zmiennej. Sposób działania tej funkcji ilustrują następujące przykłady:

> (setq lst nil) ==> NIL > (push 'a lst) ==> (A) > (push 'b lst) ==> (B A) > lst ==> (B A) > (dotimes (i 10 lst) (push i lst)) ==> (9 8 7 6 5 4 3 2 1 0 B A)

Podobną operację wykonuje funkcja pushnew, wywoływana w ten sam sposób co funkcja push. Jej działanie różni się tym, że dołączanie nowego elementu do listy następuje tylko wówczas, gdy lista ta dotychczas takiego elementu nie zawiera. Dokładniej, sprawdza się, czy podany element nie jest równy jednemu z dotychczasowych elementów listy. Domyślnie porównywanie przeprowadzane jest za pomocą funkcji eql, ale użycie innej funkcji można spowodować przekazując odpowiedni obiekt funkcyjny za pomocą argumentu kluczowego :test. Z kolei argument kluczowy :key pozwala na przekazanie funkcji, która będzie stosowana do elementów (dotychczasowych i nowego) przed ich porównywaniem — wówczas porównaniu podlegają zwracane przez tę funkcję wyniki. Użycie funkcji pushnew ilustrują proste przykłady przedstawione poniżej.

> (setq lst '(2 3)) ==> (2 3) > (pushnew 1 lst) ==> (1 2 3) > lst ==> (1 2 3) > (pushnew 2 lst) ==> (1 2 3) > (pushnew '(1 a) lst) ==> ((1 a) 1 2 3) > (pushnew '(1 a) lst) ==> ((1 A) (1 A) 1 2 3) > (pushnew '(1 a) lst :test #'equal) ==> ((1 A) (1 A) 1 2 3) > lst ==> ((1 A) (1 A) 1 2 3)

Tak jak funkcja push ma się do funkcji cons, funkcja pushnew ma się do funkcji adjoin. Ta ostatnia też dołącza element do listy tylko jeśli go w niej dotąd nie ma (a ściślej, nie ma w niej elementu równego) lecz nie zmienia jednocześnie przekazanej jako argument zmiennej listowej — zresztą ten argument w ogóle nie musi być zmienną. Powiększona (ewentualnie) lista jest zwracania. Na sposób porównywania elementów można wpływać za pomocą argumentów kluczowych :test i :key.

Dodawanie elementów na początku listy jest operacją o bardzo efektywnej implementacji. W przypadku funkcji pushnew i adjoin koszt oczywiście jest zwiększony ze względu na konieczność porównywania nowego elementu z dotychczasowymi, lecz samo jego dołączenie do listy jest proste. Jednak dołączanie elementów do listy na jej końcu jest operacją bardziej złożoną, gdyż wymaga "przesunięcia się" na końcową pozycję w liście, co wymaga czasu proporcjonalnego do liczby jej elementów. Z tego właśnie względu implementacja funkcji append może być nieefektywna. Funkcja ta przyjmuje jako argumenty dowolną liczbę list (w najprostszym przypadku dwie), które łączy w jedną listę w kolejności ich występowania na liście argumentów, zwracając połączoną listę jako wynik. Jednocześnie poszczególne listy oryginalne nie ulegają zmianie, co oznacza, że ich zawartość jest w istocie rzeczy kopiowana do nowej listy. Podobną operację łączenia list wykonuje funkcja nconc, lecz w tym przypadku nowa lista powstaje bez kopiowania przez faktyczne łączenie poszczególnych list pierwotnych, które w związku z tym mogą ulec zmianie. Jest to więc destrukcyjna wersja funkcji append. Poniższe przykłady demonstrują sposób użycia obu tych funkcji.

> (setq l1 (list 1 2 3)) ==> (1 2 3) > (setq l2 (list 4 5 6)) ==> (4 5 6) > (setq l3 (list 7 8 9)) ==> (7 8 9) > (append l1 l2 l3) ; uwaga: APPEND nie kopiuje ostatniego argumentu. ==> (1 2 3 4 5 6 7 8 9) > l1 ==> (1 2 3) > l2 ==> (4 5 6) > l3 ==> (7 8 9) > (nconc l1 l2 l3) ==> (1 2 3 4 5 6 7 8 9) > l1 ==> (1 2 3 4 5 6 7 8 9) > l2 ==> (4 5 6 7 8 9) > l3 ==> (7 8 9)

Odwzorowania iteracyjne

Oprócz omówionych wyżej odwzorowań iteracyjnych dla sekwencji dowolnego typu określone są dodatkowe odwzorowania iteracyjne specyficzne dla list. Ich istotą jest jak zwykle stosowanie pewnej funkcji kolejno do wszystkich elementów listy lub większej liczby list oraz ewentualne łączenie w odpowiedni sposób ich wyników, tak aby powstała z nich lista. Dostępny jest jednak także inny wariant odwzorowania iteracyjnego, w którym wywołania funkcji powtarzane są nie dla kolejnych elementów list, ale dla ich kolejnych podlist. Podstawowe rodzaje odwzorowań iteracyjnych dla list przeprowadzane są za pomocą funkcji mapcar, mapc, maplist i mapl.

Istnieją jeszcze funkcje: MAPCAN i MAPCON. Można ich używać np. do filtrowania elementów listy:

> (MAPCAN #'(LAMBDA (X) (WHEN X (LIST X)))
'(1 NIL 2 NIL 3 NIL 4))
==> (1 2 3 4)

Powyższy przykład zamieściłem w celach demonstracyjnych, w tym przypadku lepiej było by użyć REMOVE lub REMOVE-IF:

> (REMOVE NIL '(1 NIL 2 NIL 3 NIL 4))
==> (1 2 3 4)

> (REMOVE-IF #'NULL '(1 NIL 2 NIL 3 NIL 4))
==> (1 2 3 4)

Dygresja: w poprzednio omawianej funkcji PODLISTY użyłem MAPCAN w taki sposób że nie ma możliwości zastąpienia jej pojedynczą funkcją operującą na sekwencjach (mam na myśli REMOVE, itp.).

Jeśli chcemy przetworzyć tylko jedną listę (tak jak w powyższym przykładzie) lepiej jest użyć REMOVE, itp. MAPCAN przydaje się jeśli mamy do czynienia z kilkoma listami:

(DEFPARAMETER *PANOWIE*
(MAPCAR #'(LAMBDA (X) (CONS X 'SIEDZI-W-DOMU))
'(STEFAN JERZY ZBIGNIEW LECH FRANCISZEK)))

(DEFPARAMETER *PANIE*
(MAPCAR #'(LAMBDA (X) (CONS X 'SIEDZI-W-DOMU))
'(KATARZYNA JOLANTA KRYSTYNA BARBARA OLA)))

(DEFUN ZMIEŃ-STATUS (&REST OSOBY)
(DOLIST (LUD¬ OSOBY)
(LET ((ENTRY
(ASSOC LUD¬ (APPEND *PANOWIE* *PANIE*))))
(WHEN ENTRY
(RPLACD ENTRY
(IF (EQ 'SIEDZI-W-DOMU (CDR ENTRY))
'NIE-WIADOMO-GDZIE-PRZEBYWA
'SIEDZI-W-DOMU))))))

(DEFUN NA-PIWKU-P ()
(MAPCAN #'(LAMBDA (&REST X)
(MAPCAR #'CAR
(REMOVE 'SIEDZI-W-DOMU X
:KEY #'CDR)))
*PANOWIE*
*PANIE*))

> (ZMIEŃ-STATUS 'STEFAN 'JOLANTA 'LECH 'OLA)
==> NIL

> *PANIE*
==> ((KATARZYNA . SIEDZI-W-DOMU)
(JOLANTA . NIE-WIADOMO-GDZIE-PRZEBYWA)
(KRYSTYNA . SIEDZI-W-DOMU)
(BARBARA . SIEDZI-W-DOMU)
(OLA . NIE-WIADOMO-GDZIE-PRZEBYWA)
(MAGDALENA . SIEDZI-W-DOMU))

> (NA-PIWKU-P)
==> (STEFAN JOLANTA LECH OLA)

> (ZMIEŃ-STATUS 'STEFAN 'JOLANTA 'LECH 'OLA 'FRANCISZEK)
==> NIL

> (NA-PIWKU-P)
==> (FRANCISZEK)

W jednym z pierwszych komentarzy wspomniałem że Lisp umożliwia literalny zapis cyklicznych struktur. Pierwszą definicję z powyższego przykładu można by zapisać tak:

(DEFPARAMETER *PANOWIE*
(MAPCAR #'CONS
'(STEFAN JERZY ZBIGNIEW LECH FRANCISZEK)
'#0=(SIEDZI-W-DOMU . #0#)))

> *PANOWIE*
==>
((FRANCISZEK . SIEDZI-W-DOMU)
(LECH . SIEDZI-W-DOMU)
(ZBIGNIEW . SIEDZI-W-DOMU)
(JERZY . SIEDZI-W-DOMU)
(STEFAN . SIEDZI-W-DOMU))

Ponieważ w przykładzie używam list asocjacyjnych najlepiej jest użyć operatora dedykowanego do ich konstrukcji:

(DEFPARAMETER *PANOWIE*
(PAIRLIS
'(STEFAN JERZY ZBIGNIEW LECH FRANCISZEK MACIEJ)
(MAKE-LIST 6 :INITIAL-ELEMENT 'SIEDZI-W-DOMU)))

Każda z funkcji realizujących odwzorowania iteracyjne dla list przyjmuje obiekt funkcyjny jako pierwszy argument oraz dowolną liczbę list jako pozostałe argumenty, przy czym liczba argumentów obiektu funkcyjnego musi odpowiadać liczbie przekazanych list. Obiekt funkcyjny stosowany jest w przypadku funkcji mapcar i mapc kolejno do pierwszych elementów wszystkich list, potem do ich drugich elementów itd. Dla funkcji maplist i mapl argumentami wywołań obiektu funkcyjnego są kolejno całe listy, ich podlisty powstałe przez pominięcie pierwszego elementu, następnie podlisty bez dwóch pierwszych elementów itd. W obu przypadkach proces ten kończy się po wyczerpaniu elementów najkrótszej z list. Wartości uzyskiwane z kolejnych wywołań dostarczonej funkcji są łączone w listę wynikową w przypadku odwzorowania realizowanego przez funkcję mapcar lub maplist. W przypadku funkcji mapc i mapl są one ignorowane i jako wynik zwracana jest pierwsza lista będąca argumentem funkcji — wówczas obiekt funkcyjny stosowany jest tylko ze względu na swoje efekty uboczne. Przedstawione niżej przykładu demonstrują sposób użycia omawianych funkcji.

> (mapcar #'(lambda (x y) (+ x y)) '(1 2 3 4 5) '(2 4 6 8 10)) ==> (3 6 9 12 15) > (setq sum 0) > (mapc #'(lambda (x) (incf sum x)) '(1 2 3 4 5)) ==> (1 2 3 4 5) > sum ==> 15 > (maplist #'append '(1 2 3 4) '(a b c d e)) ==> ((1 2 3 4 A B C D) (2 3 4 B C D) (3 4 C D) (4 D)) > (setq lst nil) > (mapl #'(lambda (l) (push l lst)) '(a b c d e)) ==> (A B C D E) > lst ==> ((E) (D E) (C D E) (B C D E) (A B C D E))

W tym przykładzie:

(MAPCAR #'(LAMBDA (X Y) (+ X Y))
'(1 2 3 4 5)
'(2 4 6 8 10))

LAMBDA jest niepotrzebna. Wystarczy napisać:

(MAPCAR #'+ '(1 2 3 4 5) '(2 4 6 8 10))

Krótsza forma w niektórych implementacjach (np. CMUCL, ale nie w SBCL) powoduje niepotrzebną rezerwację pamięci — co może mieć wpływ na wydajność.

Tylko wektory

Najważniejsze specyficzne dla wektorów operacje dotyczą ich tworzenia i dostępu do elementów — o obu tych operacjach była już mowa. Nie są natomiast dla nich dostępne funkcje służące do ich łączenia ani żadne dodatkowe formy odwzorowań iteracyjnych.

Tworzenie wektorów

Podstawowym sposobem tworzenia wektorów jest użycie omawianej już wyżej funkcji make-array, której jako argument przekazuje się długość wektora oraz ewentualnie za pomocą argumentu kluczowego initial-element wartość używaną do inicjalizacji jego wszystkich elementów. Dostępna jest jednak także alternatywna metoda, pozwalająca utworzyć wektor bezpośrednio z podanych elementów. Działanie takie ma funkcja vector, tworząca wektor, którego kolejnymi elementami są jej wszystkie argumenty. Następujący przykład demonstruje jej użycie:

> (vector 1 2 3 'a 'b 'c) ==> #(1 2 3 A B C)

Dostęp do elementów

Swobodny dostęp do elementów wektora zapewnia funkcja indeksowania svref, która została już w wystarczającym stopniu omówiona wyżej.

Standardowe funkcje

W krótkim przewodniku po języku Lisp trudno wymienić i omówić choćby niewielką część przydatnych w pisaniu programów standardowych funkcji. Najważniejsze z tych, które dotyczą przetwarzania sekwencji (list i wektorów) zostały omówione wyżej nieco dokładniej, ze względu na ich specyficzne dla języka Lisp znaczenie i brak odpowiedników w większości innych języków programowania. Tu wymienione i bardzo krótko scharakteryzowane będą wybrane inne funkcje standardowe. Dla każdej z nich w nawiasie podana będzie lista argumentów, a następnie krótki sposobu działania i przykład użycia.

Funkcje matematyczne

W tej grupie znajdują się funkcje o argumentach i wartościach liczbowych, między innymi obliczające wartości matematycznych funkcji elementarnych, takich jak funkcja potęgowa, wykładnicza i logarytmiczna, funkcje trygonometryczne itd. W większości mają one łatwe do odgadnięcia nazwy i sposób działania zgodny z oczekiwaniami. Wymieniamy tylko kilka z nich.

  • abs (x) — zwraca wartość bezwzględną argumentu x:

    > (abs -1.2)
    ==> 1.2
  • evenp, oddp (x) — zwracają t jeśli argument jest odpowiednio liczbą parzystą i nieparzystą oraz nil w przeciwnym przypadku:

    > (oddp 251)
    ==> NIL
    > (evenp 251)
    ==> T
  • floor, ceiling (x) — zwracają wartość argumentu zaokrąglona odpowiednio w dół i w górę do najbliższej liczby całkowitej jako pierwszą wartość oraz resztę jako drugą wartość:

    > (floor 3.75)
    ==> 3 ;
    0.75
    > (ceiling 3.25)
    ==> 4 ;
    -0.75
  • round (x) — zwraca wartość argumentu zaokrąglona do najbliższej liczby całkowitej jako pierwszą wartość oraz resztę jako drugą wartość:

    > (round 3.75)
    ==> 4 ;
    -0.25
    > (round 3.25)
    ==> 3 ;
    0.25
  • exp (x) — zwraca wartość funkcji wykładniczej dla argumentu x, czyli wartość e podniesioną do potęgi x:

    > (exp 1)
    ==> 2.7182817
  • expt (x y) — zwraca wartość x podniesioną do potęgi y:

    > (expt 27 0.33)
    ==> 2.9672222
  • log (x) — zwraca logarytm naturalny z argumentu x:

    > (log 2.7182818)
    ==> 0.99999994
  • max (x1 x2 ...) — zwraca maksymalną z wartości dowolnej liczby argumentów:

    > (max -1 3 18 -5 4)
    ==> 18
  • min (x1 x2 ...) — zwraca minimalną z wartości dowolnej liczby argumentów:

    > (min -1 3 18 -5 4)
    ==> -5
  • mod (x y) — zwraca wartość x modulo y:

    > (mod 23 4)
    ==> 3
  • sin, cos, tan (x) — zwracają wartość funkcji sinus, cosinus, tangens dla argumentu x:

    > (sin 3.14)
    ==> 0.001592548
  • sqrt (x) — zwraca pierwiastek kwadratowy z argumentu x:

    > (sqrt 2)
    ==> 1.4142135
  • random (x) — zwraca nieujemną liczbę pseudolosową mniejszą od wartości x i typu takiego jak x:

    > (random 10)
    ==> 4
    > (random 10.0)
    ==> 6.800773

Operacje na napisach

Chociaż ze względu na dostępność typu symbolicznego napisy są w języku Lisp używane rzadziej niż w innych językach programowania, repertuar standardowych funkcji umożliwia wykonywania na nich podstawowych manipulacji, takich jak konkatenacja, wyszukiwanie podnapisów, wyodrębnianie fragmentów i pojedynczych znaków.

  • string (s) — zwraca argument przekształcony do postaci napisu, przy czym dostępne są następujące przekształcenia: 1) jeśli s jest napisem, jest zwracany; 2) jeśli s jest symbolem, zwracana jest jego nazwa jako napis (z wielkimi literami); 3) jeśli s jest znakiem, zwracany jest jednoznakowy napis złożonego z tego znaku:

    > (string "abc")
    ==> "abc"
    > (string 'abc)
    ==> "ABC"
    > (string #\a)
    ==> "a"
  • string= ( s1 s2) — zwraca t jeśli napisy s1 i s2 są jednakowe albo nil w przeciwnym przypadku:

    > (string= "abc" (string 'abc))
    ==> NIL
    > (string= "ABC" (string 'abc))
    ==> T

    STRING=, STRING>, STRING<, ... działają także na symbolach (porównują nazwy, btw. do uzyskania nazwy symbolu służy funkcja STRING lub SYMBOL-NAME). Tak więc w powyższym przykładzie użycie funkcji STRING nie jest koniczne:

    > (STRING= "abc" 'ABC) ==> NIL
    > (STRING= "abc" '|abc|) ==> T
    > (STRING= "ABC" 'ABC) ==> T
    > (STRING-EQUAL "abc" 'ABC) ==> T
    > (STRING-EQUAL "abc" "ABC") ==> T
  • string<, string>, string<=, string>= (s1 s2) — jeśli napis s1 jest odpowiednio mniejszy, większy, mniejszy lub równy i większy niż równy od napisu s2 w sensie porządku leksykograficznego, zwracają liczbę początkowych identycznych znaków w tych napisach; w przeciwnym przypadku zwracają nil:

    > (string< "abcdef" "abcz")
    ==> 3
    > (string>= "abcdef" "abcz")
    ==> NIL
  • string-concat ( s1 s2) — zwraca napis powstały z konkatenacji ("sklejenia") napisów s1 i s2:

    > (string-concat "abc" "def")
    ==> "abcdef"

    Funkcji STRING-CONCAT nie ma w standardzie, występuje ona w konkretnej implementacji — w CLISPie. Do sklejania (między innymi) napisów używa się funkcji CONCATENATE.

  • string-upcase (s) — zwraca kopię napisu s, w której małe litery są zmienione na wielkie:

    > (string-upcase "abcDEF")
    ==> "ABCDEF"
  • string-downcase (s) — zwraca kopię napisu s, w której wielkie litery są zmienione na małe:

    > (string-downcase "abcDEF")
    ==> "abcdef"

Powyższa lista nie wyczerpuje operacji na napisach (podobnie jak inne zestawienia z tego tutorialu przedstawia tylko część operatorów).

Operacje na zbiorach

Funkcje tej grupy realizują operacje teoriomnogościowe zakładając, że zbiory reprezentowane są za pomocą list. Dla każdej z tych funkcji jest jednoznacznie określone, jakie elementy znajdą się w liście zwracanej jako wynik, lecz ich kolejność jest zależna od implementacji. Do porównywania elementów list używana jest domyślnie funkcja eql. Funkcje wyznaczające iloczyn i różnicę zbiorów mają swoje wersje "destrukcyjne" o nazwach, do których dodano przedrostek n. Zwracają one takie same wyniki jak wersje zwykle, ale są bardziej efektywne unikając — tam gdzie jest to możliwe — kopiowania fragmentów list i używając fragmentów list dostarczonych jako argumenty. Oznacza to, że listy te mogą zostać zmodyfikowane.

  • union (s1 s2) — zwraca listę reprezentującą sumę zbiorów reprezentowanych przez listy s1 i s2:

    > (union '(1 2 3 4) '(3 4 5 6))
    ==> (1 2 3 4 5 6)
  • intersection (s1 s2) — zwraca listę reprezentującą iloczyn zbiorów reprezentowanych przez listy s1 i s2:

    > (intersection '(1 2 3 4) '(3 4 5 6))
    ==> (3 4)
  • nintersection (s1 s2) — jak intersect, ale może zmodyfikować listy s1 i s2 aby uniknąć kopiowania.
  • set-difference (s1 s2) — zwraca listę reprezentującą różnicę zbiorów reprezentowanych przez listy s1 i s2:

    > (set-difference '(1 2 3 4) '(3 4 5 6))
    ==> (1 2)
  • nset-difference (s1 s2) — jak set-difference, ale może zmodyfikować listy s1 i s2 aby uniknąć kopiowania.

Operatory o których mowa w tym zestawieniu akceptują różnorakie argumenty kluczowe (np. :KEY, :TEST). Proponuję zajrzeć do (CHLS: 14.1.2.2 Lists as Sets).

Organizacja kodu

Złożone programy składają się z wielu funkcji, z których niektóre mogą być wywoływane bezpośrednio przez użytkownika udostępniając mu poszczególne cechy i możliwości programu, a niektóre są tylko używane wewnętrznie przez inne funkcje. Może niekiedy zachodzić także potrzeba wykorzystania zmiennych globalnych, przechowujących pewne dane lub ustawienia używane przez więcej niż jedną funkcję (lub więcej niż jedno wykonanie jednej funkcji). Aby łatwiejsze było panowanie nad dużą liczbą funkcji i zmiennych, kontrolowanie dostępu do nich, unikanie konfliktów nazw oraz wielokrotne wykorzystywanie wybranych grup funkcji, większe programy w języku Lisp organizowane są jednostki nazywane pakietami. W praktyce pakiety najczęściej związane są jednoznacznie z plikami źródłowymi. Pozwalają one na określenie, które z symboli (przede wszystkim nazw funkcji i zmiennych) zdefiniowanych w danym pliku są dostępne na zewnątrz oraz symbole z jakich innych pakietów są w nim wykorzystywane.

"W praktyce pakiety najczęściej związane są jednoznacznie z plikami źródłowymi." O ile wiem praktyka jest inna — duży pakiet jest często rozbijany na kilka(naście) plików.

Definiowanie pakietów

Pakiet, który fizycznie jest najczęściej tożsamy z plikiem źródłowym, logicznie może być traktowany jako zbiór symboli wraz z ich ewentualnymi definicjami (dotyczy to symboli będących nazwami funkcji, zmiennych, struktur itp.). Takie same symbole z różnych pakietów traktowane są jak różne symbole, czyli nie mogą między nimi wystąpić konflikty nazw. Symbole każdego pakietu dzielą się na dwie grupy: publiczne (eksportowane) i prywatne, co pozwala ograniczyć dostęp do niektórych symboli.

Najprostszą metodą definiowania pakietów jest użycie makrodefinicji defpackage, która automatycznie zastępowana jest wywołaniami odpowiednich funkcji. Ogólna postać jej zastosowania jest następująca:

(defpackage nazwa (:documentation tekst) (:use nazwa1 nazwa2 ...) (:export symbol1 symbol2 ...))

Operator DEFPACKAGE ma dużo więcej opcji.

Bezpośrednio po słowie defpackage występuje nazwa definiowanego pakietu. Nazwy pakietów mogą być zapisywane na dwa sposoby: albo jako symbole pisane wielkimi literami ujęte w cudzysłowy (np. "MOJ_PAKIET") albo symbole pisane za pomocą dowolnych liter poprzedzone dwukropkiem (np. :moj_pakiet).

Nazwą pakietu może być: napis (składający się z dowolnych znaków), symbol, znak. W przypadku symbolu lub znaku nazwa pakietu jest taka jakby na argumencie została wywołana funkcja STRING. W razie wątpliwości można zerknąć na definicje: package designator, string designator. Nie jest więc tak że jesteśmy ograniczeni do wielkich liter, bądź są nałożone jakieś restrykcje na sposób zapisu symboli. Btw, zdanie "...symbole pisane wielkimi literami ujęte w cudzysłowy..." jest bez sensu. Jeśli coś jest ograniczone cudzysłowami to jest napisem (no chyba że owe cudzysłowy są poprzedzone backslashem albo ujęte w znaki 'vertical bar' (|)).

Każda z kolejnych fraz w nawiasach jest opcjonalna i może być pominięta. Po słowie :documentation może być podany dowolny tekst ujęty w cudzysłowy jako opis pakietu. Po słowie :use można wymienić dowolną liczbę nazw innych (wcześniej zdefiniowanych) pakietów używanych przez definiowany pakiet. Taka deklaracja używania powoduje, że ich publiczne symbole stają się w tym pakiecie dostępne tak jak jego własne symbole, czyli są importowane (wtedy nie mogą w nim być definiowane takie same symbole, gdyż spowodowałoby to konflikt nazw). Wreszcie po słowie :export można wymienić dowolną liczbę symboli definiowanego pakietu, które będą publiczne, a więc dostępne dla innych pakietów. Symbole reprezentowane są tu przez ich nazwy, w postaci napisów w cudzysłowach, w których wszystkie litery są wielkie. Wszystkie pozostałe symbole pakietu będą jego symbolami prywatnymi.

Autor niepotrzebnie upiera się przy wielkich literach...

W poniższym przykładzie definiowany jest pakiet "MOJ_PAKIET", który używa pakietu "COMMON-LISP" (jest to pakiet zawierający wszystkie standardowe funkcje i zmienne globalne języka Common Lisp) oraz udostępnia jako publiczne symbole *zm1*, +zm2+, fun1 i fun2.

> (defpackage "MOJ_PAKIET" (:documentation "Przykladowy pakiet.") (:use "COMMON-LISP") (:export "*ZM1*" "+ZM2+" "FUN1" "FUN2")) ==> #< PACKAGE MOJ_PAKIET>

Definicja pakietu powoduje utworzenie pakietu o podanej nazwie i właściwościach, nie powoduje jednak automatycznie, że symbole, które następnie zostaną zdefiniowane, będą symbolami tego pakietu. Wszystkie nowe symbole należą zawsze do pakietu aktualnego w chwili ich wystąpienia. Pakietem aktualnym po uruchomieniu interpretera języka LISP jest pakiet "USER". Do jego zmiany służy funkcja in-package, której jako argument podaje się nazwę nowego pakietu aktualnego. Aby zdefiniować zmienne lub funkcje pewnego pakietu, należy najpierw uczynić go pakietem aktualnym. Dla zdefiniowanego w poprzednim przykładzie pakietu mogłoby to wyglądać następująco:

  • Myślę że autor pisząc "...nowe symbole należą zawsze do pakietu aktualnego w chwili ich wystąpienia..." ma na myśli chwilę kiedy zostaną wczytane przez odpowiednią procedurę.
  • Pakietem aktualnym po uruchomieniu jest pakiet denotowany przez napis "COMMON-LISP-USER", w skrócie (nickname) "CL-USER", a nie jak pisze autor "USER" (skrót "USER" jest charakterystyczny dla CLISPu, dalej poprawiłem kod coby nie pisać komentarza za każdym razem. CLISP oczywiście obsługuje skrót "CL-USER" — jest zgodny ze standardem ANSI — tyle że jak większość implementacji wprowadza wiele rozszerzeń).
  • Operator IN-PACKAGE jest makrem a nie funkcją.
  • "Aby zdefiniować zmienne lub funkcje pewnego pakietu, należy najpierw uczynić go pakietem aktualnym." tak się zazwyczaj robi, ale nie jest to konieczne — można kwalifikować symbole (o czym jest później mowa ale tylko w kontekście 'odczytu').
> (in-package "MOJ_PAKIET") ==> #< PACKAGE MOJ_PAKIET> > (defvar *zm1* 0) ==> *ZM1* > (defparameter +zm2+ nil) ==> +ZM2+ > (defun fun1 (x) (+ x *zm1*)) ==> FUN1 > (defun fun2 (x) (cons x +zm2+)) ==> FUN2

Używanie pakietów

Symbole każdego pakietu mogą być używane w ramach tego samego pakietu bez żadnych ograniczeń. Zatem najprostszym sposobem wykorzystania zmiennych lub funkcji zdefiniowanych w pewnym pakiecie jest uczynienie go pakietem aktualnym. Jednak to może spowodować, że niedostępne staną się symbole z pakietu poprzednio aktualnego. Zmiana pakietu aktualnego nie jest więc wygodnym sposobem korzystania z symboli różnych pakietów, a w szczególności nie pozwala na jednoczesne odwoływanie się do symboli z więcej niż jednego pakietu. Rozwiązaniem problemu jest używanie pakietów w innych pakietach.

Używanie przez pakiet innego pakietu, czyli importowanie jego symboli publicznych, pozwala na korzystanie z nich tak, jak gdyby były własnymi symbolami pakietu importującego. Jeśli symbol taki sam jak jeden z symboli importowanych jest już tam zdefiniowany, wystąpi konflikt nazw. Z symboli publicznych innego pakietu można jednak także korzystać bez ich importowania. Wystarczy w tym celu poprzedzać je przedrostkiem kwalifikującym złożonym z nazwy ich pakietu i dwukropka. W podobny sposób można się też odwołać do niepublicznych (prywatnych) symboli pakietu — trzeba wówczas użyć podwójnego dwukropka. Zatem różnica między symbolami publicznymi i prywatnymi nie polega ściśle rzecz biorąc na tym, że pierwsze są, a drugie nie są dostępne z zewnątrz, lecz na tym, że pierwsze mogą być importowane, a drugie nie, oraz na innej postaci przedrostka kwalifikującego dla symboli nieimportowanych. Dobry styl programowania nakazuje jednak, aby z możliwości dostępu do prywatnych symboli pakietów nigdy nie korzystać, a symbole z innych pakietów importować raczej niż używać przedrostka kwalifikującego (o ile nie powoduje to konfliktu nazw).

Używanie przez pakiet innych pakietów można zadeklarować nie tylko przy jego definiowaniu, lecz także w dowolnym momencie za pomocą funkcji use-package, jeśli pakietem importującym jest pakiet aktualny. Argumentem tej funkcji jest jedna lub więcej nazw pakietów, z których publiczne symbole zostaną zaimportowane do pakietu aktualnego. W szczególności dla pakietu z poprzednich przykładów jego zmiennych *zm1* i +zm2+ oraz funkcji fun1 i fun2 można używać w domyślnym pakiecie "CL-USER" na różne sposoby, zilustrowane w poniższym przykładzie:

> (in-package "MOJ_PAKIET") ==> #< PACKAGE MOJ PAKIET> > (setq *zm1* 10) ==> 10 > (in-package "CL-USER") ==> #< PACKAGE CL-USER> > (setq moj_pakiet:+zm2+ '(0)) ==> (0) > (moj_pakiet:fun1 5) ==> 15 > (use-package "MOJ_PAKIET") ==> T > (fun2 5) ==> (5 0)

Pakiety a pliki źródłowe

Praktycznie regułą jest jednoznacznie przyporządkowanie pakietów i plików źródłowych. Plik odpowiadający pewnemu pakietowi, któremu sensownie jest nadać nazwę identyczną z nazwą pakietu (z dodatkowym przyrostkiem .lsp lub .lisp), rozpoczyna się wówczas definicją tego pakietu, po czym następuje wywołanie funkcji in-package czyniącej ten pakiet pakietem aktualnym. Pozostałą część pliku wypełniają dowolne formy, w tym definicje zmiennych i funkcji. Nie ma potrzeby przywracania na końcu pliku poprzedniego pakietu aktualnego, gdyż zapewnia to funkcja load, która zawsze po wczytaniu pliku przywraca pakiet aktualny z chwili jej wywołania.

W przypadku, gdy definiowany w pewnym pliku pakiet używa innych pakietów, odpowiadające im pliki powinny być ładowane wcześniej. Jednak kontrolowanie tego mogłoby być dla użytkownika kłopotliwe. Ciężar tej kontroli może przejąć środowisko języka Lisp, jeśli w pakietach używa się w odpowiedni sposób funkcji require i provide. Funkcja require służy do sygnalizacji wymagania pewnych nazw (podanych jako symbole lub napisy), w praktyce tożsamych z nazwami pakietów. W przypadku, gdy którakolwiek z tych nazw nie jest dostępna, następuje ładowanie odpowiedniego pliku. Dokładny mechanizm wyznaczania nazwy pliku do załadowania jest zależny od konkretnego środowiska — na ogół sprowadza się do dodania do wymaganej a niedostępnej nazwy charakterystycznego dla tego środowiska przyrostka, najczęściej .lsp lub .lisp. Nazwę uważa się za dostępną, jeśli była ona argumentem wcześniejszego wywołania funkcji provide. Zatem w każdym pliku umieszcza się (zwykle na początku) wywołanie tej funkcji z nazwą odpowiadającą nazwie pliku (bez przyrostka). Jako ilustrację rozważmy trzy pliki o zawartości przedstawionej niżej.

  1. Plik "pakiet1.lsp":

    (provide 'pakiet1)

    (defpackage "PAKIET1"
    (:export "X1" "Y1" "Z1"))
  2. Plik "pakiet2.lsp":

    (provide 'pakiet2)

    (defpackage "PAKIET2"
    (:export "X2" "Y2" "Z2"))
  3. Plik "pakiet3.lsp":

    (require 'pakiet1 'pakiet2)

    (provide 'pakiet3)
    (defpackage "PAKIET3"
    (:use "PAKIET1" "PAKIET2"
    (:export "X3" "Y3" "Z3"))

Pakiet "PAKIET3" używa pakietów "PAKIET1" i "PAKIET2". Jednak nie ma potrzeby ładować plików "pakiet1.lsp" i "pakiet2.lsp" przed ładowaniem pliku "pakiet3.lsp". gdyż dzięki użyciu w odpowiedni sposób funkcji provide i require zadba o to środowisko. Po wywołaniu:

(load "pakiet3.lsp")

pliki "pakiet1.lsp" 1 "pakiet2.lsp" zostaną w razie potrzeby (czyli jeśli nie zostały wczytane wcześniej) załadowane przed plikiem "pakiet3.lsp".

 


Aby dodawać komentarze musisz być zalogowany!


Kontakt

Jeśli chcesz się z nami skontaktować napisz na adres: info(at)binboy.org lub odwiedź nasz profil na Facebooku!

O Nas

Serwis binboy.org to kopalnia wiedzy dla wszystkich z branży IT, w szczególności dla programistów i webmasterów. To duży zbiór kursów programowania, tutoriali, darmowych ebooków, setki kodów źródłowych itp.

Bądź w kontakcie

Panel użytkownika

Zaloguj się do panelu użytkownika.
Nie masz konta? Zarejestruj się!
Zapomniałeś hasła?