Przewodnik po języku Common Lisp


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 czynuią 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ć.

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.

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

Dane

W języku Lisp występują typy danych i odpowiadające im mechanizmy przetwarzania znane z innch 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.

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:

> (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 inymi 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 zmienych 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ępuja 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 s wykły sposób 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.

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:

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

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.

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 odstepu, 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

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 lst '#(a b c d e))

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 cat), wektory często tworzone są od razu w pożądanym rozmiarze, choć może on być póxniej 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

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 skladowych, 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ąde 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 sa 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 zmiennopozycjny, 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:

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.

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 neg 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
> (neg 0)
==> nil
> (neg '())
==> 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

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 bespoś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 ostatnioej 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.

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: 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ć:

(dotimes (elem lst wynik)
  forma
   ...)
W nawiasach po słowie dolist występują: 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 nastepuje 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)

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ć zaprzestane. 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 wziać 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 f1)
            (f2 first f2))
        (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)

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

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 konktretnych sytuacjach. Kontynują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łądają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 namniej 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.

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ć funckję 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 funckji 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-form 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

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łasnie 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))))

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
    )

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.

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.

Łą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

Odzworowania 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ą odzworowania iteracyjnego jest przektształ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 odzworowanie 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 porzykładów odwzorowań iteracyjnych realizowanych za pomocą funkcji map przedstawiono poniżej.
> (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)
> (setq sum 0)
==> 0
> (map nil #'(lambda (x) (incf sum x))
       '(1 2 3 4 5))
==> NIL
> sum
==> 15
a

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.

Odwzorowania redukcyjne

Odwzorowania iteracyjne 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)

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. Ilsutrują to poniższe przykłady.

> (setq lst '(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 (ścliś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ówynwania 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 '(1 2 3))
==> (1 2 3)
> (setq l2 '(4 5 6))
==> (4 5 6)
> (setq l3 '(7 8 9))
==> (7 8 9)
> (append l1 l2 l3)
==> (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)

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

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

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.

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.

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

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.

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

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). 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 cydzysł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. 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:

> (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 "USER" na różne sposoby, zilustrowane w poniższym przykładzie:

> (in-package "MOJ_PAKIET")
==> #< PACKAGE MOJ PAKIET>
> (setq *zm1* 10)
==> 10
> (in-package "USER")
==> #< PACKAGE 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".