Przewodnik po języku Common Lisp V 1.01

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 setq są globalne,
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 ((<em>zmienna</em> <em>forma</em>)
(<em>zmienna</em> <em>forma</em>)
...
(<em>zmienna</em> <em>forma</em>))
<em>forma</em>
<em>forma</em>
...
<em>forma</em>)
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:
dla funkcji
eqrówność oznacza identyczność -- dwa obiekty są równe wtedy, gdy są w istocie tym samym (a nie tylko takim samym) obiektem,dla funkcji
eqlrówność oznacza zwykłą równość wartości w przypadku liczb tego samego typu, zwykłą równość w przypadku znaków oraz identyczność w pozostałych przypadkach,dla funkcji
equalrówność oznacza to samo co dla funkcjieqlw przypadku liczb, znaków i symboli oraz tzw. strukturalną równoważność dla napisów i list,dla funkcji
equalprówność oznacza strukturalną równoważność nie tylko w przypadku list, lecz również w przypadku wektorów i struktur.
Obiekty o identycznym zapisie (a więc intuicyjnie równe) niekoniecznie
są identyczne dla funkcji eq. Identyczność ta oznacza, że
porównywane obiekty są wewnętrznie jednym i tym samym obiektem, choć
może on być dostępny za pomocą różnych zmiennych. Identyczne są
zawsze takie same symbole, czasem liczby całkowite i znaki (zależnie
od implementacji), ale na ogół już nie liczby ułamkowe i
zmiennopozycyjne oraz napisy. Listy o takiej samej zawartości są
identyczne tylko jeśli są obie jedną i tą samą listą. Działanie
funkcji eq ilustrują następujące przykłady.
> (eq 'a 'a) ==> T > (eq 1.2 1.2) ==> NIL > (setq x 1.2) ==> 1.2 > (setq y x) ==> 1.2 > (eq x y) ==> T > (eq "abc" "abc") ==> NIL > (setq s1 "abc") ==> "abc" > (setq s2 s1) ==> "abc" > (eq s1 s2) ==> T > (eq '(a b) '(a b)) ==> NIL > (setq l1 '(a b)) ==> (A B) > (setq l2 l1) ==> (A B) > (eq l1 l2) ==> T
Działanie funkcji eql różni się od działania funkcji
eq tylko dla liczb i znaków. Według niej liczby takich samych
typów o takich samych wartościach są równe. Równe są również takie
same znaki. Obiekty innych typów uznawane są za równe tylko jeśli są
identyczne, tak jak to było wyjaśnione dla funkcji
eq. Funkcja eql jest najczęściej używanym testem
równości w języku Lisp. Kilka przykładów jej użycia znajduje się
poniżej:
> (eql 2 2) ==> T > (eql 2.0 2) ==> NIL > (eql 2.5 2.5) ==> T > (eql #\a #\a) ==> T > (eql #\a #\A) ==> NIL > (eql "abc" "abc") ==> NIL
Interpretacja równości realizowana przez funkcję equal różni
się od tej przyjętej w funkcji eql tylko dla napisów i
list. W przypadku obiektów tych typów równość nie wymaga już
identyczności, a tylko równości ich poszczególnych elementów
składowych. Napisy są równe według tej funkcji, jeśli równe są ich
wszystkie odpowiednie znaki (porównywane tak jak przez
eql). Równość list jest z kolei definiowana rekurencyjnie:
dwie listy są równe w sensie funkcji equal, jeśli równe w
sensie tej funkcji są ich pierwsze elementy ("głowy") oraz równe w
sensie tej funkcji są listy ich pozostałych elementów ("ogony"). To
właśnie jest strukturalna równoważność, która oznacza, że aby dwie
listy były równe, muszą mieć taką samą strukturę (w sensie
zagnieżdżenia list w listach), a odpowiadające sobie elementy na
najniższym poziomie (nie będące listami) muszą być równe. Dla obiektów
innych typów złożonych -- wektorów i struktur -- równość także w
przypadku funkcji equal jest równoważna
identyczności. lustrację działania funkcji equal stanowi
następująca seria prostych przykładów:
> (equal "abc" "abc") ==> T > (equal "abc" "ABC") ==> NIL > (equal '(a b c) '(a b c)) ==> T > (equal '(a (b c) d) '(a (b c) d)) ==> T > (equal '(a (b c) d) '(a b c d)) ==> NIL > (equal '((a b) "bcd") '((a b) "bcd")) ==> T
W przypadku funkcji equalp interpretacja równości jako
strukturalnej równoważności obejmuje nie tylko napisy i listy, lecz
także wektory i struktury. Dwa wektory są równe w sensie tej funkcji,
jeśli mają tyle samo elementów i ich wszystkie odpowiednie elementy są
równe w sensie tej funkcji. Dwie struktury tego samego typu są równe,
jeśli odpowiednio równe są ich składowe. Dla innych obiektów funkcja
ta działa tak samo jak funkcja equal.
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 <em>forma1</em> <em>forma2</em> ...) (unless <em>forma1</em> <em>forma2</em> ...)
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:
- licznik -- zmienna, której będą przypisywane kolejne numery iteracji, zaczynając od 0,
- liczba-powtórzeń -- forma, która jest wartościowana jednokrotnie na początku i której wartość (liczba całkowita) określa liczbę powtórzeń iteracji,
- wynik -- forma, która jest wartościowana jednokrotnie na
końcu i której wartość jest wynikiem formy
dotimes(jeśli zostanie pominięta, wynikiem jestnil.
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ą:
elem -- zmienna, której będą przypisywane kolejne elementy listy, względem której przeprowadzana jest iteracja, zaczynając od pierwszego,
lst -- forma, która jest wartościowana jednokrotnie na początku i której wartość musi być listą,
- wynik -- forma, która jest wartościowana jednokrotnie na
końcu i której wartość jest wynikiem formy
dolist(jeśli zostanie pominięta, wynikiem jestnil.
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
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.
abs(x) -- zwraca wartość bezwzględną argumentu x:
> (abs -1.2) ==> 1.2
evenp,oddp(x) -- zwracajątjeśli argument jest odpowiednio liczbą parzystą i nieparzystą oraznilw przeciwnym przypadku:
> (oddp 251) ==> NIL > (evenp 251) ==> T
floor,ceiling(x) -- zwracają wartość argumentu zaookrągloną odpowiednio w dół i w górę do najbliższej liczby całkowitej jako pierwszą wartość oraz resztę jako drugą wartość:
> (floor 3.75)
==> 3 ;
0.75
> (ceiling 3.25)
==> 4 ;
-0.75
round(x) -- zwraca wartość argumentu zaookrągloną do najbliższej liczby całkowitej jako pierwszą wartość oraz resztę jako drugą wartość:
> (round 3.75)
==> 4 ;
-0.25
> (round 3.25)
==> 3 ;
0.25
exp(x) -- zwraca wartość funkcji wykładniczej dla argumentu x, czyli wartość e podniesioną do potęgi x:
> (exp 1) ==> 2.7182817
expt(x y) -- zwraca wartość x podniesioną do potęgi y:
> (expt 27 0.33) ==> 2.9672222
log(x) -- zwraca logarytm naturalny z argumentu x:
> (log 2.7182818) ==> 0.99999994
max(x1 x2 ...) -- zwraca maksymalną z wartości dowolnej liczby argumentów:
> (max -1 3 18 -5 4) ==> 18
min(x1 x2 ...) -- zwraca minimalną z wartości dowolnej liczby argumentów:
> (min -1 3 18 -5 4) ==> -5
mod(x y) -- zwraca wartość x modulo y:
> (mod 23 4) ==> 3
sin,cos,tan(x) -- zwracają wartość funkcji sinus, cosinus, tangens dla argumentu x:
> (sin 3.14) ==> 0.001592548 - <code>sqrt</code> (<em>x</em>) -- zwraca pierwiastek kwadratowy z argumentu <em>x</em>: <pre> > (sqrt 2) ==> 1.4142135
random(x) -- zwraca nieujemną liczbę pseudolosową mniejszą od wartości x i typu takiego jak x:
> (random 10) ==> 4 > (random 10.0) ==> 6.800773
Operacje na napisach
Chociaż ze względu na dostępność typu symbolicznego napisy są w języku Lisp używane rzadziej niż w innych językach programowania, repertuar standardowych funkcji umożliwia wykonywania na nich podstawowych manipulacji, takich jak konkatenacja, wyszukiwanie podnapisów, wyodrębnianie fragmentów i pojedynczych znaków.
string(s) -- zwraca argument przekształcony do postaci napisu, przy czym dostępne są następujące przekształcenia: 1) jeśli s jest napisem, jest zwracany; 2) jeśli s jest symbolem, zwracana jest jego nazwa jako napis (z wielkimi literami); 3) jeśli s jest znakiem, zwracany jest jednoznakowy napis złożonego z tego znaku:
> (string "abc") ==> "abc" > (string 'abc) ==> "ABC" > (string #\a) ==> "a"
string=( s1 s2) -- zwracatjeśli napisy s1 i s2 są jednakowe albonilw przeciwnym przypadku:
> (string= "abc" (string 'abc)) ==> NIL > (string= "ABC" (string 'abc)) ==> T
string<,string>,string<=,string>=(s1 s2) -- jeśli napis s1 jest odpowiednio mniejszy, większy, mniejszy lub równy i większy niż równy od napisu s2 w sensie porządku leksykograficznego, zwracają liczbę początkowych identycznych znaków w tych napisach; w przeciwnym przypadku zwracająnil:
> (string< "abcdef" "abcz") ==> 3 > (string>= "abcdef" "abcz") ==> NIL
string-concat( s1 s2) -- zwraca napis powstały z konkatenacji ("sklejenia") napisów s1 i s2:
> (string-concat "abc" "def") ==> "abcdef"
string-upcase (s)-- zwraca kopię napisu s, w której małe litery są zmienione na wielkie:
> (string-upcase "abcDEF") ==> "ABCDEF"
string-downcase (s)-- zwraca kopię napisu s, w której wielkie litery są zmienione na małe:
> (string-downcase "abcDEF") ==> "abcdef"
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.
union(s1 s2) -- zwraca listę reprezentującą sumę zbiorów reprezentowanych przez listy s1 i s2:
> (union '(1 2 3 4) '(3 4 5 6)) ==> (1 2 3 4 5 6)
intersection(s1 s2) -- zwraca listę reprezentującą iloczyn zbiorów reprezentowanych przez listy s1 i s2:
> (intersection '(1 2 3 4) '(3 4 5 6)) ==> (3 4)
nintersection(s1 s2) -- jakintersect, ale może zmodyfikować listy s1 i s2 aby uniknąć kopiowania.set-difference(s1 s2) -- zwraca listę reprezentującą różnicę zbiorów reprezentowanych przez listy s1 i s2:
> (set-difference '(1 2 3 4) '(3 4 5 6)) ==> (1 2)
nset-difference(s1 s2) -- jakset-difference, ale może zmodyfikować listy s1 i s2 aby uniknąć kopiowania.
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.
[ol]
- Plik
"pakiet1.lsp":
(provide 'pakiet1) (defpackage "PAKIET1" (:export "X1" "Y1" "Z1"))
- Plik
"pakiet2.lsp":
(provide 'pakiet2) (defpackage "PAKIET2" (:export "X2" "Y2" "Z2"))
- Plik
"pakiet3.lsp":
(require 'pakiet1 'pakiet2) (provide 'pakiet3) (defpackage "PAKIET3" (:use "PAKIET1" "PAKIET2" (:export "X3" "Y3" "Z3"))
[/ol]
Pakiet "PAKIET3" używa pakietów "PAKIET1" i
"PAKIET2". Jednak nie ma potrzeby ładować plików
"pakiet1.lsp" i "pakiet2.lsp" przed ładowaniem pliku
"pakiet3.lsp". gdyż dzięki użyciu w odpowiedni sposób funkcji
provide i require zadba o to środowisko. Po wywołaniu:
(load "pakiet3.lsp")
pliki "pakiet1.lsp" 1 "pakiet2.lsp" zostaną w razie
potrzeby (czyli jeśli nie zostały wczytane wcześniej) załadowane przed
plikiem "pakiet3.lsp".
Aby dodawać komentarze musisz być zalogowany!
