Kolekcje to abstrakcyjna klasa struktur danych, służąca do reprezentowania zbiorów elementów o wspólnych cechach bądź wspólnym przeznaczeniu. Umożliwiają one wyrażanie takich zestawów w sposób zgrupowany w pojedynczym obiekcie. W Clojure możemy korzystać z kilku wbudowanych kolekcji, a konkretnie z list, wektorów, map i zbiorów.
Kolekcje
Kolekcja (ang. collection), zwana też niekiedy kontenerem (ang. container), jest nazwą rodziny złożonych struktur danych, służących do grupowania elementów i pozwalających na dostęp do nich w ujednolicony sposób.
Warto mieć na względzie, że kolekcja nie musi być zbiorem danych konkretnego rodzaju, lecz strukturą, która grupuje elementy o pewnych wspólnych właściwościach. Mogą to być cechy implementacyjne (np. podobne typy danych) bądź związane z założeniami logiki aplikacji (np. dane o podobnym przeznaczeniu).
Możemy wyróżnić trzy podstawowe rodzaje kolekcji:
- liniowe (ang. linear),
- asocjacyjne (ang. associative),
- drzewiaste (ang. tree).
Niezależnie od tego, z jakiego konkretnie typu kolekcją mamy do czynienia, w Clojure jesteśmy w stanie korzystać z jednolitego interfejsu dostępu do niej, czyli z zestawu operatorów (funkcji) pozwalających nią zarządzać. Dodatkowo istnieją też funkcje, które umożliwiają przeprowadzanie operacji specyficznych dla pewnych rodzajów kolekcji.
Wbudowane kolekcje języka Clojure to:
Ze względu na charakterystykę map, list i wektorów, mogą one służyć również do wyrażania struktur drzewiastych. Umożliwiającą to cechą jest możliwość zagnieżdżania kolekcji w kolekcjach, ponieważ ich elementy nie muszą być jednakowego typu, a więc mogą również być kolekcjami.
Kolekcje mogą być wyposażone w sekwencyjny interfejs dostępu i są wtedy również sekwencjami. Oznacza to, że na kolekcjach można operować, wykorzystując funkcje służące do obsługi sekwencji.
Przedstawione dalej działania na kolekcjach są operacjami związanymi z tymi
konkretnymi rodzajami kolekcji (z małymi wyjątkami, np. dotyczącymi zmiany kolejności
i pozyskiwania fragmentów z użyciem funkcji rseq
, subseq
i rsubseq
czy usuwania
elementów z wektorów). Nie znajdziemy więc niektórych gotowych konstrukcji
realizujących pewne – wydawałoby się podstawowe – czynności, jak np. wydzielanie
fragmentów list. Oznacza to, że tego typu operacje nie są pojedynczymi funkcjami, co
jest najprawdopodobniej efektem specyfiki konkretnej struktury danych, która może nie
być zoptymalizowana pod kątem realizowania niektórych przekształceń. Jeżeli w tym
rozdziale trudno będzie znaleźć opis konkretnej operacji na kolekcji, warto zajrzeć
do kolejnej części poświęconej sekwencjom, ponieważ wszystkie opisywane
tu kolekcje mogą być również traktowane sekwencyjnie.
Listy
Lista (ang. list) to abstrakcyjny typ danych, który pozwala na liniowe porządkowanie elementów. Najprościej do implementowania list wykorzystać strukturę danych zwaną listą połączoną (ang. linked list) – umożliwia ona organizowanie elementów w taki sposób, że dla każdego z nich utrzymywana jest informacja o relacji z elementem następującym po nim (lista jednokierunkowa) lub z elementem następującym po nim oraz poprzedzającym go (lista dwukierunkowa).
Wiemy, że list można używać nie tylko do organizowania kodu programu w Lispach, ale również jako struktury służącej do przechowywania danych użytkowych. Możemy więc korzystać z list jak z kolekcji i przechowywać w nich dowolne dane.
Przeznaczeniem list jest grupowanie elementów z zachowaniem porządku, który zależy od kolejności ich dodawania do listy (jest w stosunku do niej odwrotny). Zoptymalizowane są pod kątem szybkiego przyłączania nowych elementów do ich czół.
Z list będziemy korzystali przede wszystkim do sterowania procesami związanymi ze szczegółami implementacyjnymi programu (np. do tworzenia stosów), rzadziej do przetwarzania dużych zbiorów danych przynależących do logiki aplikacji. Innym zastosowaniem tej struktury danych jest łączenie innych struktur (np. innych list i pojedynczych wartości) w logicznie powiązane łańcuchy.
Zarówno listy przechowujące dane (formy listowe), jak i reprezentujące w pamięci
listowe S-wyrażenia kodu źródłowego (formy wywołania) są reprezentowane strukturami
typu clojure.lang.PersistentList
, dla których utrzymywana jest informacja
o rozmiarze (liczbie elementów). Listy stanowiące elementy drzewa składniowego mogą
być też w pewnych okolicznościach reprezentowane przez tzw. leniwe listy, czyli
sekwencje połączonych komórek typu clojure.lang.Cons
. Dla tego typu
list nie będzie przechowywany licznik rozmiaru i nie zadziałają na nich wszystkie
operacje, które możemy wykonywać na zwykłych listach.
Tworzenie list
Tworzyć listowe struktury danych możemy na trzy sposoby:
wpisując listowe S-wyrażenia – zostaną one odwzorowane w pamięci w kolejności wprowadzania elementów, a pierwszy z nich powinien być formą symbolową identyfikującą podprogram do wykonania (funkcję, makro lub formę specjalną), zaś pozostałe przekazywanymi argumentami – powstają w ten sposób formy wywołania funkcji, makr lub form specjalnych;
korzystając z konstrukcji
list
lub innej, tworzącej formy stałe list (lub po prosu formy listowe) na podstawie przekazanych wartości argumentów, które staną się kolejnymi elementami (w tej samej kolejności, w jakiej je podano, korzystając zlist
) po przeliczeniu każdego do formy stałej;korzystając z literalnego zapisu w formie zacytowanych, listowych S-wyrażeń – powstają wtedy formy listowe, w których dodawane elementy są formami stałymi S-wyrażeń wyszczególnionych na kolejnych pozycjach i nie są poddawane wartościowaniu (tu również pierwotny porządek elementów będzie taki sam, jak porządek wprowadzania).
Pierwszy sposób będzie służył do zapisywania kodu źródłowego programu przeznaczonego do wykonania, a dwa kolejne do tworzenia użytkowych struktur danych.
Listowe S-wyrażenia
Zacznijmy od przypomnienia listy, która reprezentuje kod programu, a powstaje, gdy skorzystamy z listowego S-wyrażenia. Jego pierwszym elementem powinien być operator, wyrażony na przykład symbolem (formą symbolową) odnoszącym się do funkcji, makra lub formy specjalnej, a kolejnymi argumenty, które zostaną przekazane podczas wywoływania podprogramu tej formy.
Użycie:
(operator & operand…)
.
Utworzone w ten sposób listy to formy wywołania, które służą do wyrażania kodu źródłowego, chociaż w niektórych przypadkach można je traktować jak kolekcje i operować na nich (np. gdy zostaną zacytowane lub gdy mamy do czynienia z tzw. makrami). Ich formy (wywołania funkcji, wywołania makra lub specjalne) będą wartościowane do rezultatów wykonania podprogramów.
Wewnętrznie (w drzewie składniowym programu) listy te będą reprezentowane z użyciem
obiektów typu clojure.lang.PersistentList
.
Lista z argumentów, list
Listowe formy można tworzyć z wykorzystaniem funkcji list
. Takie listy są
kolekcjami reprezentowanymi obiektami typu clojure.lang.PersistentList
, czyli
niemutowalnymi, jednokierunkowymi listami, które składają się z połączonych relacją
następczości elementów.
Funkcja list
przyjmuje zero lub więcej argumentów, których wartości staną się
elementami listy. Każdy argument będzie przed umieszczeniem na liście wartościowany,
czyli potraktowany jak forma, którą należy przeliczać, aż stanie się formą stałą
(wyrażającą wartość własną).
Funkcja list
zwraca listę (obiekt typu clojure.lang.PersistentList
).
Użycie:
(list & element…)
.
Warto przypomnieć, że elementy wyrażeń listowych, podobnie jak inne argumenty wyrażane symbolicznie, mogą być oddzielone nie tylko znakami spacji, ale również przecinkami:
Lista z kolekcji, list*
Podobną do funkcji list
jest list*
(ze znakiem asterysku na końcu nazwy).
Symbol gwiazdki w nazwie jest umownym sposobem poinformowania programisty o tym, że mamy do czynienia ze spłaszczonymi argumentami (ang. splatted arguments), tzn. poszczególne elementy są ekstrahowane z kolekcji przekazanej jako ostatni argument zanim zostaną dodane do listy.
Użycie:
(list* kolekcja)
,(list* element… kolekcja)
.
W wersji jednoargumentowej funkcja list*
przyjmuje kolekcję, której elementy
zostaną dodane do listy. W wersji wieloargumentowej kolekcja powinna być podana jako
ostatni argument, opcjonalnie poprzedzony pojedynczymi elementami podanymi jako
wartości wcześniejszych argumentów.
Jeżeli nie chcemy przekazywać kolekcji, możemy zamiast niej przekazać jako ostatni
argument wartość nil
lub pustą kolekcję. Wtedy funkcja list*
zachowa się podobnie
do list
.
Wartością zwracaną jest lista, czyli obiekt typu clojure.lang.PersistentList
.
Zacytowane listowe S-wyrażenia
Przyjrzyjmy się jeszcze mechanizmowi cytowania. Zacytować możemy zarówno listowe S-wyrażenie, jak i listę pustą.
Użycie:
'()
,(quote lista)
,'(element…)
.
Widzimy, że cytowanie listowego S-wyrażenia różni się od użycia formy list
tym, że
nie jest dokonywane obliczanie wartości (wartościowanie) podanych elementów,
które nie są formami stałymi. Zamiast tego elementami stają się reprezentacje
podanych w kodzie źródłowym S-wyrażeń.
Wspomnianą różnicę możemy zaobserwować, gdy podamy niepoprawne formy (np. formy symbolowe, które nie są powiązane z żadnymi wartościami):
Wyrażenie z pierwszej linii generuje błąd, ponieważ w argumentach wywołania pojawia
się symbol c
, który nie jest powiązany z wartością, a więc nie można potraktować go
jak formy symbolowej i uzyskać formy stałej w procesie rozpoznawania nazw
i ewaluacji.
Gdybyśmy chcieli przekazać jako argument symbol w formie stałej, a nie identyfikowany jego nazwą obiekt, moglibyśmy użyć cytowania (wskazującego na literalną postać symbolu) lub odpowiedniej funkcji zwracającej symbol jako stałą wartość:
Zacytowane S-wyrażenia będą w pamięci reprezentowane obiektami typu
clojure.lang.PersistentList
.
Dostęp do list
Za pobieranie elementu listy o podanym numerze kolejnym odpowiadają funkcje first
,
last
, nth
i peek
.
Pierwszy element, first
Funkcja first
pobiera pierwszy element listy. Przyjmuje ona jeden argument, którym
powinna być lista, a zwraca wartość jej pierwszego elementu lub wartość nieustaloną
nil
, jeżeli nie znaleziono pierwszego elementu (mamy do czynienia z listą pustą).
Użycie:
(first lista)
.
Ostatni element, last
Funkcja last
pobiera ostatni element listy. Przyjmuje ona jeden argument, którym
powinna być lista, a zwraca jej ostatni element lub wartość nieustaloną nil
, jeżeli
nie znaleziono ostatniego elementu (mamy do czynienia z listą pustą).
Użycie:
(last lista)
.
Pierwszy element, peek
Funkcja peek
działa w odniesieniu do list podobnie jak first
, ale jest szybsza,
ponieważ nie korzysta z dostępu następczego, lecz z dostępu swobodnego do pierwszego
elementu struktury danych (oznacza to po prostu mniej wewnętrznych wywołań
funkcji). Przyjmuje ona jeden argument, którym powinna być lista, a zwraca wartość
jej pierwszego elementu lub wartość nieustaloną nil
, jeżeli nie znaleziono
pierwszego elementu (mamy do czynienia z listą pustą).
Użycie:
(peek lista)
.
Wybrany element, nth
Funkcja nth
pozwala odczytać element listy o wskazanym numerze kolejnym (licząc od
0). Przyjmuje dwa obowiązkowe argumenty: pierwszym powinna być lista, a drugim
wspomniany numer.
Funkcja zwraca wartość elementu o podanym indeksie lub wartość podaną jako opcjonalny, trzeci argument, jeżeli indeks nie istnieje.
W przypadku podania numeru kolejnego, który nie odpowiada żadnemu elementowi listy
i jeżeli nie użyto trzeciego argumentu, zgłaszany jest wyjątek
IndexOutOfBoundsException
.
Użycie:
(nth lista numer-kolejny zastępnik?)
.
Przeszukiwanie list
Wyszukiwanie wartości wśród elementów listy możemy zrealizować korzystając z metod Javy.
Metody .indexOf
i .lastIndexOf
Wyszukiwanie wartości można realizować z wykorzystaniem wbudowanych metod Javy, które przyjmują dwa argumenty: listę i wartość poszukiwanego elementu. Wartością zwracaną jest numer indeksu (licząc od 0), pod którym element o podanej wartości się znajduje. Metody zwracają wartość -1, gdy poszukiwany element nie może być odnaleziony.
Użycie:
(.indexOf lista element)
,(.lastIndexOf lista element)
.
Uwaga: Listy nie służą do składowania wartości, które często mają być wyszukiwane. W powyższym przykładzie korzystamy z metod obiektu Javy, na bazie którego konstruowana jest jednokierunkowa lista, jednak ze względów wydajnościowych powinno unikać się takiego zastosowania.
Dodawanie elementów do listy
Dodawanie komórek, cons
Aby dodać do listy nowy element na jej czele, możemy skorzystać z funkcji
cons
. Przyjmuje ona dwa argumenty: dodawany element i obiekt listy.
Zwracana struktura nie jest stałą listą, ale komórką cons (obiektem typu
clojure.lang.Cons
), który tworzy tzw. leniwą listę, zawierającą dwie istotne
informacje: dodawany element i odwołanie do kolejnego elementu lub
struktury. Problematycznym może być, że takiego obiektu nie możemy już przetwarzać
z użyciem wszystkich funkcji, które służą do obsługi list bazujących na obiekcie
IPersistentList
.
Użycie:
(cons element lista)
.
Dodawanie elementów, conj
Preferowanym sposobem dodawania elementu do listy jest użycie funkcji conj
(z ang. conjoin). Wykorzystuje ona polimorficzne wywołania odpowiednich metod Javy
w zależności od rodzaju kolekcji. W ten sposób dołączany jest element, a na zwracanej
strukturze możemy przeprowadzać te same operacje, co na pierwotnej. W przypadku list
będzie to obiekt typu clojure.lang.IPersistentList
.
Funkcja conj
przyjmuje dwa obowiązkowe argumenty. Pierwszym może być lista,
a drugim dodawany element. Zwracaną wartością jest nowa lista z dodanym na jej czele
elementem przekazanym jako drugi argument.
Opcjonalnie możemy przekazać do wywołania conj
więcej argumentów, których wartości
zostaną dodane do wynikowej struktury. Pierwszy podawany jako argument element będzie
umieszczony na czele listy jako pierwszy, drugi w drugiej kolejności itd. Oznacza to,
że ostatni podawany argument stanie się pierwszym elementem zwracanej listy
(odwrócona kolejność).
Użycie:
(conj lista element & element…)
.
Usuwanie elementów z listy
Usuwanie elementów z list możliwe jest z wykorzystaniem funkcji pop
i rest
.
Ponieważ mamy do czynienia z danymi niemutowalnymi, więc oczywiście rezultatami będą
nowe listy, które od oryginalnych różnią się jednym elementem, a nie ze strukturami
zmodyfikowanymi.
Bez pierwszego elementu, pop
Listę bez pierwszego elementu uzyskamy przez wywołanie funkcji pop
. Przyjmuje
ona jeden argument, którym powinna być lista, a zwraca obiekt typu PersistentList
(nową listę).
Użycie:
(pop lista)
.
Uwaga: Próba użycia pop
na liście pustej spowoduje zgłoszenie wyjątku
IllegalStateException
.
Poza pierwszym elementem, rest
Listę złożoną ze wszystkich elementów poza pierwszym uzyskamy z użyciem funkcji
rest
, która dla podanej jako argument listy zwraca tzw. resztę.
Zwracanym obiektem jest lista (obiekt typu PersistentList
).
Użycie:
(rest lista)
.
Cytowanie i wartościowanie list
Listowe S-wyrażenia mogą być zacytowane z użyciem quote
lub lukru składniowego
w postaci pojedynczego apostrofu. Są wtedy formami stałymi, a nie formami wywołania:
Taką listę również można skonstruować z użyciem funkcji list
:
Zacytowany został tylko symbol plusa, bo w przeciwnym razie byłby on wartościowany do
obiektu funkcyjnego identyfikowanego symbolem +
(operacja sumowania). Literały
liczbowe zostawiliśmy, ponieważ już są formami stałymi (wyrażającymi własne
wartości).
Na liście możemy dokonywać operacji i przekształcać ją, korzystając z różnorakich form, aby potem znów zmienić formę listową na złożoną (poddawaną wyliczaniu):
To samo można oczywiście zapisać w jednej linii, a następnie sprawdzić, jaka wartość będzie zwrócona:
Widzimy, że na zawartości list można operować funkcjami, jednak nic nie stoi na
przeszkodzie, aby potem znów potraktować je jak wyrażenia przeznaczone do
obliczenia. W tym celu musimy użyć konstrukcji, która zmieni formę stałą naszej listy
w formę wywołania. Będzie ona przez ewaluator traktowana jako kod do wykonania
i wartościowana. Możemy to osiągnąć posługując się konstrukcją eval
:
W wyniku otrzymaliśmy formę stałą prezentowaną przez REPL jako atomowe S-wyrażenie (literał liczbowy będący atomem), które wyraża wartość liczbową 7. W skrócie: otrzymaliśmy wartość 7.
Widzimy więc, jak łatwo zapisać kod jako dane, a następnie dane przetworzyć tak, aby stały się kodem. W Lispie granica między nimi jest płynna, a wraz z mechanizmem tworzenia makr pozwala budować metajęzyki i języki dziedzinowe przeznaczone do konkretnych zastosowań, jak też tworzyć programy, które są szczęśliwymi programami.
Programs that write programs are the happiest programs in the world.
— Andrew Hume
Wektory
Wektor (ang. vector) to liniowa struktura danych, która przypomina jednowymiarową tablicę, lecz różni się od niej tym, że możemy dynamicznie dodawać i usuwać elementy. Wektory służą do przechowywania danych w sposób uporządkowany, opatrując elementy numerami indeksów. Dostęp do takich danych jest swobodny, tzn. nie trzeba przeszukiwać całego wektora, aby odnieść się do elementu o znanej pozycji – można po prostu zażądać odczytu wartości umieszczonej pod danym indeksem.
Wektory w Clojure są strukturami danych zoptymalizowanymi pod kątem dodawania elementów do ich końca.
Wektory możemy budować korzystając z odpowiednich funkcji bądź wyrażać literalnie, używając wektorowych S-wyrażeń. W przeciwieństwie do symbolicznych wyrażeń listowych nie będą one tworzyły form wywołania, lecz formy wektorowe (do przechowywania danych) bądź powiązaniowe (do wytwarzania powiązań symboli z wartościami). Podane elementy form wektorowych zapisanych jako wektorowe S-wyrażenia będą przeliczane przed utworzeniem struktury danych, aż każdy z nich będzie formą stałą.
Jeżeli chodzi o przetwarzanie danych związanych z logiką aplikacji, wektory nadają się do tego celu bardziej niż listy. W programach będzie to więc struktura używana do zarządzania uporządkowanymi zbiorami informacji, w których istotny jest szybki dostęp do elementów o znanych pozycjach. Dzięki wewnętrznemu zastosowaniu zagnieżdżonych drzew bazujących na funkcjach mieszających optymistyczna czasowa złożoność obliczeniowa dla operacji odczytu, aktualizacji, pobierania fragmentów i dodawania elementów do wektorów jest bliska stałej – O(1); chociaż złożoność oczekiwana będzie logarytmiczna – Θ(log n).
Tworzenie wektorów
Wektory można tworzyć z użyciem wektorowych S-wyrażeń, funkcji vector
, vec
lub
vector-of
.
Wektor z argumentów, vector
Funkcja vector
przyjmuje zero lub więcej argumentów, których wartości (po
przeliczeniu do form stałych) staną się elementami wektora.
Wartością zwracaną jest wektor (obiekt typu clojure.lang.PersistentVector
).
Użycie:
(vector & element…)
.
Wektor typowy, vector-of
W przypadku tworzenia wektorów, których elementami będą dane określonego typu, możemy
skorzystać z funkcji vector-of
. Przyjmuje ona jeden obowiązkowy argument, którego
wartość określa typ danych umieszczanych w wektorze i zero lub więcej dodatkowych
argumentów, których wartości staną się elementami wektora.
Zwracaną wartością jest wektor (obiekt typu clojure.lang.PersistentVector
).
Typ danych możemy wyrazić słowem kluczowym, które powinno należeć
do z góry określonego zbioru: :int
, :long
, :float
, :double
, :byte
,
:short
, :char
, :boolean
.
Stworzony z użyciem vector-of
wektor będzie składał się z obiektów konkretnego
typu podstawowego (ang. primitive type), a nie typu
generycznego, z którego podczas każdej operacji dostępu musiałby być
rozpakowywany (ang. unboxed) właściwy obiekt. Ma to pozytywny wpływ na prędkość
dostępu do elementów kolekcji.
Użycie:
(vector-of typ & element…)
.
Wektor z sekwencji, vec
Funkcja vec
działa w odniesieniu do wektorów podobnie, jak funkcja list*
w odniesieniu do list. Przyjmuje dowolną kolekcję i buduje zawartość na bazie jej
kolejnych elementów.
Zwracaną wartością jest wektor (obiekt typu clojure.lang.PersistentVector
).
Użycie:
(vec napis)
,(vec sekwencja)
.
Wektorowe S-wyrażenia
Wektorowe S-wyrażenie (ang. vector S-expression) to element składniowy pozwalający w przejrzysty sposób wyrażać wektory bądź powiązania symboli z wartościami. Składa się z zestawu elementów ujętych w nawiasy kwadratowe lub z samych nawiasów kwadratowych (w przypadku wektorów pustych).
Wektorowe S-wyrażenia mogą przyjmować następujące formy:
- wektorową (służącą do przechowywania danych);
- powiązaniowe (służące do tworzenia powiązań symboli z wartościami):
- wektor parametryczny funkcji,
- wektor powiązań zmiennych dynamicznych lub leksykalnych.
Formy wektorowe
Jeżeli podane elementy wektora literalnego (tworzącego formę wektorową) są formami innymi niż stałe, będą wartościowane, a w odpowiednich miejscach zostaną umieszczone rezultaty przeprowadzanych obliczeń.
Użycie:
[]
,[element…]
.
Formy powiązaniowe
Wektorowe formy powiązaniowe posłużą nam do tworzenia powiązań symboli z wartościami z użyciem różnych form specjalnych oraz makr. Cechowały je będą pary wyrażeń składające się z symboli i towarzyszących im wyrażeń inicjujących odpowiedzialnych za ustawianie wartości początkowych. Wyjątkiem od tej reguły będą wektory parametryczne funkcji, w których podawane są same formy powiązaniowe symboli, ponieważ początkowe wartości będą pochodziły z argumentów przekazywanych podczas wywoływania.
Użycie:
[]
,[symbol…]
,[para-powiązaniowa…]
,
gdzie para-powiązaniowa
to:
symbol wyrażenie-inicjujące
.
Zacytowane wektorowe S-wyrażenia
Wektorowe S-wyrażenia mogą być również zacytowane. W takim przypadku podawane elementy nie będą wartościowane, nawet jeżeli nie są formami stałymi (niewyrażającymi wartości własnych). Zamiast tego rezultat ewaluacji każdego z wprowadzonych jako kolejne elementy S-wyrażeń znajdzie się w wynikowej strukturze. Tego rodzaju wyrażenia mogą być używane do tworzenia form wektorowych.
Użycie:
'[]
,'[element…]
,(quote [])
,(quote [element…])
.
Dostęp do wektorów
Pobieranie elementu wektora o określonej lokalizacji (o określonym numerze kolejnym
indeksu) możliwe jest z użyciem get
, nth
, first
, last
, peek
, albo przez
odwołanie się do obiektu wektora jak do funkcji.
Pierwszy element, first
Funkcja first
pobiera pierwszy element wektora. Przyjmuje ona jeden argument,
którym powinien być wektor, a zwraca jego pierwszy element lub wartość nil
, jeżeli
nie znaleziono pierwszego elementu (mamy do czynienia z wektorem pustym).
Użycie:
(first wektor)
.
Ostatni element, last
Funkcja last
pobiera ostatni element wektora. Przyjmuje ona jeden argument, którym
powinien być wektor, a zwraca jego ostatni element lub wartość nil
, jeżeli nie
znaleziono ostatniego elementu (mamy do czynienia z wektorem pustym).
Użycie:
(last wektor)
.
Ostatni element, peek
Funkcja peek
działa w odniesieniu do wektorów podobnie jak last
, ale jest
szybsza, ponieważ nie korzysta z dostępu następczego, lecz z dostępu swobodnego.
Przyjmuje ona jeden argument, którym powinien być wektor, a zwraca jego ostatni
element lub wartość nieustaloną nil
, jeżeli nie znaleziono ostatniego elementu
(mamy do czynienia z wektorem pustym).
Zauważmy, że działanie peek
na wektorach różni się od działania tej funkcji na
listach (tam zwraca ona pierwszy element).
Użycie:
(peek wektor)
.
Wybrany element, get
Sprawdzenia czy element o podanym indeksie istnieje w wektorze i pobrania jego
wartości można dokonać z wykorzystaniem funkcji get
.
Użycie:
(get wektor indeks wartość-domyślna?)
.
Funkcja przyjmuje dwa obowiązkowe argumenty: pierwszym powinien być wektor, a drugim
numer kolejny poszukiwanego elementu (licząc od 0). Jeżeli element o podanym numerze
indeksu znajduje się w wektorze, jego wartość zostanie zwrócona – w przeciwnym razie
zwrócona będzie wartość nieustalona nil
lub wartość przekazana jako opcjonalny,
trzeci argument.
Wybrany element, nth
Funkcja nth
pozwala odczytać element wektora o wskazanym numerze kolejnym (licząc
od 0). Przyjmuje dwa obowiązkowe argumenty: pierwszym powinien być wektor, a drugim
wspomniany numer.
Funkcja zwraca wartość elementu o podanym numerze indeksu lub wartość podaną jako opcjonalny, trzeci argument, jeżeli indeks nie istnieje.
W przypadku podania numeru kolejnego, który nie odpowiada żadnemu elementowi wektora
i jeżeli nie użyto trzeciego argumentu, zgłaszany jest wyjątek
IndexOutOfBoundsException
.
Użycie:
(nth wektor numer-kolejny zastępnik?)
.
Dzielenie wektorów
Istnieją funkcje, dzięki którym możemy dokonywać podziału wektorów na mniejsze wektory.
Wydzielanie zakresu, subvec
Funkcja subvec
tworzy wektor z elementów istniejącego wektora. Jej pierwszym
argumentem powinien być wektor źródłowy, a kolejnym początkowy numer indeksu, od
którego zacznie się wydzielanie wektora pochodnego (licząc od 0).
Opcjonalnie można podać trzeci argument, który będzie oznaczał końcowy numer indeksu (nie wchodzący w skład wektora wynikowego).
Funkcja subvec
zwraca wektor.
Jeżeli podano numery indeksów dla nieistniejących elementów lub zakres jest
niewłaściwie określony, zgłoszony zostanie wyjątek IndexOutOfBoundsException
.
Użycie:
(subvec wektor początek koniec?)
.
Wybieranie elementów, replace
Tworzenie wektora pochodnego z elementów o podanych numerach indeksów jest możliwe
z użyciem funkcji replace
. Przyjmuje ona dwa argumenty: wektor źródłowy i wektor
zawierający numery indeksów wybranych elementów. Zwracaną wartością jest wektor
składający się z elementów wybranych ze źródłowego wektora.
Jeżeli w wektorze z numerami indeksów znajdą się numery indeksów, pod którymi nie znajdziemy żadnych elementów, albo inne dane, wartości te zostaną umieszczone w wynikowym wektorze.
Użycie:
(replace wektor wektor-indeksów)
.
Przeszukiwanie wektorów
Przeszukiwanie indeksów wektorów możliwe jest z użyciem generycznej funkcji
contains?
lub wykorzystaniu wektorowej formy przeszukiwania. Z kolei wyszukiwanie
wartości możemy zrealizować z wykorzystaniem metod Javy.
Wektor jako funkcja
Obiekty wektorów wyposażone są w funkcyjny interfejs, tzn. są również specyficznymi
funkcjami. Listowe S-wyrażenie, którego pierwszym elementem jest wektor, będzie
potraktowane jak forma przeszukiwania wektora (ang. vector lookup
form). Przyjmuje ona jeden obowiązkowy argument, który oznacza element identyfikowany
numerem indeksu, a zwraca jego wartość (podobnie jak w przypadku get
). Jeżeli
element o podanym kluczu nie istnieje w wektorze, zgłaszany jest wyjątek
java.lang.IndexOutofboundsexception
.
Użycie:
(wektor klucz)
.
Wyszukiwanie indeksu, contains?
Sprawdzić czy element o podanym numerze kolejnym istnieje w wektorze możemy z użyciem
generycznej funkcji contains?
. Przyjmuje ona dwa argumenty: pierwszym powinien być
wektor, a drugim numer kolejny poszukiwanego elementu (licząc od 0). Funkcja zwraca
true
, jeżeli element o podanym numerze indeksu istnieje, a false
w przeciwnym
razie.
Użycie:
(contains? wektor indeks)
.
Metody .indexOf
i .lastIndexOf
Wyszukiwanie wartości można realizować z wykorzystaniem wbudowanych metod Javy, które pobierają dwa argumenty: wektor i wartość elementu, której poszukujemy. Wartością zwracaną jest numer indeksu (licząc od 0), pod którym dany element się znajduje.
Użycie:
(.indexOf wektor element)
,(.lastIndexOf wektor element)
.
Uwaga: Wektory nie służą do składowania wartości, które często mają być wyszukiwane przez porównywanie z innymi. W powyższym przykładzie korzystamy z metod obiektu Javy, jednak ze względów wydajnościowych powinno unikać się takiego zastosowania wektorów.
Łączenie wektorów
Do łączenia zawartości wektorów służy funkcja into
.
Funkcja into
Funkcja into
przyjmuje dwa argumenty, którymi mogą być wektory, a zwraca wektor
będący ich złączeniem.
Użycie:
(into wektor-docelowy wektor-źródłowy)
.
Aktualizowanie wektorów
Podmiana wartości, assoc
Tworzenie wektora pochodnego z podmienioną wartością wybranego elementu
zrealizujemy z użyciem funkcji assoc
. Przyjmuje ona trzy argumenty: wektor, numer
indeksu i nową wartość dla elementu o podanym numerze kolejnym (licząc od 0).
Funkcja assoc
zwraca wektor.
Użycie:
(assoc wektor indeks wartość)
.
Podmiana zagnieżdżonej, assoc-in
Tworzenie wektora pochodnego z podmienioną wartością wybranego elementu
zagnieżdżonej struktury zrealizujemy z użyciem funkcji assoc-in
. Przyjmuje ona
trzy argumenty: wektor, ścieżkę z numerami indeksów (lub identyfikatorami innych
struktur pośrednich) i nową wartość dla elementu o podanym numerze kolejnym (licząc
od 0) w ostatniej specyfikowanej indeksami ścieżki strukturze.
Funkcja assoc-in
zwraca wektor.
Użycie:
(assoc-in wektor ścieżka wartość)
.
Przeliczenie wartości, update
Przeliczenie wartości elementu struktury wskazanego numerem indeksu umożliwia funkcja
update
. Jako pierwszy argument przyjmuje ona wektor, kolejnym powinien być numer
indeksu zmienianego elementu (licząc od 0), a ostatnim funkcja, która przyjmuje jeden
argument i zwraca wartość, która zostanie użyta do aktualizacji wartości elementu.
Funkcja update
zwraca nowy wektor.
Użycie:
(update wektor klucz operator)
.
Przeliczanie zagnieżdżonej, update-in
Zmiana wartości zagnieżdżonego elementu na bazie poprzedniej wartości możliwa
jest dzięki funkcji update-in
. Przyjmuje ona trzy argumenty: wektor, wektor
indeksów i funkcję przekształcającą. Dla każdego elementu, którego numer kolejny
znajdzie się w wektorze indeksów, będzie wywołana przekazana funkcja. Powinna ona
przyjmować jeden argument, którym będzie aktualna wartość przetwarzanego elementu,
a zwracać wartość, która zastąpi zastaną.
Wartością zwracaną przez funkcję update-in
jest wektor.
(update-in wektor wektor-indeksów funkcja)
.
Warto zwrócić uwagę na użycie funkcji constantly
w powyższym
przykładzie. Jest to jedna z tzw. funkcji wyższego rzędu, ponieważ zwracaną przez nią
wartością jest inna funkcja. Zadaniem tej ostatniej w przypadku constantly
jest
zwracanie stałej wartości niezależnie od wartości przekazanego argumentu. Używamy
jej, ponieważ update-in
wymaga, aby trzecim argumentem była funkcja, a nie wartość,
a chcemy uzyskać konkretną wartość, niezależną od poprzedniej. Potrzebujemy więc
funkcji, która będzie ją zwracać.
Dodawanie elementów do wektorów
Dodawanie na końcu, conj
Tworzenie wektora pochodnego z dodanym elementem na końcu możliwe jest dzięki
generycznej funkcji conj
operującej na kolekcjach. Przyjmuje ona dwa obowiązkowe
argumenty: wektor i dodawany element, a zwraca nowy wektor.
Opcjonalnie możemy podać jako kolejne argumenty więcej elementów – zostaną one dodane do wynikowego wektora w kolejności ich przekazywania.
Użycie:
(conj wektor element & element…)
.
Dołączanie do końca, into
Dobrym sposobem dodawania jednego lub więcej elementów do wektora jest skorzystanie
z funkcji into
, ponieważ czasowa złożoność obliczeniowa tej operacji jest liniowa –
O(n) (gdzie n jest liczbą dodawanych elementów). Przyjmuje ona dwa argumenty:
wektor docelowy i wektor źródłowy, a zwraca nowy wektor, którego zawartość jest
złączeniem podanych.
Użycie:
(into wektor-docelowy wektor-źródłowy)
.
Dodawanie wartości, assoc
Tworzenie wektora pochodnego z dodanym elementem zrealizujemy również z użyciem
funkcji assoc
. Przyjmuje ona trzy argumenty: wektor, numer indeksu i wartość dla
elementu o podanym numerze kolejnym (licząc od 0). Musi to być numer o jeden większy,
niż numer ostatniego elementu. Podanie błędnego numeru indeksu spowoduje zgłoszenie
wyjątku IndexOutOfBoundsException
.
Funkcja assoc
zwraca wektor.
Użycie:
(assoc wektor indeks wartość)
.
Dodawanie zagnieżdżonej, assoc-in
Tworzenie wektora pochodnego z dodanym elementem pochodzącym z zagnieżdżonej
struktury zrealizujemy z użyciem funkcji assoc-in
. Przyjmuje ona trzy argumenty:
wektor, ścieżkę z numerami indeksów (lub kluczami innych struktur pośrednich) dla
kolejno napotykanych struktur i wartość elementu o podanym numerze kolejnym (licząc
od 0), która zostanie umieszczona w ostatniej strukturze określonej indeksami
ścieżki. Ponieważ chcemy dodawać elementy, a nie je zastępować, musi to być numer
o jeden większy, niż numer ostatniego elementu specyfikowanej struktury. Podanie
błędnego numeru indeksu spowoduje zgłoszenie wyjątku IndexOutOfBoundsException
.
Funkcja assoc-in
zwraca wektor.
Użycie:
(assoc-in wektor ścieżka wartość)
.
Usuwanie elementów z wektorów
Usuwanie elementów wektorów możliwe jest z wykorzystaniem funkcji pop
, rest
,
a także zastosowania subvec
wraz z funkcją łączącą powstałe wektory.
Bez pierwszego elementu, pop
Wektor bez pierwszego elementu uzyskamy przez wywołanie funkcji pop
. Przyjmuje
ona jeden argument, którym powinien być wektor, a zwraca obiekt typu
clojure.lang.PersistentVector
(nowy wektor).
Użycie:
(pop wektor)
.
Uwaga: Próba użycia pop
na wektorze pustym spowoduje wygenerowanie wyjątku
IllegalStateException
.
Poza pierwszym elementem, rest
Sekwencję elementów wektora poza pierwszym uzyskamy z wykorzystaniem funkcji
rest
, która dla podanego jako argument wektora zwraca tzw. resztę.
Wartością zwracaną jest sekwencja utworzona na bazie wektora.
Jeżeli potrzebujemy rezultatu w postaci wektora, możemy przekształcić wynik
z użyciem funkcji vec
, albo zamiast z rest
skorzystać
z pop
.
Użycie:
(rest wektor)
.
Usuwanie wybranego elementu
Nowy wektor bez wybranego elementu możemy stworzyć z wykorzystaniem kombinacji
funkcji concat
i subvec
lub into
i subvec
.
Użycie:
(concat & wektor…)
,(subvec wektor początek koniec?)
.
W rezultacie otrzymamy sekwencję (rezultat wywołania funkcji
concat
), która nie wytworzy dodatkowych struktur pamięciowych, lecz
będzie przechowywała odwołania do dwóch wektorów wydzielonych z bazowego. Operacja
dzielenia wektorów ma stałą czasową złożoność obliczeniową – O(1).
Gdybyśmy jako rezultatu potrzebowali stałej struktury wektora zamiast sekwencji,
możemy dokonać konwersji z użyciem funkcji vec
.
Innym sposobem usuwania elementu jest skorzystanie z into
. Funkcja ta jest
tzw. konstrukcją przejściową (ang. transient), co oznacza, że w celu szybszej
generacji wyniku wewnętrznie korzysta z danych mutowalnych, chociaż zwracany rezultat
jest, zgodnie z konwencją, wartością niezmienną.
Korzystając z biblioteki Criterium, porównajmy prędkości usuwania pojedynczego elementu dla przedstawionych strategii:
Rezultaty:
- użycie
concat
: 335 ns; - użycie
concat
ivec
: 133 µs; - użycie
into
: 25 µs.
Wniosek:
Jeżeli nie potrzebujemy wektora, lecz zależy nam na samych wartościach elementów,
możemy oszczędzić pamięć i czas procesora, wybierając użycie concat
do łączenia
wektorów.
Jeżeli zależy nam na tym, aby wynikową strukturą był również wektor, warto skorzystać
z into
, która jest kilkukrotnie szybsza niż wywoływanie vec
.
Przekształcanie wektorów
Odwzorowywanie, mapv
Funkcja mapv
jako pierwszy argument przyjmuje funkcję, która będzie użyta
w odniesieniu do każdego kolejnego elementu podanych wektorów. Rezultaty zostaną
umieszczone jako elementy zwracanego wektora. Operator (przekazywana funkcja) musi
przyjmować tyle argumentów, ile wektorów zostało podanych. Wtedy będzie wywoływana
dla zbioru wartości stanowiących kolejne elementy każdego z wektorów.
Użycie:
(mapv operator wektor & wektor…)
.
Można oczywiście jako operatora użyć funkcji jednoargumentowej i jednego zestawu danych wejściowych, jeżeli istnieje taka potrzeba:
Filtrowanie, filterv
Funkcja filterv
jako pierwszy argument przyjmuje funkcję warunkującą, a jako drugi
wektor. Przekazana funkcja, zwana predykatem, jest wywoływana dla wartości każdego
kolejnego elementu przekazanego wektora. Jeżeli zwracana przez nią wartość będzie
różna od false
i różna od nil
, dany element zostanie umieszczony w zwracanym
wektorze wyjściowym.
Użycie:
(filterv predykat wektor)
.
Redukowanie z indeksem, reduce-kv
Funkcja reduce-kv
użyta w odniesieniu do wektorów działa podobnie do
reduce
, z tą różnicą, że wywołuje podany jako pierwszy argument operator
dla każdego elementu i jego numeru indeksu (poza akumulatorem i wartością
aktualnie przetwarzanego elementu). Funkcja przyjmuje też wartość początkową
akumulatora jako argument na drugiej pozycji.
Funkcja zwraca rezultat ostatniego wywołania operatora na wartości akumulatora, ostatnio przetwarzanym elemencie wektora i jego numerze kolejnym (licząc od 0).
Użycie:
(reduce-kv operator akumulator wektor)
.
Możemy sami sprawdzić jak wyglądają kolejne wywołania, tworząc sumator, który poza wyliczeniem wyświetli przyjmowane argumenty:
W efekcie otrzymamy:
(+ 0 0 1)
(+ 1 1 2)
(+ 4 2 3)
(+ 9 3 4)
16
Pierwsza kolumna to symbol operacji, druga to zakumulowany rezultat poprzedniego działania, trzecia bieżący numer indeksu wektora, a ostatnia to wartość elementu obecnego pod podanym numerem indeksu.
Funkcja reduce-kv
zalicza się do funkcji operujących na sekwencjach,
czyli abstrakcyjnych strukturach reprezentujących kolekcje. Została umieszczona w tym
miejscu, ponieważ jest specjalnym wariantem reduce
, który operuje na
wektorach (i innych kolekcjach indeksowanych numerycznie).
Więcej szczegółów dotyczących redukowania (zwijania) sekwencji wartości można znaleźć w części poświęconej transduktorom.
Odwracanie kolejności, rseq
Na podstawie wektorów można tworzyć sekwencje o odwróconej kolejności
elementów. Służy do tego funkcja rseq
. W stałym czasie zwraca ona sekwencję na
bazie podanego wektora. Przyjmuje jeden argument, którym powinien być wektor,
a zwraca leniwą sekwencję o kolejności elementów odwróconej względem kolejności
w wektorze.
Gdybyśmy chcieli na bazie sekwencji znów uzyskać stałą strukturę pamięciową, możemy
stworzyć wektor, korzystając z funkcji vec
lub wprowadzić elementy do
istniejącego wektora (funkcja into
).
Użycie:
(rseq wektor)
.
Zobacz także:
- „Funkcja
rseq
„ w rozdziale X.
Mapy
Mapa (ang. map) to asocjacyjna kolekcja, która pozwala wyrażać przyporządkowania wartości (ang. values) do kluczy (ang. keys). Kluczami mapy mogą być dowolne obiekty, choć przy naprawdę dużych strukturach zaleca się korzystanie, jeżeli to możliwe, ze słów kluczowych lub wypakowanych (ang. unboxed) typów numerycznych.
Mapy przystosowane są do szybkiego odnajdywania wartości na podstawie kluczy i swobodnego dodawania nowych elementów.
Do oddzielania poszczególnych par klucz–wartość, a także do oddzielania samych kluczy od wartości, w zapisie symbolicznym korzysta się z przecinka i/lub ze znaku spacji:
Kluczami mogą być dane dowolnych typów, nie tylko słowa kluczowe.
Wartościami map mogą być też inne mapy. Dzięki temu możemy tworzyć zagnieżdżone, drzewiaste struktury.
Rodzaje map
Niektóre mapy zachowują porządek wprowadzania elementów, inne nie. Możemy wyróżnić następujące rodzaje map:
mapy zwykłe, zwane też mapami haszowanymi czy mapami bazującymi na funkcji mieszającej;
mapy sortowane (ang. sorted maps) – kryterium sortowania są klucze;
mapy tablicowe (ang. array maps) – zachowujące porządek dodawanych elementów;
mapy strukturalizowane (ang. struct maps) – ze z góry określonym zbiorem kluczy;
mapy właściwości JavaBean (ang. JavaBean maps) – reprezentują cechy obiektów Javy.
Uwaga: Mapy tablicowe pozwalają na dostęp z użyciem funkcji operujących na mapach, jednak wewnętrznie są tablicami. Prędkości przeszukiwania i tworzenia kolekcji pochodnych są w nich znacznie mniejsze, niż w przypadku map bazujących na funkcjach mieszających. Dla tych ostatnich złożoność obliczeniowa dostępu do elementu wskazywanego kluczem jest stała – O(1). Z kolei dla map tablicowych liniowa – O(n). W praktyce warto korzystać z map tablicowych wtedy, gdy kolekcja ma mniej niż 9 par.
Wewnętrznie mapy to struktura, której elementy zlokalizowane są w miejscach będących rezultatem obliczenia wartości funkcji mieszającej, która stosuje tzw. transformację kluczową. Dla każdego podanego klucza obliczana jest lokalizacja elementu w strukturze, a następnie dokonywany jest skok do tej lokalizacji. Cała mapa zajmuje więcej miejsca (są tam niewykorzystane przestrzenie, zarezerwowane dla wartości, które mogłyby się pojawić, gdyby zostały użyte), jednak prędkość przeszukiwania i dodawania nowych elementów jest bardzo duża – nie zależy liniowo od ich liczby.
Tworzenie map
Tworzyć mapowe formy możemy z użyciem jednej z służących do tego funkcji lub symbolicznych, mapowych wyrażeń.
Mapa haszowana, hash-map
Funkcja hash-map
przyjmuje zero lub więcej argumentów, których całkowita liczba
powinna być parzysta. Każda wartość przekazanego, nieparzystego argumentu będzie
kluczem, a następującego po nim skojarzoną z nim w mapie wartością.
Funkcja zwraca mapę (obiekt typu clojure.lang.PersistentHashMap
), a dla map
pustych mapę tablicową (obiekt typu clojure.lang.PersistentArrayMap
), która
zachowuje kolejność wprowadzanych elementów.
Użycie:
(hash-map & klucz–wartość…)
,
gdzie klucz–wartość
to:
klucz wartość
.
Mapa sortowana, sorted-map
Funkcja sorted-map
przyjmuje zero lub więcej argumentów, których liczba powinna być
parzysta. Każda wartość przekazanego, nieparzystego argumentu będzie kluczem,
a następującego po nim skojarzoną z nim w mapie wartością.
Funkcja zwraca mapę sortowaną (obiekt typu clojure.lang.PersistentTreeMap
),
która zachowuje porządek sortowania wprowadzanych elementów, a kryterium sortowania
jest porównywanie wartości kluczy.
Użycie:
(sorted-map & klucz–wartość…)
,
gdzie klucz–wartość
to:
klucz wartość
.
Mapa sortowana, sorted-map-by
Funkcja sorted-map-by
jeden obowiązkowy argument, którym powinna być dwuargumentowa
funkcja porównująca (tzw. komparator), która zwraca wartość -1 (lub mniejszą), 0
lub 1 (lub większą), w zależności od tego czy pozycja wartości pochodzącej
z pierwszego argumentu powinna być mniejsza, równa czy większa od pozycji wartości
pochodzącej z argumentu drugiego.
Do funkcji sorted-map-by
można też przekazać opcjonalne argumenty, których liczba
powinna być parzysta. Każda wartość przekazanego, nieparzystego argumentu będzie
kluczem, a następującego po nim skojarzoną z nim w mapie wartością.
Funkcja zwraca mapę sortowaną (obiekt typu clojure.lang.PersistentTreeMap
),
która zachowuje porządek sortowania wprowadzanych elementów, a kryterium sortowania
jest określone przekazanym komparatorem.
Użycie:
(sorted-map-by komparator & klucz–wartość…)
,
gdzie klucz–wartość
to:
klucz wartość
.
Uwaga: Niektóre konsole REPL mogą niepoprawnie wyświetlać mapy sortowane (gubiąc
porządek z powodu konwersji do mapy innego rodzaju), więc rezultaty umieszczone
w przykładzie mogą się różnić od uzyskanych. Receptą może być np. konwersja wyniku do
wektora z użyciem vec
.
Mapa tablicowa, array-map
Funkcja array-map
przyjmuje zero lub więcej argumentów, których liczba powinna być
parzysta. Każda wartość przekazanego, nieparzystego argumentu będzie kluczem,
a następującego po nim skojarzoną z nim w mapie wartością.
Funkcja zwraca mapę tablicową (obiekt typu clojure.lang.PersistentArrayMap
),
która zachowuje kolejność wprowadzanych elementów.
Uwaga: Mapy tablicowe korzystają wewnętrznie z tablic i nie powinno używać się ich do obsługi większych zbiorów danych (liczących 8 i więcej par), chociaż dobrze sprawdzają się jako konstrukcje wyrażające konfigurację programu czy służące do sterowania jego wykonywaniem (dane implementacyjne).
Użycie:
(array-map & klucz–wartość…)
.
Mapa z wektorów, zipmap
Funkcja zipmap
pozwala tworzyć mapę na podstawie dwóch wektorów przekazanych jako
dwa obowiązkowe argumenty: pierwszy powinien zawierać klucze, a drugi wartości, które
mają być skojarzone z kluczami. Czynnikiem logicznie łączącym elementy pochodzące
z wektorów jest ich pozycja.
Wartością zwracaną przez funkcję zipmap
jest mapa (obiekt typu PersistentHashMap
)
lub mapa tablicowa (obiekt typu PersistentArrayMap
), jeżeli par klucz–wartość jest
mniej niż 9.
Użycie:
(zipmap klucze wartości)
.
Mapa z wektora, group-by
Dzięki funkcji group-by
możliwe jest tworzenie map, które zawierają pary
klucz–wektor. Funkcja ta przyjmuje dwa obowiązkowe argumenty: predykat (funkcję
używaną do grupowania) i kolekcję danych wejściowych wyrażoną wektorem.
Wartości zwracane przez przekazaną funkcję będą użyte jako klucze, natomiast elementy zostaną dodane do wektorów przypisanych do tych kluczy. Dzięki temu możliwe jest dzielenie i grupowanie kolekcji danych względem pewnych ich cech.
Wartością zwracaną przez funkcję group-by
jest mapa (obiekt typu
PersistentHashMap
) lub mapa tablicowa (obiekt typu PersistentArrayMap
), jeżeli
par klucz–wartość jest mniej niż 9.
Użycie:
(group-by predykat wektor)
.
Mapa częstości, frequencies
Funkcja frequencies
przyjmuje jeden argument, którym powinien być wektor, a zwraca
mapę zawierającą elementy wektora jako klucze i przypisane do nich częstości
wystąpień tych kluczy w wektorze.
Typem wartości zwracanej przez funkcję frequencies
mapa (obiekt typu
PersistentHashMap
) lub mapa tablicowa (obiekt typu PersistentArrayMap
), jeżeli
wynikowych par klucz–wartość jest mniej niż 9.
Użycie:
(frequencies wektor)
.
Odwzorowanie JavaBean, bean
Funkcja bean
przyjmuje jako argument obiekt Javy, a zwraca mapę reprezentującą
właściwości JavaBean tego obiektu. Mapa jest przeznaczona tylko do odczytu i jest
obiektem typu clojure.lang.APersistentMap
uwidacznianym przez klasę pośredniczącą
clojure.core.proxy
.
Mapowe S-wyrażenia
Mapowe S-wyrażenie (ang. map S-expression) to element składniowy pozwalający w przejrzysty sposób tworzyć mapy. Składa się z zestawu par klucz–wartość ujętych w nawiasy klamrowe lub z samych nawiasów klamrowych (w przypadku map pustych). Klucze są elementami nieparzystymi, a przypisane im wartości muszą następować zaraz po nich. Separatorem kluczy i wartości jest znak spacji lub przecinka, albo obydwa te znaki.
Jeżeli podane elementy są formami innymi niż stałe, będą wartościowane, a w mapie zostaną umieszczone rezultaty obliczeń.
Rezultatem obliczenia mapowego S-wyrażenia, które reprezentuje formę mapową, jest
mapa (obiekt typu clojure.lang.PersistentHashMap
) lub mapa tablicowa (obiekt typu
clojure.lang.PersistentArrayMap
), jeżeli podano mniej niż 9 par klucz–wartość.
Mapowe S-wyrażenia znajdują również zastosowanie w formach powiązaniowych, np. w procesie obsługi argumentów nazwanych funkcji i przy mapowych wyrażeniach powiązaniowych oraz inicjujących, wykorzystywanych w procesie dekompozycji (destrukturyzacji).
Użycie:
{}
,{klucz–wartość…}
,
gdzie klucz–wartość
to:
klucz wartość
.
Wyrażenia mapowe mogą również być używane w formach powiązaniowych symboli, aby przypisywać im metadane. Mówimy wtedy o wyrażeniach metadanowych.
Użycie:
^{klucz–wartość…}
,
gdzie klucz–wartość
to:
klucz wartość
.
Zacytowane mapowe S-wyrażenia
Mapowe S-wyrażenia mogą zostać zacytowane. W takim przypadku podawane wartości i klucze nie będą wartościowane, nawet jeżeli są formami, które nie wyrażają wartości własnych, lecz w mapie zostaną umieszczone struktury danych odpowiadające umieszczanym S-wyrażeniom.
Użycie:
'{}
,'{klucz–wartość…}
,(quote {})
,(quote {klucz–wartość…})
,
gdzie klucz–wartość
to:
klucz wartość
.
Dostęp do map
Pierwszy element, first
Do pobierania pierwszego elementu mapy możemy użyć funkcji first
. Przyjmuje ona
jeden argument (mapę), a zwraca pierwszą parę przyporządkowań (obiekt typu
clojure.lang.MapEntry
) lub wartość nieustaloną nil
, jeżeli pierwszy element nie
istnieje.
Użycie:
(first mapa)
.
Zauważmy, że w przykładzie mamy do czynienia z mapą nieuporządkowaną, której
pierwszym element jest identyfikowany kluczem :b
.
Zwracana przez funkcję first
wartość tak naprawdę nie jest wektorem, lecz jest
przez REPL symbolicznie prezentowana jako wektor. W istocie jest to pojedynczy
element mapy, o którego typie możemy się przekonać samodzielnie:
Ostatni element, last
Do pobierania ostatniego elementu mapy możemy użyć funkcji last
. Przyjmuje ona
jeden argument (mapę), a zwraca ostatnią parę przyporządkowań (obiekt typu
clojure.lang.MapEntry
) lub wartość nieustaloną nil
, jeżeli ostatni element nie
istnieje.
Użycie:
(last mapa)
.
Zwracana przez funkcję last
wartość tak naprawdę nie jest wektorem, lecz jest przez
REPL symbolicznie wyrażana jako wektor. W istocie jest to pojedynczy element mapy
(obiekt typu clojure.lang.MapEntry
).
Pobieranie wartości, get
Pobrania wartości skojarzonej z podanym kluczem można dokonać korzystając
z generycznej funkcji get
. Przyjmuje ona dwa obowiązkowe argumenty: mapę i klucz
wskazujący na element, którego wartość chcemy pobrać.
Funkcja get
zwraca wartość elementu o podanym kluczu lub wartość nieustaloną nil
,
jeżeli klucza nie znaleziono. Gdy podano trzeci, opcjonalny argument, zamiast nil
zwrócona będzie jego wartość.
Użycie:
(get mapa klucz wartość-domyślna?)
.
Pobieranie zagnieżdżonych, get-in
Pobieranie wartości skojarzonej z kluczem w zagnieżdżonej mapie można zrealizować
z użyciem get-in
. Przyjmuje ona dwa obowiązkowe argumenty: mapę i wektor
określający ścieżkę kluczy wiodącą do elementu, którego wartość chcemy pobrać.
Funkcja get-in
zwraca wartość elementu o podanej ścieżce określonej kluczami lub
wartość nieustaloną nil
, jeżeli sekwencji kluczy nie znaleziono. Jeżeli podano
trzeci, opcjonalny argument, jego wartość będzie zwrócona zamiast nil
.
Użycie:
(get-in mapa wektor-ścieżki wartość-domyślna?)
.
Wartość elementu, val
Pobieranie wartości dla elementu mapy umożliwia funkcja val
.
Użycie:
(val element-mapy)
.
Funkcja przyjmuje jeden argument, którym powinien być element mapy (obiekt typu
clojure.lang.MapEntry
), a zwraca przechowywaną tam wartość.
Wszystkie wartości, vals
Pobieranie wszystkich wartości mapy umożliwia funkcja vals
.
Użycie:
(vals mapa)
.
Funkcja przyjmuje jeden argument (mapę), a zwraca sekwencję wartości.
Klucz elementu, key
Pobieranie klucza dla pojedynczego elementu możliwe jest dzięki funkcji key
.
Użycie:
(key element-mapy)
.
Funkcja przyjmuje jeden argument, którym powinien być element mapy (obiekt typu
clojure.lang.MapEntry
), a zwraca przechowywany tam klucz.
Wszystkie klucze, keys
Na pobieranie wszystkich kluczy mapy pozwala funkcja keys
.
Użycie:
(keys mapa)
.
Funkcja przyjmuje jeden argument (mapę), a zwraca sekwencję kluczy.
Przeszukiwanie map
Mapa jako funkcja
Obiekty map wyposażone są w funkcyjny interfejs, tzn. są również specyficznymi
funkcjami. Możemy umieścić mapę jako pierwszy element listowego S-wyrażenia i będzie
ono wtedy formą przeszukiwania mapy. Przyjmuje ona jeden obowiązkowy argument,
który oznacza element identyfikowany kluczem, a zwraca jego wartość (podobnie jak
w przypadku get
). Jeżeli element o podanym kluczu nie istnieje w mapie, zwracana
jest wartość nieustalona nil
, chyba że jako drugi argument podano wartość domyślną
(w takim przypadku zwracana jest właśnie ona).
Użycie:
(mapa klucz)
.
Słowo kluczowe jako funkcja
Słowa kluczowe implementują interfejs IFn
, czyli są funkcjami. W tej
formie (przeszukiwania map) znajdują zastosowanie przy pobieraniu wartości
elementów – oczywiście pod warunkiem, że kluczami tych map są słowa kluczowe.
Słowa kluczowe występujące w formie przeszukiwania mapy (umieszczone na pierwszym
miejscu listowego S-wyrażenia) przyjmują jeden obowiązkowy argument (mapę),
a zwracają wartość elementu mapy, który jest identyfikowany danym słowem kluczowym
(w roli klucza). Jeżeli w mapie nie istnieje element o podanym kluczu, zwracana jest
wartość nieustalona nil
, chyba że przekazano drugi argument – w takim przypadku
zwracana jest jego wartość.
Użycie:
(słowo-kluczowe mapa wartość-domyślna?)
.
Wyszukiwanie elementu, find
Pobieranie elementu (klucza i wartości) dla podanego klucza można zrealizować
z użyciem funkcji find
.
Użycie:
(find mapa klucz)
.
Funkcja przyjmuje dwa argumenty: pierwszym powinna być mapa, a drugim wartość klucza identyfikującego poszukiwany element.
Wartością zwracaną jest element mapy (obiekt typu clojure.lang.MapEntry
) lub
wartość nieustalona nil
, jeżeli element nie został znaleziony.
Wyszukiwanie klucza, contains?
Aby sprawdzić, czy podany klucz istnieje, należy użyć generycznej funkcji
contains?
. Przyjmuje ona dwa obowiązkowe argumenty: mapę i poszukiwany klucz,
a zwraca wartość true
, jeżeli element o podanym kluczu istnieje. W przeciwnym razie
zwraca wartość false
.
Użycie:
(contains? mapa klucz)
.
Dodawanie elementów
Dodawanie asocjacji, assoc
Wytworzenie mapy pochodnej z dodanymi nowymi elementami można uzyskać stosując
funkcję assoc
. Przyjmuje ona trzy obowiązkowe argumenty: mapę, klucz i wartość,
która ma być identyfikowana podanym kluczem. Opcjonalnie możemy podać kolejne pary
argumentów, aby umieścić w mapie więcej wartości identyfikowanych kluczami.
Funkcja assoc
zwraca mapę z dodanymi elementami.
Użycie:
(assoc mapa klucz–wartość & klucz–wartość…)
,
gdzie klucz–wartość
to:
klucz wartość
.
Dodawanie zagnieżdżonych, assoc-in
Wytworzenie mapy pochodnej z dodanymi elementami, których ścieżka określona jest
wektorem kluczy uzyskamy dzięki funkcji assoc-in
. Przyjmuje ona trzy obowiązkowe
argumenty. Pierwszym powinna być mapa, kolejnym wektor kluczy z tej mapy
określający ścieżkę, a ostatnim wartość, jaka powinna być wpisana do elementu
identyfikowanego ścieżką.
Funkcja assoc-in
zwraca nową mapę z dodanym elementem, który wskazano ścieżką
kluczy. Jeżeli klucze struktury pośredniej nie istnieją, zostaną utworzone.
Użycie:
(assoc-in mapa ścieżka wartość)
.
Usuwanie elementów
Usuwanie asocjacji, dissoc
Wytworzenie mapy pochodnej z usuniętymi elementami o podanych kluczach możliwe jest
dzięki funkcji dissoc
. Przyjmuje ona obowiązkowe dwa argumenty: mapę i klucz
identyfikujący element, który ma zostać usunięty. Opcjonalnie możemy podać jako
kolejne argumenty więcej kluczy, aby usunąć więcej elementów.
Funkcja dissoc
zwraca mapę z usuniętymi elementami identyfikowanymi podanymi
kluczami.
Użycie:
(dissoc mapa klucz & klucz…)
.
Usuwanie zagnieżdżonych, dissoc-in
Funkcja dissoc-in
pozwala usuwać klucze w zagnieżdżonych mapach. Nie jest ona
w momencie pisania tej części podręcznika obecna w rdzeniu języka, ale istnieje
w repozytorium
core.incubator
Użycie:
(dissoc-in mapa ścieżka)
.
Aktualizowanie map
Zmiana wartości, assoc
Funkcja assoc
pozwala na wytworzenie mapy pochodnej ze zmienioną wartością elementu
o podanym kluczu.
Użycie:
(assoc mapa klucz–wartość)
,
gdzie klucz–wartość
to:
klucz wartość
.
Zmiana zagnieżdżonej, assoc-in
Wytworzenie zagnieżdżonej mapy pochodnej ze zmienioną wartością elementu określonego
podaną ścieżką kluczy możliwe jest dzięki funkcji assoc-in
.
Użycie:
(assoc-in mapa ścieżka wartość)
.
Jeżeli klucze struktury pośredniej nie istnieją, zostaną utworzone.
Funkcja assoc-in
potrafi też operować na bardziej skomplikowanych strukturach, na
przykład wektorach zawierających mapy. W takich przypadkach pierwszym z kluczy
w ścieżce będzie numer indeksu:
W powyższym przykładzie ścieżką kluczy jest 1 :wiek
, a więc w wektorze zostanie
pobrany element o indeksie 1 (czyli drugi w kolejności), a następnie dla jego
wartości, którą jest mapa, zostanie wybrany do aktualizacji element o kluczu :wiek
.
Aktualizacja wartości, update
Aktualizację wartości elementu struktury wskazanego kluczem umożliwia funkcja
update
. Jako pierwszy argument przyjmuje ona mapę, kolejnym powinna być wartość
klucza zmienianego elementu, a ostatnim funkcja, która przyjmuje jeden argument
i zwraca wartość. Wartość ta zostanie użyta do aktualizacji wartości elementu.
Funkcja update
zwraca nową mapę.
Użycie:
(update mapa klucz operator)
.
Aktualizacja zagnieżdżonej, update-in
Aktualizację wartości elementu zagnieżdżonej struktury wskazanego ścieżką kluczy,
umożliwia funkcja update-in
. Jako pierwszy argument przyjmuje ona mapę, kolejnym
powinna być sekwencja określająca ścieżkę kluczy wiodących do elementu, a ostatnim
funkcja, która przyjmuje jeden argument i zwraca wartość. Wartość ta zostanie użyta
do aktualizacji wartości wskazanego elementu struktury.
Funkcja update-in
zwraca nową mapę.
Użycie:
(update-in mapa sekwencja-ścieżki operator)
.
Przekształcanie map sortowanych
Mapy sortowane zachowują kolejność elementów, a więc możemy na nich wykonywać
operacje, które biorą pod uwagę porządek struktury. Należą do nich rseq
, subseq
i rsubseq
.
Odwracanie kolejności, rseq
Funkcja rseq
w stałym czasie tworzy sekwencję na bazie podanej mapy
sortowanej, przy czym kolejność elementów jest odwrócona względem kolejności
w mapie. Pierwszym i jedynym przekazywanym jej argumentem powinna być mapa,
a wartością zwracaną jest sekwencja elementów mapy (obiektów typu
clojure.lang.PersistentTreeMap$RedVal
).
Użycie:
(rseq mapa-sortowana)
.
Odwracanie kolejności map sortowanych z użyciem sekwencyjnego interfejsu dostępu
generuje sekwencję, której można używać bezpośrednio, jednak wówczas tracimy prędkość
związaną z brakiem indeksowania na bazie kluczy. Jeżeli więc zależy nam na zachowaniu
struktury, możemy przekształcić rezultat rseq
do mapy, na przykład korzystając
z funkcji into
.
Sekwencja z zakresu, subseq
Funkcja subseq
pozwala na tworzenie sekwencji zawierającej elementy
określone zakresem wyznaczonym operatorami i wartościami przekazanymi jako
argumenty. Przyjmuje ona 3 lub 5 argumentów.
W wersji trójargumentowej należy podać mapę sortowaną, funkcję testującą i wartość
przekazywaną jako drugi argument funkcji testującej (pierwszym będzie klucz kolejno
przetwarzanego elementu podczas porównywania). Jeżeli na przykład podamy > 2
,
w wynikowej sekwencji znajdą się wyłącznie elementy, których klucze są wartościami
większymi od 2.
W wersji pięcioargumentowej należy również podać mapę, jednak kolejne 4 argumenty to pary określające zakresy: funkcja testująca i wartość dla dolnej granicy zakresu, a następnie funkcja testująca i wartość dla górnej granicy zakresu.
Funkcja zwraca sekwencję zawierającą pary klucz–wartość wyrażone obiektami typu
clojure.lang.PersistentTreeMap$BlackVal
.
Najczęściej stosowanymi funkcjami testującymi do określania granic zakresów są: <
,
<=
, >
i >
.
Użycie:
(subseq mapa-sortowana test wartość)
,(subseq mapa-sortowana test-start wartość-start test-stop wartość-stop)
.
Odwrócona sek. z zakresu, rsubseq
Funkcja rsubseq
działa jak połączenie rseq
i subseq
, tzn. umożliwia tworzenie
sekwencji z zakresu elementów mapy, a dodatkowo kolejność elementów jest
odwrócona. Przyjmuje ona 3 lub 5 argumentów.
W wersji trójargumentowej należy podać mapę sortowaną, funkcję testującą i wartość
przekazywaną jako drugi argument funkcji testującej (pierwszym będzie klucz kolejno
przetwarzanego elementu podczas porównywania). Funkcja testująca wraz z wartością
wyrażają po prostu granicę zakresu, np. podanie < 2
sprawi, że w sekwencji zostaną
umieszczone tylko te elementy, których klucze są wartościami mniejszymi niż 2.
W wersji pięcioargumentowej należy również podać mapę, lecz kolejne 4 argumenty powinny być dwoma parami określającymi zakresy. Para pierwsza: funkcja testująca i wartość dla dolnej granicy zakresu; para druga: funkcja testująca i wartość dla górnej granicy zakresu.
Funkcja zwraca sekwencję zawierającą pary klucz–wartość wyrażone obiektami typu
clojure.lang.PersistentTreeMap$BlackVal
.
Najczęściej stosowanymi funkcjami testującymi do określania granic zakresów są: <
,
<=
, >
i >
.
Użycie:
(rsubseq mapa-sortowana test wartość)
,(rsubseq mapa-sortowana test-start wartość-start test-stop wartość-stop)
.
Działania algebraiczne na mapach
W stosunku do map można używać działań rachunku relacyjnego (ang. relational algebra), ponieważ te struktury danych są zbiorami wyrażającymi relacje.
Przemianowanie kluczy, rename-keys
Zmiana kluczy możliwa jest dzięki funkcji rename-keys
. Przyjmuje ona dwa
obowiązkowe argumenty: mapę i mapę wyrażającą zmiany. Ta ostatnia powinna zawierać
pary, których klucze odpowiadają kluczom podanej mapy, a ich wartości wyrażają nowe
nazwy kluczy.
Funkcja zwraca mapę ze zmienionymi nazwami kluczy.
Użycie:
(clojure.set/rename-keys mapa mapa-zmian)
.
Odwracanie relacji, map-invert
Tworzenie relacji odwrotnej przez zamianę kluczy z wartościami umożliwia funkcja
map-invert
. W przypadku powielających się wartości, wykorzystywana jako klucz jest
pierwsza napotkana wartość.
Funkcja przyjmuje jeden argument (mapę) i zwraca mapę, której klucze są wartościami podanej mapy, a wartości kluczami przypisanymi pierwotnie do tych wartości.
Użycie:
(map-invert mapa)
.
Złączenie zewnętrzne, merge
Funkcja merge
pozwala dokonać złączenia map z lewostronnym przesłanianiem wartości
w przypadku takich samych kluczy. Przyjmuje ona zero lub więcej argumentów, które
powinny być mapami, a zwraca mapę będącą ich złączeniem.
Użycie:
(merge & mapa…)
.
Złączenie z przesłanianiem, merge-with
Złączenie map z lewostronnym przesłanianiem wartości w przypadku takich samych kluczy
i z przemianowywaniem wartości przez podany operator możliwe jest dzięki funkcji
merge-with
.
Przyjmuje ona jeden obowiązkowy argument (funkcję operującą), która powinna przyjmować tyle argumentów, ile podano map, a następnie dokonywać łączenia podanych wartości w oczekiwany sposób (np. przez złączenie kolekcji czy sumowanie liczb). Kolejnymi, opcjonalnymi argumentami funkcji są mapy, które będą używane jako źródło danych.
Funkcja zwraca mapę będącą złączeniem map podanych jako argumenty.
Użycie:
(merge-with operator & mapa…)
.
W powyższych przykładach korzystamy z funkcji str
, która łączy łańcuchy
tekstowe podane jako argumenty, a także z funkcji +
sumującej wartości
liczbowe. Obie przekazujemy jako operatory do funkcji wyższego rzędu merge-with
,
która wywoła je dla wartości elementów map przekazanych jako argumenty, pod
warunkiem, że mamy do czynienia z konfliktem, tzn. identyfikowane tym samym kluczem
elementy można znaleźć w więcej niż jednej podanej mapie. Dla elementów, których
klucze są unikatowe w obrębie wszystkich podanych map (nie występują konflikty), nie
będzie stosowana funkcja przeliczająca – zostaną po prostu skopiowane do wyjściowej
struktury.
Złączenie z sekwencji, into
Do łączenia zawartości map może też posłużyć polimorficzna funkcja
into
. Przyjmuje ona minimum dwa argumenty, z których pierwszy jest
mapą, na bazie której powstanie nowa mapa, a drugim sekwencja par klucz—wartość
wyrażonych jako sekwencyjne struktury.
Wartością zwracaną w przypadku map będzie również mapa.
Użycie:
(into mapa-docelowa sekwencja-par)
.
Selekcja elementów, select-keys
Tworzenie mapy pochodnej, która zawiera wyłącznie elementy o podanych kluczach polega
na wywołaniu funkcji select-keys
. Przyjmuje ona dwa obowiązkowe argumenty: mapę
i wektor określający klucze. Wartością zwracaną jest mapa zawierająca wyłącznie
elementy identyfikowane kluczami, które podano w wektorze przekazanym jako drugi
argument.
Użycie:
(select-keys map ścieżka)
.
Mapy strukturalizowane
Mapa strukturalizowana, zwana też mapą typu struct (ang. struct map), to mapa o ściśle określonym zbiorze kluczy, które mogą się w niej znaleźć. Z uwagi na tę właściwość cechuje ją nieco szybszy dostęp do elementów. Opcjonalnie mapy strukturalizowane mogą zawierać też dowolne, dodatkowe klucze, nieznane podczas ich tworzenia.
Mapy strukturalizowane nie są jedynym sposobem porządkowania danych o ustalonych strukturach. W większości przypadków zaleca się korzystanie z tzw. rekordów, które zostaną omówione później.
Tworzenie map strukturalizowanych
Tworzenie map jest dwuetapowe. W pierwszym kroku należy utworzyć strukturę możliwych
kluczy, korzystając z konstrukcji create-struct
, a w kolejnym wywołać struct-map
lub struct
, aby utworzyć właściwą instancję struktury wyrażanej mapą.
Tworzenie struktury, create-struct
Funkcja create-struct
tworzy strukturę, której pola są identyfikowane wartościami
przekazanymi jako argumenty. Należy przekazać przynajmniej jeden argument.
Funkcja zwraca obiekt struktury (clojure.lang.PersistentStructMap
).
Użycie:
(create-struct klucz & klucz…)
.
Definiowanie struktur, defstruct
Funkcja defstruct
tworzy strukturę i umieszcza odniesienie do niej w zmiennej
globalnej. Pierwszym argumentem powinna być nazwa zmiennej
wyrażona niezacytowanym symbolem, a kolejnymi nazwy kluczy. Wartością zwracaną jest
zmienna globalna (obiekt typu Var
) odnosząca się do struktury bazowej (typu
clojure.lang.PersistentStructMap
).
Wewnętrznie funkcja defstruct
wywołuje def
na rezultacie
create-struct
.
Użycie:
(defstruct symbol klucz & klucz…)
.
Tworzenie mapy, struct-map
Funkcja struct-map
tworzy mapę strukturalizowaną na podstawie obiektu struktury
i podanych danych asocjacyjnych. Przyjmuje jeden obowiązkowy argument, którym jest
obiekt struktury i argumenty opcjonalne w postaci par wyrażających początkowe
wartości poszczególnych pól. Pierwszymi elementami par powinny być klucze o nazwach
takich samych, jak nazwy pól, zaś drugimi nadawane wartości.
Funkcja zwraca mapę strukturalizowaną
(obiekt typu clojure.lang.PersistentStructMap
).
Użycie:
(struct-map struktura & klucz–wartość…)
,
gdzie klucz–wartość
to:
klucz wartość
.
Tworzenie mapy, struct
Funkcja struct
działa podobnie do struct-map
: tworzy mapę strukturalizowaną na
podstawie obiektu struktury. Różni się jednak charakterem przyjmowanych argumentów,
ponieważ kolejne wartości pól struktury muszą być wyrażone pozycyjnie, a nie
asocjacyjnie. Funkcja przyjmuje jeden obowiązkowy argument, którym jest definicja
struktury i argumenty opcjonalne, którymi są wartości kolejnych pól struktury.
Funkcja struct
zwraca mapę strukturalizowaną
(obiekt typu clojure.lang.PersistentStructMap
).
Użycie:
(struct struktura & wartość…)
.
Użytkowanie map strukturalizowanych
Korzystanie z map strukturalizowanych nie różni się od użytkowania innych map. Można na nich przeprowadzać takie same operacje, jak na mapach, włączając w to dynamiczne dodawanie kluczy, które nie zostały zdefiniowane we wzorcowej strukturze. Jest tylko jeden wyjątek: nie można usuwać elementów, których klucze zdefiniowano.
Poza interfejsem mapowym mapy strukturalizowane wyposażone są w szybkie akcesory dla zdefiniowanych pól.
Dostęp do pól, accessor
Funkcja accessor
zwraca funkcję, której można użyć do szybkiego odczytu wybranego
pola konkretnej instancji struktury. Wewnętrznie zawiera ona wywołanie metody
odczytującej odpowiedni atrybut obiektu.
Jako pierwszy argument należy podać obiekt struktury, a jako drugi wyrażoną słowem kluczowym nazwę pola. Funkcja zwraca obiekt funkcyjny.
Użycie:
(accessor struktura klucz)
.
Zbiory
Zbiór (ang. set) to kolekcja, która służy do przechowywania unikatowych w jej obrębie elementów. Cechuje ją szybkie wyszukiwanie, ponieważ wewnętrznie jest tablicą mieszającą, której indeksami są wartości elementów.
Tworzenie zbiorów
Tworzyć zbiory możemy z użyciem jednej z kilku funkcji lub symbolicznego wyrażenia zbiorowego.
Zbiór z sekwencji, set
Funkcja set
tworzy nowy zbiór na podstawie podanej sekwencji, którą
może być dowolna kolekcja wyposażona w sekwencyjny interfejs dostępu. Zwraca ona
zbiór, którego elementami są wartości z sekwencji.
Użycie:
(set sekwencja)
.
Zbiór z argumentów, hash-set
Funkcja hash-set
tworzy nowy zbiór na podstawie zestawu elementów podanych jako
argumenty. Jeżeli nie podano argumentów zwracany jest zbiór pusty, a jeżeli podano
je, wartością zwracaną będzie zbiór zawierający ich wartości.
Użycie:
(hash-set & element…)
.
Zbiór sortowany, sorted-set
Funkcja sorted-set
działa tak samo jak hash-set
, lecz tworzy zbiór, którego
kolejność elementów będzie zachowywała porządek sortowania. Przyjmuje ona zero lub
więcej argumentów, których wartości staną się elementami zbioru, a wartością zwracaną
jest zbiór sortowany.
Użycie:
(sorted-set & element…)
.
Zbiór sortowany, sorted-set-by
Funkcja sorted-set-by
działa podobnie jak sorted-set
i również tworzy zbiór
sortowany, z tą jednak różnicą, że kryterium tego sortowania można ustalić.
Przyjmuje ona jeden obowiązkowy argument, którym powinna być funkcja porównująca
(tzw. komparator). Powinna ona przyjmować dwa argumenty, a zwracać wartość -1 (lub
mniejszą), 0 lub 1 (lub większą), w zależności od tego czy pozycja pierwszego
argumentu powinna być mniejsza, równa czy większa od pozycji argumentu drugiego.
Opcjonalne argumenty przyjmowane przez sorted-set-by
to wartości, które staną się
elementami tworzonego zbioru. Funkcja zwraca zbiór sortowany.
Użycie:
(sorted-set-by komparator & element…)
.
Zbiorowe S-wyrażenia
Zbiorowe S-wyrażenie (ang. set S-expression) to element składniowy pozwalający w przejrzysty sposób tworzyć zbiory. Składa się z zestawu wartości ujętych w nawiasy klamrowe poprzedzone symbolem kratki lub z samych nawiasów klamrowych poprzedzonych tym symbolem (w przypadku zbiorów pustych). Separatorem wartości jest znak spacji lub przecinka, albo obydwa te znaki.
Jeżeli podane elementy są formami innymi niż stałe, będą wartościowane, a w zbiorze zostaną umieszczone rezultaty obliczeń.
Użycie:
#{}
,#{element…}
.
Zacytowane zbiorowe S-wyrażenia
Wyrażenia zbiorowe mogą być zacytowane. W takim przypadku podawane elementy nie będą wartościowane, nawet jeżeli są formami, które nie wyrażają wartości własnych.
Użycie:
'#{}
,'#{element…}
,(quote #{})
,(quote #{element…})
.
Dostęp do zbiorów
Pobieranie elementu, get
Sprawdzania czy element istnieje w zbiorze można dokonać z wykorzystaniem generycznej
funkcji get
.
Użycie:
(get zbiór wartość wartość-domyślna?)
.
Funkcja get
przyjmuje dwa obowiązkowe argumenty: pierwszym powinien być zbiór,
a drugim poszukiwany element. Jeżeli element znajduje się w zbiorze, jego wartość
zostanie zwrócona. W przeciwnym wypadku zwrócona będzie wartość nieustalona nil
lub
wartość przekazana jako opcjonalny, trzeci argument.
Przeszukiwanie zbiorów
Zbiór jako funkcja
Obiekty zbiorów wyposażone są w funkcyjny interfejs, tzn. są również specyficznymi
funkcjami. Na poziomie semantycznym mogą stawać się formami przeszukiwania
zbiorów. Przyjmują wtedy jeden argument wyrażający wartość poszukiwanego elementu
(podobnie jak get
), a zwracają tę wartość, jeżeli znajduje się ona
w zbiorze. W przeciwnym przypadku zwracana jest wartość nieustalona nil
.
Użycie:
(zbiór wartość)
.
Słowo kluczowe jako funkcja
Słowa kluczowe implementują interfejs IFn
, czyli są również
funkcjami. W tej formie (przeszukiwania zbiorów) znajdują zastosowanie przy
pobieraniu wartości zbiorów, oczywiście pod warunkiem, że ich elementami są słowa
kluczowe.
Słowa kluczowe w formie funkcji przyjmują jeden obowiązkowy argument (m.in. zbiór),
a zwracają wartość elementu, jeżeli taka sama wartość została znaleziona
w zbiorze. Jeżeli element nie istnieje, zwracana jest wartość nieustalona nil
,
chyba że przekazano drugi argument – w takim przypadku zwracana jest jego wartość.
Użycie:
(słowo-kluczowe zbiór wartość-domyślna?)
.
Wyszukiwanie elementu, contains?
Sprawdzania czy element istnieje w zbiorze można dokonać z wykorzystaniem generycznej
funkcji contains?
. Przyjmuje ona dwa argumenty: pierwszym powinien być zbiór,
a drugim poszukiwana wartość elementu. Funkcja zwraca true
, jeżeli podany element
istnieje w zbiorze, a false
w przeciwnym razie.
Użycie:
(contains? zbiór wartość)
.
Dodawanie i usuwanie elementów
Dodawanie elementów, conj
Dodawanie elementów (tworzenie zbioru z dodanymi nowymi elementami) można zrealizować
z użyciem generycznej funkcji conj
.
Użycie:
(conj zbiór element & element…)
.
Funkcja conj
przyjmuje dwa obowiązkowe argumenty: zbiór źródłowy i element, który
powinien zostać dodany do zbioru. Opcjonalnie można podać więcej elementów jako
kolejne argumenty.
Wartością zwracaną jest nowy zbiór z dodanymi elementami.
Usuwanie elementów, disj
Usuwanie elementu (tworzenie zbioru z usuniętymi wybranymi elementami) polega na
wywołaniu generycznej funkcji disj
. Przyjmuje ona jeden obowiązkowy argument (zbiór
źródłowy) i zero lub więcej argumentów określających wartości usuwanych
elementów. Funkcja zwraca nowy zbiór z usuniętymi wybranymi elementami.
Użycie:
(disj zbiór & element…)
.
Działania algebraiczne
W stosunku do zbiorów możemy korzystać z funkcji algebry zbiorów i algebry relacji (w przypadku zbiorów zawierających dane relacyjne).
Złączenia, join
Złączenie naturalne zbiorów złożonych z map można uzyskać z użyciem funkcji
join
, która przyjmuje dwa argumenty (dwa zbiory map wyrażających relacje), a zwraca
zbiór map wyrażający ich złączenie naturalne.
Użycie:
(clojure.set/join zbiór-map drugi-zbiór-map)
.
Złączenia równościowego (ze wskazaniem kluczy używanych do łączenia) zbiorów
złożonych z map można również dokonać z wykorzystaniem funkcji join
, lecz
w wariancie z trzema argumentami: dwa pierwsze to zbiory map wyrażających relacje,
a ostatni zbiór kluczy używanych do złączenia. Funkcja zwraca zbiór map wyrażający
rezultat operacji.
Użycie:
(clojure.set/join zbiór-map drugi-zbiór-map mapa-kluczy)
.
Projekcja, project
Projekcja zbiorów map reprezentujących relacje przez pozostawienie tylko zbiorów
o kluczach określonych podaną kolekcją umożliwia funkcja project
. Jest to
filtrowanie zbiorów map po kluczach tych ostatnich.
Użycie:
(clojure.set/project zbiór-map zbiór-map kolekcja-kluczy)
.
Podzbiór, select
Wybieranie podzbioru elementów o wskazanych kryteriach umożliwia funkcja select
.
Użycie:
(clojure.set/select predykat zbiór)
.
Suma zbiorów, union
Użycie:
(clojure.set/union & zbiór…)
.
Różnica zbiorów, difference
Użycie:
(clojure.set/difference zbiór & zbiór-odejmowanych…)
.
Część wspólna zbiorów, intersection
Użycie:
(clojure.set/intersection zbiór & inny-zbiór…)
.
Przemianowanie w relacjach, rename
Zmiana nazw kluczy dla zbioru relacji wyrażonych mapami możliwa jest dzięki funkcji
clojure.set/rename
. Przyjmuje ona dwa argumenty: mapę relacji i mapę wyrażającą
pożądane zmiany nazw. Rezultatem wykonania funkcji jest zbiór map wyrażających
relacje, których klucze zostały przemianowane.
Użycie:
(clojure.set/rename mapa-relacji mapa-zmian)
.
Grupowanie zbioru relacji, index
Funkcja index
pozwala grupować wyrażone mapami, umieszczone w zbiorze relacje.
Przyjmuje ona dwa argumenty: pierwszy jest zbiorem relacji, a drugi wektorem
zawierającym klucze, które będą użyte do grupowania.
Rezultatem działania funkcji agregującej jest mapa par, której pierwszymi elementami są mapy zawierające kryterium grupowania, zaś drugimi zbiory relacji, które do tego kryterium pasują, wyrażone jako mapy.
Użycie:
clojure.set/index zbiór-map kolekcja-kluczy
.
Możliwe jest też grupowanie po więcej niż jednym kluczu. W takim przypadku warunek grupowania jest logicznym iloczynem dopasowań wartości kluczy (każdy z nich musi być spełniony, aby element został zgrupowany):
Przekształcanie zbiorów sortowanych
Zbiory sortowane zachowują kolejność elementów, a więc możemy na nich wykonywać
operacje, które zarządzają uporządkowaniem. Należą do nich rseq
, subseq
i rsubseq
.
Odwracanie kolejności, rseq
Funkcja rseq
w stałym czasie tworzy sekwencję na bazie podanego
zbioru sortowanego, przy czym kolejność elementów jest odwrócona względem
kolejności w zbiorze. Pierwszym i jedynym przekazywanym jej argumentem powinien być
sortowany zbiór.
Użycie:
(rseq zbiór-sortowany)
.
Odwracanie kolejności zbiorów sortowanych z użyciem sekwencyjnego interfejsu dostępu
generuje sekwencję, której można używać bezpośrednio, jednak wówczas tracimy prędkość
związaną ze swobodnym rodzajem dostępu. Jeżeli więc zależy nam na zachowaniu
struktury, możemy przekształcić rezultat rseq
do zbioru, na przykład korzystając
z funkcji into
.
Sekwencja z zakresu, subseq
Funkcja subseq
pozwala na tworzenie sekwencji zawierającej elementy
określone zakresem wyznaczonym operatorami i wartościami przekazanymi jako
argumenty. Przyjmuje ona 3 lub 5 argumentów.
W wersji trójargumentowej należy podać zbiór sortowany, funkcję testującą i wartość
przekazywaną jako drugi argument funkcji testującej (pierwszym będzie klucz kolejno
przetwarzanego elementu podczas porównywania). Jeżeli na przykład podamy > 2
,
w wynikowej sekwencji znajdą się wyłącznie elementy, których klucze są wartościami
większymi od 2.
W wersji pięcioargumentowej należy również podać zbiór, jednak kolejne 4 argumenty to pary określające zakresy: funkcja testująca i wartość dla dolnej granicy zakresu, a następnie funkcja testująca i wartość dla górnej granicy zakresu.
Funkcja zwraca sekwencję wartości z określonego zakresem fragmentu zbioru.
Najczęściej stosowanymi funkcjami testującymi do określania granic zakresów są: <
,
<=
, >
i >
.
Użycie:
(subseq zbiór-sortowany test wartość)
,(subseq zbiór-sortowany test-start wartość-start test-stop wartość-stop)
.
Odwrócona sekw. z zakresu, rsubseq
Funkcja rsubseq
działa jak połączenie rseq
i subseq
, tzn. umożliwia tworzenie
sekwencji z zakresu elementów zbioru, a dodatkowo kolejność elementów
jest odwrócona. Przyjmuje ona 3 lub 5 argumentów.
W wersji trójargumentowej należy podać zbiór sortowany, funkcję testującą i wartość
przekazywaną jako drugi argument funkcji testującej (pierwszym będzie klucz kolejno
przetwarzanego elementu podczas porównywania). Funkcja testująca wraz z wartością
wyrażają po prostu granicę zakresu, np. podanie < 2
sprawi, że w sekwencji zostaną
umieszczone tylko te elementy, których klucze są wartościami mniejszymi niż 2.
W wersji pięcioargumentowej należy również podać mapę, lecz kolejne 4 argumenty powinny być dwoma parami określającymi zakresy. Para pierwsza: funkcja testująca i wartość dla dolnej granicy zakresu; para druga: funkcja testująca i wartość dla górnej granicy zakresu.
Funkcja zwraca sekwencję zawierającą zakres wartości ze zbioru uporządkowany w odwróconej kolejności.
Najczęściej stosowanymi funkcjami testującymi do określania granic zakresów są: <
,
<=
, >
i >
.
Użycie:
(rsubseq zbiór-sortowany test wartość)
,(rsubseq zbiór-sortowany test-start wartość-start test-stop wartość-stop)
.
Operacje generyczne
Na wszystkich kolekcjach można dokonywać pewnych wspólnych operacji, niezależnie od tego z jaką strukturą danych mamy konkretnie do czynienia.
Zarządzanie elementami
Dodawanie elementów, conj
Funkcja conj
służy do dodawania elementów do kolekcji. Powstaje wówczas nowa
kolekcja z dodanymi elementami, które przekazano jako argumenty. Funkcja przyjmuje
dwa obowiązkowe argumenty: kolekcję i dodawany element. Opcjonalnie można podać
kolejne elementy wyrażone wartościami kolejnych argumentów.
Umiejscowienie dodawanych elementów zależy od konkretnej kolekcji. Na przykład w przypadku list będą to ich czoła, a w przypadku wektorów końce.
Funkcja zwraca kolekcję z dodanymi elementami.
Użycie:
(conj kolekcja element & element…)
.
Dołączanie elementów, into
Dzięki funkcji into
możemy dołączać elementy jednych kolekcji do drugich lub
tworzyć nowe kolekcje (jeżeli podane są puste). Przyjmuje ona dwa obowiązkowe
argumenty: kolekcję docelową i kolekcję źródłową. Wartością zwracaną jest kolekcja
tego samego typu, co kolekcja docelowa z dołączonymi elementami pochodzącymi
z kolekcji źródłowej.
Użycie:
(into kolekcja-docelowa kolekcja-źródłowa)
.
Tworzenie podobnych kolekcji
Podobna kolekcja, empty
Funkcja empty
generuje pustą kolekcję tego samego typu, co kolekcja przekazana
jako argument.
Użycie:
(empty kolekcja)
.
Wartościowanie niepustych kolekcji
Zawsze niepuste, not-empty
Możemy sprawdzić czy kolekcja jest niepusta z użyciem funkcji not-empty
. Dla
pustych zestawów zwróci ona nil
, a dla tych, które mają przynajmniej jeden element
zwróci ich obiekt.
Użycie:
(not-empty kolekcja)
.
Badanie elementów kolekcji
Operator =
, porównywanie
Operator =
pozwala porównywać kolekcje. Kolekcje są równe, gdy zawierają takie
same elementy w takiej samej kolejności.
Użycie:
(= kolekcja & inna-kolekcja…)
.
Zliczanie elementów, count
Operator count
umożliwia zliczanie elementów kolekcji. Przyjmuje jeden
argument, którym może być kolekcja, tablica Javy, mapa lub nawet łańcuch tekstowy,
a zwraca liczbę elementów.
Użycie:
(count kolekcja)
.
Testowanie kolekcji
Testowanie typów
Istnieje kilka przydatnych funkcji, które pozwalają sprawdzać z jakiego typu kolekcją mamy do czynienia:
(coll? wartość)
–true
, gdy wartość jest kolekcją;(list? wartość)
–true
, gdy wartość jest listą;(vector? wartość)
–true
, gdy wartość jest wektorem;(set? wartość)
–true
, gdy wartość jest zbiorem;(map? wartość)
–true
, gdy wartość jest mapą;(record? wartość)
–true
, gdy wartość jest rekordem;(seq? wartość)
–true
, gdy wartość jest (również) sekwencją.
Warto zauważyć, że spośród głównych kolekcji jedynie listę (typu
clojure.lang.PersistentList
) można nazwać sekwencją, ponieważ implementuje ona
interfejs clojure.lang.ISeq
.
Testowanie cech
Możemy sprawdzać co cechuje kolekcję, używając jednej z przeznaczonych do tego funkcji:
(seqable? wartość)
–true
, gdy możliwa sekwencyjna reprezentacja;(sequential? wartość)
–true
, gdy ma sekwencyjny interfejs;(associative? wartość)
–true
, gdy jest asocjacyjna;(sorted? wartość)
–true
, gdy jest sortowana;(counted? wartość)
–true
, gdy jest policzalna w skończonym czasie;(reversible? wartość)
–true
, gdy można odwracać kolejność elementów.
Warto w tym miejscu krótko omówić różnicę między kolekcją, którą można reprezentować
sekwencyjnie, a taką, która ma sekwencyjny interfejs. Reprezentowanie kolekcji
w postaci sekwencji polega na użyciu względem niej funkcji seq
. Zwraca ona obiekt
pełnoprawnej sekwencji skojarzonej z elementami podanej kolekcji. Gdy na takim
obiekcie wywołamy funkcję seq?
, zwróci ona prawdę. Zanim dokonamy
przekształcenia kolekcji w sekwencję, możemy sprawdzić, czy taka operacja jest
możliwa, wywołując właśnie seqable?
.
Z kolei sequential?
pozwala sprawdzić, czy kolekcja implementuje Sequential
–
interfejs Javy używany w roli znacznika i dodawany do kolekcji, dla
których możliwy jest następczy dostęp do ich elementów (np. z użyciem funkcji first
i rest
). Nie oznacza to jednak, że takie kolekcje są sekwencjami. Przykładem może
być tu obiekt wektora – będzie on sekwencyjny (można uzyskiwać dostęp do kolejnych
elementów), lecz nie będzie sekwencją. Możemy jednak wytworzyć sekwencję bazującą na
wektorze (z użyciem funkcji seq
).
Różność, distinct?
Sprawdzanie czy dwie lub więcej kolekcji jest od siebie różnych możliwe jest
dzięki funkcji distinct?
.
Użycie:
(distinct? kolekcja & inna-kolekcja…)
.
Pustość, empty?
Sprawdzanie czy kolekcja jest pusta może być osiągnięte z wykorzystaniem funkcji
empty?
.
Użycie:
(empty? kolekcja)
.
„Czy każdy”, every?
Aby sprawdzić czy każdy element kolekcji spełnia warunek określony predykatem
(wyrażonym funkcją zwracającą wartość logiczną), można posłużyć się funkcją every?
.
Użycie:
(every? predykat kolekcja)
.
„Czy nie każdy”, not-every?
Sprawdzenia czy chociaż jeden element nie spełnia warunku określonego
predykatem możemy dokonać z użyciem funkcji not-every?
.
Użycie:
(not-every? predykat kolekcja)
.
„Czy któryś”, some
Sprawdzanie czy którykolwiek element spełnia warunek określony predykatem
zrealizujemy funkcją some
. Zwracaną wartością będzie nil
, jeżeli żaden i zwrócona
przez podaną funkcję wartość logiczna dla pierwszego napotkanego elementu.
Użycie:
(some predykat kolekcja)
.
„Czy nie żaden”, not-any?
Sprawdzenie czy żaden element kolekcji nie spełnia warunku określonego
predykatem wyrażonym podaną funkcją może być dokonane z użyciem not-any?
.
Użycie:
(not-any? predykat kolekcja)
.
Obsługa metadanych
Kolekcje, podobnie jak symbole i obiekty referencyjne, można opcjonalnie wyposażać w metadane – pomocnicze mapy, które przechowują dodatkowe informacje.
Odczytywanie metadanych, meta
Aby pobrać metadane dla kolekcji, należy skorzystać z funkcji meta
.
Użycie:
(meta kolekcja)
.
Funkcja meta
przyjmuje kolekcję, a zwraca mapę metadanową, jeżeli kolekcji
przypisano jakieś metadane lub wartość nieustaloną nil
w przeciwnym razie.
Zauważmy, że dla zacytowanego, listowego S-wyrażenia niektóre metadane będą ustawiane automatycznie (np. kolumna i linia pliku źródłowego). Dzieje się tak dlatego, że listowe S-wyrażenia są elementami strukturalizującymi kod źródłowy programu.
Dodawanie metadanych, with-meta
Aby ustawić własne metadane dla kolekcji, trzeba użyć funkcji with-meta
.
Użycie:
(with-meta kolekcja metadane)
.
Jako pierwszy argument funkcji with-meta
należy przekazać kolekcję, a jako drugi
mapę zawierającą klucze i przypisane do nich wartości metadanych, które
powinny być przypisane kolekcji.
Wartością zwracaną jest kolekcja z ustawionymi metadanymi.
Należy pamiętać o rozróżnieniu metadanych symboli identyfikujących kolekcje od metadanych tych kolekcji.
Zobacz także: