Poczytaj mi Clojure – cz. 8: Kolekcje

Kolekcje to abstrakcyjna klasa struktur danych, służąca do porządkowania i zarządzania zbiorami informacji. Dziś dowiemy się więcej o kolekcjach języka Clojure, a konkretnie o listach, wektorach, mapach i zbiorach.

Kolekcje

Kolekcja (ang. collection), zwana też niekiedy kontenerem (ang. container) to rodzina złożonych struktur danych, które służą do grupowania wielu elementów i pozwalają na dostęp do nich w zestandaryzowany sposób.

Warto mieć na względzie, że kolekcja nie jest konkretnym typem danych, lecz nazwą określającą różne struktury o pewnych wspólnych właściwościach.

Możemy wyróżnić trzy podstawowe rodzaje kolekcji: liniowe (ang. linear), asocjacyjne (ang. associative) i drzewiaste (ang. tree).

W Clojure niezależnie od tego z jakiego konkretnie typu kolekcją mamy do czynienia, jesteśmy w stanie korzystać z takiego samego interfejsu dostępu, czyli z zestawu operatorów (funkcji) pozwalających nią zarządzać. Dodatkowo istnieją też funkcje, które umożliwiają zarządzanie właściwościami specyficznymi dla pewnych 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 w dodatku mogą być także strukturami złożonymi.

Kolekcje mogą być wyposażone w sekwencyjny interfejs dostępu i są wtedy również sekwencjami (poznamy je w kolejnym rozdziale). Oznacza to, że na kolekcjach można operować, wykorzystując funkcje służące do obsługi sekwencji.

Przedstawione poniżej działania na opisywanych kolekcjach są operacjami związanymi z tymi kolekcjami (z małymi wyjątkami, np. dotyczącymi zmiany kolejności i pozyskiwania fragmentów z użyciem funkcji rseq, subseqrsubseq czy usuwania elementów z wektorów). Nie znajdziemy tu 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, to warto zajrzeć do kolejnej części poświęconej sekwencjom, ponieważ wszystkie opisywane tu kolekcje mogą być również traktowane sekwencyjnie.

Listy

Wiemy, że list można używać nie tylko do organizowania kodu programu, ale również jako struktury służącej do przechowywania danych. Możemy więc korzystać z list jak z kolekcji.

Przeznaczeniem list jest grupowanie elementów z zachowaniem porządku, czyli kolejności ich umieszczania. Zoptymalizowane są pod kątem szybkiego dodawania nowych elementów do ich czoła.

Z list będziemy korzystali przede wszystkim do sterowania procesami związanymi z implementacją programu (np. implementacje 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 łańcuchy logicznie powiązanych danych.

Listy przechowujące dane są wartościami typu clojure.lang.PersistentList, dla których utrzymywana jest informacja o rozmiarze (liczbie elementów). Z kolei listy stanowiące elementy drzewa składniowego (reprezentujące wyrażenia kodu źródłowego po wczytaniu S-wyrażeń) będą tzw. leniwymi listami, czyli zbiorami wartości typu clojure.lang.Cons. Dla tego typu list nie będzie przechowywany licznik rozmiaru.

Tworzenie list

Tworzyć listowe formuły możemy na trzy sposoby:

  • wpisując listowe S-wyrażenia – pierwszy element powinien być symbolem identyfikującym podprogram do wykonania (funkcję, makro lub formułę specjalną), a pozostałe argumentami przekazywanymi podczas jego wywoływania – powstają w ten sposób formuły funkcyjne, makrowe lub specjalne;

  • korzystając z konstrukcji list lub innej, która tworzy formuły stałe list – wszystkie elementy są po prostu danymi do umieszczenia w strukturze listy, ale każdy podany argument jest wcześniej wartościowany (traktowany jak wyrażenie, którego wartość trzeba obliczyć);

  • korzystając z zacytowanych listowych S-wyrażeń – wszystkie elementy są danymi (formułami stałymi) i nie są poddawane wartościowaniu.

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 (formułą symbolową) odnoszącym się do funkcji, makra lub formuły specjalnej, a kolejnymi argumenty, które zostaną przekazane podczas wywoływania podprogramu tej formuły.

Użycie:

  • (operator & operand…).
Przykłady list zawierających kod programu
1
2
3
4
5
6
(inc 1)                        ; => 2
(+ 2 2)                        ; => 4
(+)                            ; => 0
(printf "trala %d\n" (+ 1 1))
; => nil
; >> trala 2

Utworzone w ten sposób listy 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 formuły (funkcyjne, makrowe lub specjalne) będą wartościowane do rezultatów wykonania podprogramów.

Wewnętrznie (w drzewie składniowym programu) listy te będą tzw. leniwymi listami, składającymi się z komórek cons (typu clojure.lang.Cons).

Lista z argumentów, list

Listy można tworzyć z wykorzystaniem formuły specjalnej list. Takie listy są kolekcjami (typu clojure.lang.PersistentList) służącymi do przechowywania danych. Można powiedzieć, że są to formuły stałe list (listy w formach stałych), to znaczy listy wyrażające wartości własne (które nie podlegają dalszemu wartościowaniu).

Formuła 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 formuła, którą należy przeliczać, aż stanie się formułą stałą (wyrażającą wartość własną).

Formuła list zwraca listę (obiekt typu clojure.lang.PersistentList).

Użycie:

  • (list & element…).
Przykłady tworzenia list
1
2
(list 1 2 3)     ; tworzenie listy
(list)           ; tworzenie pustej listy

Warto wiedzieć, że elementy wyrażeń listowych, podobnie jak inne argumenty wyrażane symbolicznie, mogą być oddzielone nie tylko znakami spacji, ale również przecinkami:

Przykłady elementów list oddzielonych przecinkami
1
2
(list 1, 2,3)
; => (1 2 3)

Lista z kolekcji, list*

Podobną do formuły specjalnej list jest funkcja 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 element, ale można ją poprzedzić pojedynczymi elementami podanymi jako wartości wcześniejszych argumentów. Jeżeli nie chcemy podawać kolekcji, to można zamiast niej przekazać jako argument wartość nil.

Wartością zwracaną jest lista, czyli obiekt typu clojure.lang.PersistentList.

Przykłady tworzenia list z użyciem funkcji list*
1
2
3
4
5
6
(list* 1 2 3 [4])           ; => (1 2 3 4)
(list* 1 2 3 [4 5 6])       ; => (1 2 3 4 5 6)
(list* 1 2 3 (list 4 5 6))  ; => (1 2 3 4 5 6)
(list* 1 2 3 '(4 5 6))      ; => (1 2 3 4 5 6) 
(list* 1 2 3 ())            ; => (1 2 3)
(list* 1, 2, 3, nil)        ; => (1 2 3)

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…).
Przykłady zacytowanych list
1
2
3
4
5
6
7
8
9
;; cytowanie listy pustej
(quote ())       ; => ()
'()              ; => ()

;; cytowanie listowego S-wyrażenia
(quote (1 2 e))  ; => (1 2 e)
(quote (1,2,3))  ; => (1 2 3)
'(1 2  e)        ; => (1 2 e)
'(1,2, 3)        ; => (1 2 3)

Widzimy, że cytowanie listowego S-wyrażenia różni się od użycia formuły list tym, że nie jest dokonywane obliczanie wartości (wartościowanie) podanych elementów. Różnicę możemy zaobserwować, gdy podamy niepoprawne formuły (np. symbole, które na nic nie wskazują):

Porównanie listy zacytowanej z listą tworzoną z użyciem list
1
2
(list 1 2 c)  ; błąd – c nie jest formułą, bo symbol nie jest powiązany
'(1 2 c)      ; nowa lista – c jest formułą stałą symbolu

Wyrażenie z pierwszej linii generuje błąd, ponieważ w argumentach wywołania pojawia się symbol c, który nie jest z niczym powiązany, a więc nie można obliczyć jego wartości.

Gdybyśmy chcieli przekazać jako argument sam symbol, a nie powiązany z nim obiekt, to moglibyśmy też użyć cytowania lub odpowiedniej funkcji tworzącej symbol w jego formie stałej.

Przykłady tworzenia form stałych symbolu
1
2
(list 1 2 'c)            ; forma stała symbolu c przez zacytowanie
(list 1 2 (symbol "c"))  ; forma stała symbolu c przez użycie funkcji

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 jej pierwszy element lub wartość nil, jeśli nie znaleziono pierwszego elementu (mamy do czynienia z listą pustą).

Użycie:

  • (first lista).
Przykłady użycia funkcji first
1
2
(first (list 1 2 3))  ; => 1
(first ())            ; => nil

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ść nil, jeśli nie znaleziono ostatniego elementu (mamy do czynienia z listą pustą).

Użycie:

  • (last lista).
Przykłady użycia funkcji last
1
2
(last (list 1 2 3))  ; => 3
(last ())            ; => nil

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 jej pierwszy element lub wartość nil, jeśli nie znaleziono pierwszego elementu (mamy do czynienia z listą pustą).

Użycie:

  • (peek lista).
Przykłady użycia funkcji peek
1
2
(peek (list 1 2 3))  ; => 1
(peek ())            ; => nil

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śli nie użyto trzeciego argumentu, generowany jest wyjątek IndexOutOfBoundsException.

Użycie:

  • (nth lista numer-kolejny zastępnik?).
Przykłady użycia funkcji nth
1
2
3
4
5
6
(def lista (list 1 2 :a :b "c"))

(nth lista 0)           ; => 1
(nth lista 7)           ; >> java.lang.IndexOutOfBoundsException
(nth lista 0   "brak")  ; => 1
(nth lista 123 "brak")  ; => "brak"

Przeszukiwanie list

Wyszukiwanie wartości możemy zrealizować, korzystając z metod Javy.

Metody .indexOf.lastIndexOf

Wyszukiwanie wartości można realizować z wykorzystaniem wbudowanych metod Javy, które pobierają dwa argumenty: listę 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 lista element),
  • (.lastIndexOf lista element).
Przykłady użycia metod indexOf i lastIndexOf
1
2
3
4
(def lista '(1 2 3 4 :a 5 :a))

(.indexOf     lista :a)  ; => 4
(.lastIndexOf lista :a)  ; => 6

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

Użycie:

  • (cons element lista).
Przykłady użycia funkcji cons
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
;; dodajemy 10 do czoła listy
(cons 10 '(1 2 3))
; => (10 1 2 3)

;; peek działa dla list
(peek '(1 2 3))
; => 1

;; lecz nie dla obiektów Cons
(peek (cons 10 '(1 2 3)))
; >> ClassCastException clojure.lang.Cons cannot be cast
; >> to clojure.lang.IPersistentStack

;; typ obiektu po wywołaniu cons
(type (cons 10 '(1 2 3)))
; => clojure.lang.Cons

Dodawanie elementów, conj

Innym sposobem dodawania elementu do listy jest użycie funkcji conj (z ang. conjoin). Tworzy ona nową komórkę na bazie konkretnej kolekcji, wykorzystując polimorficzne wywoływanie metod Javy. W ten sposób dołączany jest element, a na zwracanej strukturze możemy przeprowadzać te same operacje, co na liście.

Funkcja conj przyjmuje dwa obowiązkowe argumenty. Pierwszym powinna 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.

Użycie:

  • (conj lista element & element…).
Przykłady użycia funkcji conj
1
2
3
4
5
6
7
8
9
10
11
;; dodawanie wielu elementów
(conj '(1 2 3) 10 20)
; => (20 10 1 2 3)

;; działają operacje listowe
(peek (conj '(1 2 3) 10 20))
; => 20

;; typ obiektu po wywołaniu conj
(type (conj '(1 2 3) 10))
; => clojure.lang.PersistentList

Usuwanie elementów z listy

Usuwanie elementów list możliwe jest z wykorzystaniem funkcji poprest. Ponieważ mamy do czynienia z danymi niemutowalnymi, więc oczywiście będziemy mieli do czynienia z nowymi listami, 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).
Przykłady użycia funkcji pop
1
2
3
(pop '(1 2 3))  ; => (2 3)
(pop '(1))      ; => ()
(pop ())        ; >> IllegalStateException

Uwaga: Próba użycia pop na liście pustej spowoduje wygenerowanie wyjątku IllegalStateException.

Poza pierwszym elementem, rest

Listę złożoną ze wszystkich elementów poza pierwszym uzyskamy też 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).
Przykłady użycia funkcji rest
1
2
3
(rest '(1 2 3))  ; => (2 3)
(rest '(1))      ; => ()
(rest ())        ; => ()

Cytowanie i wartościowanie list

Gdybyśmy użyli cytowania – korzystając z quote lub z lukru składniowego w postaci pojedynczego apostrofu – to chociaż wciąż mielibyśmy do czynienia z listowym S-wyrażeniem, nie byłoby ono poddane wyliczeniu i potraktowane jak formuła złożona, ale jak formuła stała:

1
'(+ 2 2)

Taką listę również można skonstruować z użyciem formuły list:

1
(list '+ 2 2)

Zacytowany został tylko symbol plusa, bo w przeciwnym razie byłby on wartościowany do obiektu funkcyjnego identyfikowanego znakiem + (operacja sumowania). Literały liczbowe zostawiliśmy, ponieważ są formułami 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 formuł, aby potem znów zmienić formułę listową na złożoną (poddawaną wyliczaniu).

Przykład zagnieżdżonych, listowych S-wyrażeń
1
2
3
(concat    ; ta funkcja połączy poniższe listy w jedną sekwencję
 '(+ 2 2)  ; pierwsza lista (formuła stała listy)
 '(3))     ; druga lista    (formuła stała listy)

To samo można oczywiście zapisać w jednej linii, a następnie sprawdzić, jaka wartość będzie zwrócona:

1
2
(concat '(+ 2 2) '(3))
; => (+ 2 2 3)

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 formułę stałą naszej listy w formułę złożoną. Ta ostatnia jest przez interpreter traktowana jako kod do wykonania i wartościowana. Ta konstrukcja to eval:

1
2
(eval (concat '(+ 2 2) '(3)))
; => 7

W wyniku otrzymaliśmy formułę stałą (taką, która sama jest własną wartością), zaprezentowaną przez REPL jako atomowe S-wyrażenie (literał liczbowy będący atomem), które wyraża wartość liczbową 7. W skrócie powiemy, że 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 struktura danych, która przypomina jednowymiarową tablicę. Służy do przechowywania danych uporządkowanych, opatrzonych wewnętrznie 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 wyrazić tę pozycję.

Wektory są strukturami danych zoptymalizowanymi pod kątem dodawania elementów do ich końca.

W przeciwieństwie do list, wektorów nie można ich używać do tworzenia formuł funkcyjnych, makrowych czy specjalnych, tzn. takich w których operator znajduje się na pierwszym miejscu i wskazuje na pewien wykonywalny podprogram.

Jeśli chodzi o przetwarzanie danych związanych z logiką aplikacji, to wektory nadają się do tego celu bardziej niż listy. W programach będzie to więc struktura danych 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) (choć tak oczekiwana logarytmiczna – Θ(log n)).

Tworzenie wektorów

Wektory można tworzyć z użyciem wektorowych S-wyrażeń, funkcji vector lub vec, albo funkcji vector-of.

Wektor z argumentów, vector

Funkcja vector przyjmuje zero lub więcej argumentów, których wartości staną się elementami wektora. Funkcja zwraca wektor (obiekt typu clojure.lang.PersistentVector).

Użycie:

  • (vector & element…).
Przykłady użycia funkcji vector
1
2
3
4
(vector 1 2 3)  ; => [1 2 3]
(vector :a, 3)  ; => [:a 3]
(vector nil)    ; => [nil]
(vector)        ; => []

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. Funkcja zwraca 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 musi być rozpakowywany (ang. unboxed) właściwy obiekt. Ma to pozytywny wpływ na szybkość dostępu do elementów kolekcji (krótszy czas).

Użycie:

  • (vector-of typ & element…).
Przykład użycia funkcji vector-of
1
2
(vector-of :int 1 2)
; => [1 2]

Wektor z sekwencji, vec

Funkcja vec działa w odniesieniu do wektorów podobnie, jak funkcja list* w odniesieniu do list. Przyjmuje dowolną kolekcję buduje zawartość na bazie jej kolejnych elementów. Funkcja zwraca wektor (obiekt typu clojure.lang.PersistentVector).

Użycie:

  • (vec napis),
  • (vec sekwencja).
Przykłady użycia funkcji vec
1
2
3
4
5
6
7
8
9
10
11
12
(vec       1 2 3)   ; => [1 2 3]
(vec     '(1 2 3))  ; => [1 2 3]
(vec (list 1 2 3))  ; => [1 2 3]
(vec  (range 1 4))  ; => [1 2 3]
(vec     #{1 2 3})  ; => [1 3 2]
(vec  {:a 1 :b 2})  ; => [[:b 2] [:a 1]]
(vec      [1 2 3])  ; => [1 2 3]
(vec "napis")       ; => [\n \a \p \i \s]
(vec "")            ; => []
(vec nil)           ; => []
(vec [])            ; => []
(vec ())            ; => []

Wektorowe S-wyrażenia

Wektorowe S-wyrażenie (ang. vector S-expression) to element składniowy pozwalający w przejrzysty sposób tworzyć wektory. Składa się z zestawu elementów ujętych w nawiasy kwadratowe lub z samych nawiasów kwadratowych (w przypadku wektorów pustych).

Jeżeli podane elementy są formułami innymi niż stałe, to będą wartościowane, a w wektorze zostaną umieszczone rezultaty obliczeń.

Wektorowe S-wyrażenia znajdują częste składniowe zastosowanie podczas tworzenia powiązań dynamicznychleksykalnych (włączając w to wektory parametryczne definiowanych funkcji i wektory powiązaniowe).

Użycie:

  • [],
  • [element…].
Przykłady wektorowych S-wyrażeń
1
2
3
4
[1 2 3]      ; => [1 2 3]
[1, 2, 3]    ; => [1 2 3]
[1 (+ 2 1)]  ; => [1 3]
[]           ; => []

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ą formułami stałymi (niewyrażającymi wartości własnych).

Użycie:

  • '[],
  • '[element…],
  • (quote []),
  • (quote [element…]).
Przykłady cytowania wektorowych S-wyrażeń
1
2
3
4
'[]                  ; => []
'[:a x :b y]         ; => [:a x :b y]
(quote [])           ; => []
(quote [:a x :b y])  ; => [:a x :b y]

Dostęp do wektorów

Pobieranie elementu o określonej lokalizacji (o określonym numerze kolejnym) możliwe jest z użyciem get, nth, first, last, peek, albo przez użycie obiektu wektora jako 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śli nie znaleziono pierwszego elementu (mamy do czynienia z wektorem pustym).

Użycie:

  • (first wektor).
Przykłady użycia funkcji first
1
2
(first [1 2 3])  ; => 1
(first [])       ; => nil

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śli nie znaleziono ostatniego elementu (mamy do czynienia z wektorem pustym).

Użycie:

  • (last wektor).
Przykłady użycia funkcji last
1
2
(last [1 2 3])  ; => 3
(last [])       ; => nil

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ść nil, jeśli 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, tu ostatni).

Użycie:

  • (peek wektor).
Przykłady użycia funkcji peek
1
2
(peek [1 2 3])  ; => 3
(peek [])       ; => nil

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, to jego wartość zostanie zwrócona, w przeciwnym wypadku zwrócona będzie wartość nil lub wartość przekazana jako opcjonalny, trzeci argument.

Przykłady użycia funkcji get
1
2
3
(get [1 2 3] 2)       ; => 3
(get [1 2 3] 5)       ; => nil
(get [1 2 3] 5 :nic)  ; => :nic

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 indeksie 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śli nie użyto trzeciego argumentu, generowany jest wyjątek IndexOutOfBoundsException.

Użycie:

  • (nth wektor numer-kolejny zastępnik?).
Przykłady użycia funkcji nth
1
2
3
4
5
6
(def wektor [1 2 :a :b "c"])

(nth wektor   0)         ; => 1
(nth wektor   7)         ; >> błąd java.lang.IndexOutOfBoundsException
(nth wektor   0 "brak")  ; => 1
(nth wektor 123 "brak")  ; => "brak"

Dzielenie wektorów

Dzięki funkcji subvec możemy dokonywać podziału wektorów na mniejsze wektory.

Wydzielanie zakresu, subvec

Funkcja subvec tworzy wektor z elementów istniejącego o podanym zakresie. 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, to zgłoszony zostanie wyjątek IndexOutOfBoundsException.

Użycie:

  • (subvec wektor początek koniec?).
Przykłady użycia funkcji subvec
1
2
3
4
5
6
7
8
9
10
(def wektor [1 2 3 4])

(subvec wektor 0  )  ; => [1 2 3 4]
(subvec wektor 1  )  ; => [2 3 4]
(subvec wektor 2  )  ; => [3 4]
(subvec wektor 0 1)  ; => [1]
(subvec wektor 0 2)  ; => [1 2]
(subvec wektor 1 2)  ; => [2]
(subvec wektor 2 3)  ; => [3]
(subvec wektor 2 2)  ; => []

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, to wartości te zostaną umieszczone w wynikowym wektorze.

Użycie:

  • (replace wektor wektor-indeksów).
Przykłady użycia funkcji replace
1
2
3
4
5
(replace [0 "raz" "dwa" "trzy"] [1 2 3])
; => ["raz" "dwa" "trzy"]

(replace [0 "raz" "dwa" "trzy"] [1 2 :abc])
; => ["raz" "dwa" :abc]

Przeszukiwanie wektorów

Przeszukiwanie indeksów wektorów możliwe jest z użyciem funkcji contains?, z kolei wyszukiwanie wartości możemy zrealizować z wykorzystaniem metod Javy.

Wyszukiwanie indeksu, contains?

Sprawdzania czy element o podanym numerze kolejnym istnieje w wektorze można dokonać z wykorzystaniem funkcji contains?. Przyjmuje ona dwa argumenty: pierwszym powinien być wektor, a drugim numer kolejny poszukiwanego elementu (licząc od 0). Funkcja zwraca true, jeśli element o podanym numerze indeksu istnieje, a false w przeciwnym razie.

Użycie:

  • (contains? wektor indeks).
Przykłady użycia funkcji contains?
1
2
(contains? [1 2 3] 2)  ; => true
(contains? [1 2 3] 5)  ; => false

Metody .indexOf.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).
Przykłady użycia metod indexOf i lastIndexOf
1
2
3
4
(def wektor [1 2 3 4 :a 5 :a])

(.indexOf     wektor :a)  ; => 4
(.lastIndexOf wektor :a)  ; => 6

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.

Łączenie wektorów

Do łączenia zawartości wektorów służy funkcja into.

Funkcja into

Funkcja into przyjmuje dwa argumenty, którymi powinny być wektory, a zwraca wektor będący ich złączeniem.

Użycie:

  • (into wektor-docelowy wektor-źródłowy).
Przykład użycia funkcji into
1
2
(into [1 2 3 4] [5 6 7 8])
; => [1 2 3 4 5 6 7 8]

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ść).
Przykład użycia funkcji assoc
1
2
(assoc [1 2 3 4] 1 "dwa")
; => [1 "dwa" 3 4]

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) ostatniego elementu ścieżki.

Funkcja assoc-in zwraca wektor.

Użycie:

  • (assoc-in wektor ścieżka wartość).
Przykłady użycia funkcji assoc-in
1
2
3
4
5
6
7
8
(assoc-in [1 2 3 4] [1] "dwa")
; => [1 "dwa" 3 4]

(assoc-in [1 [1 2] 3 4] [1 1] "dwa")
; => [1 [1 "dwa"] 3 4]

(assoc-in [1 {:a [1 2]} 3 4] [1 :a 1] "dwa")
; => [1 {:a [1 "dwa"]} 3 4]

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ść. Wartość ta zostanie użyta do aktualizacji wartości elementu.

Funkcja update zwraca nowy wektor.

Użycie:

  • (update wektor klucz operator).
Przykład użycia funkcji update
1
2
3
4
(update [1 2 3 4]  ; aktualizowana struktura
        1 inc)     ; określenie elementu i operacji

; => [1 3 3 4]

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).
Przykłady użycia funkcji update-in
1
2
3
4
5
6
7
8
9
10
11
12
;; nazywamy wektor
(def w [1 2 3 4])

;; drugi argument to nr indeksu,
;; a trzeci funkcja do zastosowania (inc)
(update-in w [1] inc)
; => [2 2 3 4]

;; drugi argument to nr indeksu,
;; trzeci funkcja, która zawsze zwraca 5
(update-in w [1] (constantly 5))
; => [5 2 3 4]

Warto zwrócić uwagę na użycie funkcji constantly. 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. Używamy jej, ponieważ update-in wymaga, aby trzecim argumentem była funkcja, a nie wartość – potrzebujemy więc funkcji, która zwraca wartość stałą.

Dodawanie elementów do wektorów

Dodawanie na koniec, conj

Tworzenie wektora pochodnego z dodanym elementem na końcu możliwe jest dzięki conj. 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 przekazywania.

Użycie:

  • (conj wektor element & element…).
Przykłady użycia funkcji conj
1
2
3
4
(def wektor [1 2 3 4])

(conj wektor 5)     ; => [1 2 3 4 5]
(conj [1 2 3] 4 5)  ; => [1 2 3 4 5] 

Dołączanie do końca, into

Dobrym sposobem dodawania 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).
Przykład użycia funkcji into
1
2
(into [1] [2 3 4 5])
; => [1 2 3 4 5]

Dodawanie wartości, assoc

Tworzenie wektora pochodnego z dodaną wartością 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ść).
Przykład użycia funkcji assoc
1
2
(assoc [1 2 3 4] 4 5)
; => [1 2 3 4 5]

Dodawanie zagnieżdżonej, assoc-in

Tworzenie wektora pochodnego z dodaną 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 wartość dla elementu o podanym numerze kolejnym (licząc od 0) ostatniego elementu ścieżki. 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-in zwraca wektor.

Użycie:

  • (assoc-in wektor ścieżka wartość).
Przykłady użycia funkcji assoc-in
1
2
3
4
5
6
7
8
(assoc-in [1 2 3 4] [4] 5)
; => [1 2 3 4 5]

(assoc-in [1 [1 2] 3 4] [1 2] "trzy")
; => [1 [1 2 "trzy"] 3 4]

(assoc-in [1 {:a [1 2]} 3 4] [1 :a 2] "trzy")
; => [1 {:a [1 2 "trzy"]} 3 4]

Usuwanie elementów z wektorów

Usuwanie elementów wektorów możliwe jest z wykorzystaniem funkcji pop, rest i jednoczesnego zastosowania subvec wraz z funkcją łączącą 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 PersistentVector (nowy wektor).

Użycie:

  • (pop wektor).
Przykłady użycia funkcji pop
1
2
3
(pop [1 2 3])  ; => (2 3)
(pop [1])      ; => ()
(pop [])       ; >> IllegalStateException

Uwaga: Próba użycia pop na wektorze pustym spowoduje wygenerowanie wyjątku IllegalStateException.

Poza pierwszym elementem, rest

Wektor złożony ze wszystkich elementów poza pierwszym uzyskamy również 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).
Przykłady użycia funkcji rest
1
2
3
(rest [1 2 3])  ; => (2 3)
(rest [1])      ; => ()
(rest [])       ; => ()

Usuwanie wybranego elementu

Nowy wektor bez wybranego elementu możemy stworzyć z wykorzystaniem kombinacji funkcji concatsubvec lub intosubvec.

Użycie:

  • (concat & wektor…),
  • (subvec wektor początek koniec?).
Usuwanie elementu wektora z użyciem funkcji concat i subvec
1
2
3
4
5
(def drugi [1 2 3 4])  ; nazywamy wektor
(concat                ; tworzymy sekwencję z połączenia
 (subvec drugi 0 2)    ;  · części wektora przed elementem nr 2
 (subvec drugi 3))     ;  · części wektora po elemencie nr 2
; => (1 2 4)

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

Usuwanie elementu wektora z użyciem funkcji into i subvec
1
2
3
(def wektor [1 2 3 4])
(into (subvec wektor 0 2) (subvec wektor 3))
; => [1 2 4]

Korzystając z biblioteki Criterium, porównajmy prędkości usuwania pojedynczego elementu dla przedstawionych strategii.

Porównanie wydajności strategii usuwania elementów z wektorów
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
(use 'criterium.core)

;; concat

(let [wektor (vec (range 1 400))]
  (bench
   (concat (subvec wektor 0 350) (subvec wektor 351))))

;; concat + vec

(let [wektor (vec (range 1 400))]
  (bench
   (vec (concat (subvec wektor 0 350) (subvec wektor 351)))))

;; into

(let [wektor2 (vec (range 1 400))]
  (bench
   (into (subvec wektor2 0 350) (subvec wektor2 351))))

Rezultaty:

  • użycie concat: 335 ns;
  • użycie concatvec: 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 (wspomniana przekazywana funkcja) musi przyjmować tyle argumentów, ile wektorów zostało podanych.

Użycie:

  • (mapv operator wektor & wektor…).
Przykład użycia funkcji mapv
1
2
(mapv + [1 2 3 4] [10 20 30 40])
; => [11 22 33 44]

Można oczywiście jako operatora użyć funkcji jednoargumentowej i jednego zestawu danych wejściowych, jeśli istnieje taka potrzeba:

1
2
(mapv inc [1 2 3 4])
; => [2 3 4 5]

Filtrowanie, filterv

Funkcja filterv jako pierwszy argument przyjmuje funkcję warunkującą, a jako drugi wektor. Przekazana funkcja, zwana predykatem, jest wywoływana dla każdej kolejnej wartości przekazanego wektora. Jeśli zwracana przez nią wartość jest różna od false i różna od nil, to dany element jest umieszczany w zwracanym wektorze.

Użycie:

  • (filterv predykat wektor).
Przykład użycia funkcji filterv
1
2
(filterv even? [1 2 3 4])
; => [2 4]

Redukowanie, reduce

Funkcja reduce jako pierwszy argument przyjmuje operator, który powinien być użyty względem kolejnych elementów wektora (podanego jako drugi argument), w taki sposób, że wynik poprzedniego zastosowania operatora jest akumulowany i używany jako pierwszy z argumentów w odniesieniu do jego kolejnego wywołania; drugim argumentem jest aktualnie przetwarzany element.

Funkcja reduce zwraca wartość ostatniego wywołania operatora na ostatnim elemencie wektora i zakumulowanej wartości.

Użycie:

  • (reduce operator wektor).
Przykład użycia funkcji reduce
1
2
(reduce + [1 2 3 4])  ; sumowanie elementów
; => 10

Redukowanie z indeksem, reduce-kv

Funkcja reduce-kv użyta w odniesieniu do wektorów działa podobnie do reduce, z tą jednak różnicą, że wywołuje podany jako pierwszy argument operator dla każdego elementu i jego numeru indeksu (poza akumulatorem i jego wartością), a poza tym przyjmuje argument, który służy do zainicjowania akumulatora używanego do pamiętania kolejnych wartości.

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).
Przykład użycia funkcji reduce-kv
1
2
(reduce-kv + 0 [1 2 3 4])
; => 16

Możemy sami sprawdzić jak wyglądają kolejne wywołania, tworząc sumator, który poza wyliczeniem wyświetli przyjmowane argumenty:

Przykład sumatora na bazie funkcji reduce-kv
1
2
3
4
5
(defn dodaj [a b c]
  (println "(+" a b (str c ")"))
  (+ a b c))

(reduce-kv dodaj 0 [1 2 3 4])

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.

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).
Przykłady użycia funkcji rseq
1
2
3
4
5
6
7
8
9
10
11
;; odwrócona kolejność (sekwencja)
(rseq [1 2 3 4])
; => (4 3 2 1)

;; odwrócona kolejność (wektor)
(vec (rseq [1 2 3 4]))
; => [4 3 2 1]

;; odwrócona kolejność (wektor)
(into [] (rseq [1 2 3 4]))
; => [4 3 2 1]

Zobacz także:

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śli to możliwe, ze słów kluczowych lub wypakowanych (ang. unboxed) [typów numerycznych][liczby].

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.

Przykład symbolicznie wyrażonej mapy
1
{ :a 1, :b 2, :c 3 }

Kluczami mogą być dane dowolnych typów, nie tylko słowa kluczowe.

Przykład mapy z różnymi typami wartości i kluczy
1
{ 'raz 1, 'dwa 2, "trzy" 3, :lista '(1 2 3) }

Wartościami map mogą być też inne mapy. Dzięki temu można tworzyć zagnieżdżone struktury.

Przykład zagnieżdżonych mapowych S-wyrażeń
1
2
{:zakupy { :chleb 1,    :mleko 1,   :cukier 1 }
 :wagi   { :chleb 0.45, :mleko 1.1, :cukier 1 }}

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ć mapy 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ść.
Przykłady użycia funkcji hash-map
1
2
3
(hash-map :a "taki"  :b 2)  ; => {:a "taki" :b 2}
(hash-map :b 2, :a "taki")  ; => {:a "taki" :b 2}
(hash-map)                  ; => {}

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ść.
Przykłady użycia funkcji sorted-map
1
2
3
(sorted-map :a "taki"  :b 2)  ; => {:a "taki" :b 2}
(sorted-map :b 2, :a "taki")  ; => {:a "taki" :b 2}
(sorted-map)                  ; => {}

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 pierwszego argumentu powinna być wcześniejsza, równa czy większa od pozycji 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ść.
Przykłady użycia funkcji sorted-map-by
1
2
3
(sorted-map-by compare 1 "taki" 2 "inny")  ; => {1 "taki" 2 "inny"}
(sorted-map-by >       1 "taki" 2 "inny")  ; => {2 "inny" 1 "taki"}
(sorted-map)                               ; => {}

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ść…).
Przykłady użycia funkcji array-map
1
2
3
(array-map :a "taki"  :b 2)  ; => {:a "taki" :b 2}
(array-map :b 2, :a "taki")  ; => {:a "taki" :b 2}
(array-map)                  ; => {}

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).
Przykłady użycia funkcji zipmap
1
2
3
(zipmap [:a :b] [1 2])  ; => {:a 1 :b 2}
(zipmap [] [])          ; => {}
(zipmap nil nil)        ; => {}

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).
Przykłady użycia funkcji group-by
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
;; grupuj nieparzyste elementy wektora
(group-by odd? [1 2 3 4 5])
; => {true [1 3 5], false [2 4]}

;; grupuj po tożsamości
(group-by identity [1 2 3 4 5])
; => {1 [1] 2 [2] 3 [3] 4 [4] 5 [5]}

;; grupuj po podzielności bez reszty
(group-by #(filter (comp zero? (partial mod %)) (range 1 (+ 1 (/ % 2))))
          [1 2 3 4 5 6 7 8 9 10 11 12])
; => {(1)         [1 2 3 5 7 11]
; =>  (1 2)       [4]
; =>  (1 2 3)     [6]
; =>  (1 2 3 4 6) [12]
; =>  (1 2 4)     [8]
; =>  (1 2 5)     [10]
; =>  (1 3)       [9]}

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).
Przykłady użycia funkcji frequencies
1
2
3
4
(frequencies [:a, :b, :a])    ; => {:b 1 :a 2}
(frequencies ["a" "b" ":a"])  ; => {"b" 1 "a" 2}
(frequencies [])              ; => {}
(frequencies nil)             ; => {}

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.

Przykład użycia funkcji bean
1
2
3
4
5
6
7
8
9
10
11
12
13
14
(import java.util.Calendar)
(pprint (bean (.getTime (Calendar/getInstance))))
; => nil
; >> 
; >> {:day 3,
; >>  :date 3,
; >>  :time 1433340899605,
; >>  :month 5,
; >>  :seconds 59,
; >>  :year 115,
; >>  :class java.util.Date,
; >>  :timezoneOffset -120,
; >>  :hours 16,
; >>  :minutes 14}

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ą formułami innymi niż stałe, to będą wartościowane, a w mapie zostaną umieszczone rezultaty obliczeń.

Rezultatem obliczenia mapowego S-wyrażenia 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ą częste składniowe zastosowanie 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ść.
Przykłady mapowych S-wyrażeń
1
2
3
4
{:a "taki"  :b "inna"}  ; => {:a "taki" :b "inna"}
{:a "taki", :b "inna"}  ; => {:a "taki" :b "inna"}
{:a (inc 2) :b 4}       ; => {:a 3 :b 4}
{}                      ; => {}

Zacytowane mapowe S-wyrażenia

Mapowe S-wyrażenia mogą być cytowane. W takim przypadku podawane wartości i klucze nie będą wartościowane, nawet jeżeli są formułami, które nie wyrażają wartości własnych.

Użycie:

  • '{},
  • '{klucz–wartość…},
  • (quote {}),
  • (quote {klucz–wartość…}),

gdzie klucz–wartość to:

  • klucz wartość.
Przykłady cytowania mapowych S-wyrażeń
1
2
3
4
5
'{}                   ; => {}
'{:a x, :b y}         ; => {:a x, :b y}
'{:a (inc 2), :b y}   ; => {:a (inc 2), :b y}
(quote {})            ; => {}
(quote {:a x, :b y})  ; => {:a x, :b y}

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ść nil, jeśli pierwszy element nie istnieje.

Użycie:

  • (first mapa).
Przykłady użycia funkcji first
1
2
(first {:a 1, :b 2})  ; => [:b 2]
(first           {})  ; => nil

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 wyrażana jako wektor. W istocie jest to pojedynczy element mapy, o którego typie możemy się przekonać samodzielnie:

1
2
(type (first {:a 1, :b 2, :c 3}))
; => clojure.lang.MapEntry

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ść nil, jeśli ostatni element nie istnieje.

Użycie:

  • (last mapa).
Przykłady użycia funkcji last
1
2
3
(last {:a 1, :b 2})                      ; => [:a 1]
(last (into (sorted-map) {:a 1, :b 2}))  ; => [:b 2]
(last {})                                ; => nil

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 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ść nil, jeśli klucza nie znaleziono. Jeżeli podano trzeci, opcjonalny argument, to zamiast nil zwrócona będzie jego wartość.

Użycie:

  • (get mapa klucz wartość-domyślna?).
Przykład użycia funkcji get
1
2
3
4
5
(get {:klucz "wartość", :inny "druga"} :klucz)
; => "wartość"

(get {:a 1 :b 2} :c "nie ma")
; => "nie ma"

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ść nil, jeśli sekwencji kluczy nie znaleziono. Jeżeli podano trzeci, opcjonalny argument, to jego wartość będzie zwrócona zamiast nil.

Użycie:

  • (get-in mapa wektor-ścieżki wartość-domyślna?).
Przykłady użycia funkcji get-in
1
2
(get-in {:zakupy {:chleb 1, :mleko 2} } [:zakupy :chleb])  ; => 1
(get-in {:zakupy {:chleb 1, :mleko 2} } [:a :b]  :nic)     ; => :nic

Wartość elementu, val

Pobieranie wartości dla elementu 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ść.

Przykład użycia funkcji val
1
2
(val (first {:a 1, :b 2}))
; => 2

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.

Przykład użycia funkcji vals
1
2
(vals {:a 1, :b 2, :c 3})
; => (3 2 1)

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.

Przykład użycia funkcji key
1
2
(key (first {:a 1, :b 2}))
; => :b

Wszystkie klucze, keys

Na pobieranie wszystkich kluczy pozwala funkcja keys.

Użycie:

  • (keys mapa).

Funkcja przyjmuje jeden argument (mapę), a zwraca sekwencję kluczy.

Przykład użycia funkcji keys
1
2
(keys {:a 1, :b 2, :c 3})
; => (:c :b :a)

Mapa jako funkcja

Obiekty map wyposażone są w funkcyjny interfejs, tzn. są również specyficznymi funkcjami. Przyjmują one jeden obowiązkowy argument, który oznacza element identyfikowany kluczem, a zwracają jego wartość (podobnie jak w przypadku get). Jeżeli element o podanym kluczu nie istnieje w mapie, zwracana jest wartość nil, chyba że jako drugi argument podano wartość domyślną (w takim przypadku zwracana jest właśnie ona).

Użycie:

  • (mapa klucz).
Przykłady użycia mapy jako funkcji
1
2
3
4
({:a 1, :b 2, :c 3} :a)         ; => 1
({:a 1, :b 2, :c 3} :d)         ; => nil
({:a 1, :b 2, :c 3} :d "brak")  ; => "brak"
(let [mapa {:a 1}] (mapa :a))   ; => 1

Słowo kluczowe jako funkcja

Słowa kluczowe implementują interfejs IFn, czyli są funkcjami. W tej formie znajdują zastosowanie przy pobieraniu wartości elementów map – oczywiście pod warunkiem, że kluczami tych map są słowa kluczowe.

Słowa kluczowe w formie funkcji 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, to zwracana jest wartość nil, chyba że przekazano drugi argument – w takim przypadku zwracana jest jego wartość.

Użycie:

  • (słowo-kluczowe mapa wartość-domyślna?).
Przykłady użycia słów kluczowych jako funkcji
1
2
3
4
(:a {:a 1, :b 2, :c 3})        ; => 1
(:e {:a 1, :b 2, :c 3})        ; => nil
(:e {:a 1, :b 2, :c 3} :x )    ; => :x
(let [mapa {:a 1}] (:a mapa))  ; => 1

Przeszukiwanie map

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 nil, jeśli element nie został znaleziony.

Przykłady użycia funkcji find
1
2
(find {:chleb 1, :mleko 2} :mleko)    ; => [:mleko 2]
(find {:chleb 1, :mleko 2} :cytryna)  ; => nil

Wyszukiwanie klucza, contains?

Aby sprawdzić, czy podany klucz istnieje, należy użyć funkcji contains?. Przyjmuje ona dwa obowiązkowe argumenty: mapę i poszukiwany klucz, a zwraca wartość true, jeśli element o podanym kluczu istnieje. W przeciwnym razie zwraca wartość false.

Użycie:

  • (contains? mapa klucz).
Przykład użycia funkcji contains?
1
2
(contains? { :klucz "wartość" } :klucz)         ; czy mapa zawiera :klucz?
; => true                                       ; tak, zawiera

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ść.
Przykład użycia funkcji assoc
1
2
(assoc { :klucz "wartość" } :b 2)
; => {:b 2, :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ą, to zostaną utworzone.

Użycie:

  • (assoc-in mapa ścieżka wartość).
Przykład użycia funkcji assoc-in
1
2
(assoc-in { :korzeń { :gałązka 1 } } [:korzeń :druga] 3)
; => {:korzeń {:druga 3, :gałązka 1}}

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…).
Przykład użycia funkcji dissoc
1
2
(dissoc { :a 1, :b 2, :c 3 } :b :c)  ; usunięcie el. o kluczach :b i :c
; => {:a 1}

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).
Przykład użycia funkcji dissoc-in
1
2
(dissoc-in { :korzeń { :gałązka 1 :druga 3 } } [:korzeń :gałązka])
; => {:korzeń {:druga 3}}

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ść.
Przykład użycia funkcji assoc
1
2
(assoc { :klucz "wartość" } :klucz "nowa")
; => {:klucz "nowa"}

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ść).
Przykład użycia funkcji assoc-in
1
2
3
(assoc-in {:zakupy {:cukier 1}}  ; zmiana wartości klucza
          [:zakupy :cukier] 2)   ; określonego ścieżką kluczy
; => {:zakupy {:cukier 2}} 

Jeśli klucze struktury pośredniej nie istnieją, to 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:

Przykład użycia funkcji assoc-in
1
2
3
4
5
(assoc-in [{:imię "Ruprecht" :wiek 19}
           {:imię "Bożydar"  :wiek 20}]  ; aktualizowana struktura
          [ 1 :wiek ] 22)                ; określenie ścieżki kluczy i wartości

; => [{:wiek 19, :imię "Ruprecht"} {:wiek 22, :imię "Bożydar"}]

Przeliczenie wartości, update

Przeliczenie 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).
Przykład użycia funkcji update
1
2
3
4
(update {:imię "Ruprecht" :wiek 19}  ; aktualizowana struktura
         :wiek inc)                  ; określenie klucza i operacji

; => {:imię "Ruprecht", :wiek 20}

Przeliczenie zagnieżdżonej, update-in

Przeliczenie 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).
Przykład użycia funkcji update-in
1
2
3
4
5
(update-in [{:imię "Ruprecht" :wiek 19}
            {:imię "Bożydar"  :wiek 20}]  ; aktualizowana struktura
           [ 1 :wiek ] inc)               ; określenie ścieżki kluczy i operacji

; => [{:imię "Ruprecht", :wiek 19} {:imię "Bożydar", :wiek 21}]

Przekształcanie map sortowanych

Mapy sortowane zachowują kolejność elementów, a więc możemy na nich wykonywać operacje, które zarządzają uporządkowaniem. Należą do nich rseq, subseqrsubseq.

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.

Użycie:

  • (rseq mapa-sortowana).
Przykłady użycia funkcji rseq
1
2
3
4
5
6
(rseq (sorted-map :a 1, :b 2, :c 3))
; => ([:c 3] [:b 2] [:a 1])

;; rezultat do mapy
(into {} (rseq (sorted-map :a 1, :b 2, :c 3)))
; => {:c 3 :b 2 :a 1}

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 asocjacyjnym sposobem dostępu. Jeśli 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, to 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ą: <, <=, >>.

Użycie:

  • (subseq mapa-sortowana test wartość),
  • (subseq mapa-sortowana test-start wartość-start test-stop wartość-stop).
Przykłady użycia funkcji subseq
1
2
3
4
5
6
7
;; zakres jednostronny
(subseq (sorted-map :a 1, :b 2, :c 3) <= :b)
; => ([:a 1] [:b 2])

;; zakres dookreślony
(subseq (sorted-map :a 1, :b 2, :c 3) > :a <= :b)
; => ([:b 2])

Odwrócona sek. z zakresu, rsubseq

Funkcja rsubseq działa jak połączenie rseqsubseq, 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ą: <, <=, >>.

Użycie:

  • (rsubseq mapa-sortowana test wartość),
  • (rsubseq mapa-sortowana test-start wartość-start test-stop wartość-stop).
Przykłady użycia funkcji rsubseq
1
2
3
4
5
6
7
8
9
10
11
;; zakres jednostronny
(rsubseq (sorted-map :a 1, :b 2, :c 3) <= :b)
; => ([:b 2] [:a 1])

;; zakres dookreślony
(rsubseq (sorted-map :a 1, :b 2, :c 3) > :a <= :c)
; => ([:c 3] [:b 2])

;; konwersja do mapy
(into {}  (rsubseq (sorted-map :a 1, :b 2, :c 3) <= :b))
; => {:b 2 :a 1}

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).
Przykład użycia funkcji clojure.set/rename-keys
1
2
(clojure.set/rename-keys {:a 1, :b 2} {:a :x})
; => {:x 1, :b 2}

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).
Przykład użycia funkcji map-invert
1
2
(map-invert {:a 1, :b 2, :c 1})
; => {2 :b, 1 :a}

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…).
Przykład użycia funkcji merge
1
2
(merge {:a 1 :b 2} {:b 10 :c 3})
; => {:c 3, :b 10, :a 1}

Złączenie z przesłanianiem, map-str

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

Przyjmuje ona jeden obowiązkowy argument (wspomnianą 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 map-str 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…).
Przykłady użycia funkcji merge-with
1
2
3
4
5
6
7
8
(merge-with str {:a "a", :b "b"} {:c "c"})
; => {:c "c", :b "b", :a "a"}

(merge-with str {1 8, :a "a", :b "b"} {:c "c", :a "X"} {2 2})
; => {2 2, :c "c", 1 8, :b "b", :a "aX"}

(merge-with + {:a 1, :b 2} {:c 3, :a 9})
; => {:c 3, :b 2, :a 10}

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.

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).
Przykład użycia funkcji select-keys
1
2
(select-keys {:a 1, :b 2, :c 3, :d 4} [:a :d])
; => {:d 4, :a 1}

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 (Def clojure.lang.PersistentStructMap).

Użycie:

  • (create-struct klucz & klucz…).
Przykład użycia funkcji create-struct
1
2
(create-struct :jabłka :banany :chleby :mleka)
; => #<Def [email protected]>

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 (Def clojure.lang.PersistentStructMap).

Wewnętrznie funkcja defstruct wywołuje def na rezultacie create-struct.

Użycie:

  • (defstruct symbol klucz & klucz…).
Przykład użycia funkcji defstruct
1
2
(defstruct zakupy :jabłka :banany :chleby :mleka)
; => #'user/zakupy

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 definicja struktury i argumenty opcjonalne,

Funkcja zwraca mapę strukturalizowaną (obiekt typu clojure.lang.PersistentStructMap).

Użycie:

  • (struct-map struktura & klucz–wartość…),

gdzie klucz–wartość to:

  • klucz wartość.
Przykład użycia funkcji struct-map
1
2
3
(defstruct  zakupy :jabłka   :banany   :chleby  :mleka)
(struct-map zakupy :jabłka 2 :banany 4 :chleby 2)
; => {:jabłka 2, :banany 4, :chleby 2, :mleka nil}

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ść…).
Przykłady tworzenia map strukturalizowanych
1
2
3
(defstruct zakupy :jabłka :banany :chleby :mleka)
(struct    zakupy 2 4 2)
; => {:jabłka 2, :banany 4, :chleby 2, :mleka nil}

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 innych mapach, włączając w to dynamiczne dodawanie kluczy, które nie zostały zdefiniowane w bazowej strukturze. Jest tylko jeden wyjątek: nie można usuwać elementów, których klucze zdefiniowano w strukturze.

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).
Przykład użycia funkcji set
1
2
3
4
5
6
7
(set [1 2 3 4 :a :b :c])  ; jeden argument, który jest kolekcją
(set           '(1 2 3))  ; inicjowany zacytowaną listą
(set       (list 1 2 3))  ; inicjowany listą
(set         #{1 2 3})    ; inicjowany innym zbiorem
(set        {:a 1 :b 2})  ; inicjowany parami mapy (jako wektory)
(set           "abcdef")  ; inicjowany łańcuchem znakowym (sekwencja liter)
(set                nil)  ; zbiór pusty

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…).
Przykłady użycia funkcji hash-set
1
2
(hash-set 1 2 3 4)  ; wartości argumentów zostaną umieszczone w zbiorze
(hash-set        )  ; zbiór pusty

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…).
Przykład użycia funkcji sorted-set
1
2
(sorted-set 4 3 2 1)  ; jak hash-set, ale zbiór będzie posortowany
(sorted-set        )  ; jak sorted-set, ale zbiór pusty

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ć wcześniejsza, 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…).
Przykłady użycia funkcji sorted-set-by
1
2
(sorted-set-by > 1 8 4)  ; => #{1 4 8}
(sorted-set-by > 1 8 4)  ; => #{}

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ą formułami innymi niż stałe, to będą wartościowane, a w zbiorze zostaną umieszczone rezultaty obliczeń.

Użycie:

  • #{},
  • #{element…}.
Przykłady użycia zbiorowego S-wyrażenia
1
2
3
#{}                       ; zbiór pusty
#{1 2 3 4 :a :b :c}       ; lukier syntaktyczny dla hash-set
#{1, 2, 3, 4, :a, :b :c}  ; możemy też korzystać z przecinków

Zacytowane zbiorowe S-wyrażenia

Wyrażenia zbiorowe mogą być również cytowane. W takim przypadku podawane elementy nie będą wartościowane, nawet jeżeli są formułami, które nie wyrażają wartości własnych.

Użycie:

  • '#{},
  • '#{element…},
  • (quote #{}),
  • (quote #{element…}).
Przykłady cytowania zbiorowych S-wyrażeń
1
2
3
4
'{}                   ; => {}
'{:a x, :b y}         ; => {:a x, :b y}
(quote {})            ; => {}
(quote {:a x, :b y})  ; => {:a x, :b y}

Dostęp do zbiorów

Pobieranie elementu, get

Sprawdzania czy element istnieje w zbiorze można dokonać z wykorzystaniem funkcji get.

Użycie:

  • (get zbiór wartość wartość-domyślna?).

Funkcja przyjmuje dwa obowiązkowe argumenty: pierwszym powinien być zbiór, a drugim poszukiwany element. Jeżeli element znajduje się w zbiorze, to jego wartość zostanie zwrócona. W przeciwnym wypadku zwrócona będzie wartość nil lub wartość przekazana jako opcjonalny, trzeci argument.

Przykłady użycia funkcji get
1
2
3
(get #{1,2,3} 2)       ; => 2
(get #{1,2,3} 5)       ; => nil
(get #{1,2,3} 5 :nic)  ; => :nic

Zbiór jako funkcja

Obiekty zbiorów wyposażone są w funkcyjny interfejs, tzn. są również specyficznymi funkcjami. Przyjmują one jeden argument wyrażający wartość poszukiwanego elementu (podobnie jak get) i zwracają tę wartość, jeśli znajduje się w zbiorze. W przeciwnym przypadku zwracana jest wartość nil.

Użycie:

  • (zbiór wartość).
Przykłady użycia zbioru jako funkcji
1
2
3
(#{1,2,3} 2)                    ; => 2
(#{1,2,3} 5)                    ; => nil
(let [zbiór #{1 2}] (zbiór 1))  ; => 1

Słowo kluczowe jako funkcja

Słowa kluczowe implementują interfejs IFn, czyli są również funkcjami. W tej formie znajdują zastosowanie przy pobieraniu wartości zbiorów, oczywiście pod warunkiem, że są to słowa kluczowe.

Słowa kluczowe w formie funkcji przyjmują jeden obowiązkowy argument (zbiór), a zwracają wartość elementu, jeżeli został znaleziony w zbiorze. Jeżeli element nie istnieje, zwracana jest wartość nil, chyba że przekazano drugi argument – w takim przypadku zwracana jest jego wartość.

Użycie:

  • (słowo-kluczowe zbiór wartość-domyślna?).
Przykłady użycia słów kluczowych jako funkcji
1
2
3
4
(:a #{:a :b :c})                   ; => :a
(:e #{:a :b :c})                   ; => nil
(:e #{:a :b :c} :x)                ; => :x
(let [zbiór :a #{:a}] (:a zbiór))  ; => :a

Przeszukiwanie zbiorów

Wyszukiwanie elementu, contains?

Sprawdzania czy element istnieje w zbiorze można dokonać z wykorzystaniem funkcji contains?. Przyjmuje ona dwa argumenty: pierwszym powinien być zbiór, a drugim poszukiwana wartość elementu. Funkcja zwraca true, jeśli podany element istnieje w zbiorze, a false w przeciwnym razie.

Użycie:

  • (contains? zbiór wartość).
Przykłady użycia funkcji contains?
1
2
(contains? #{1,2,3} 2)  ; => true
(contains? #{1,2,3} 5)  ; => false

Dodawanie i usuwanie elementów

Dodawanie elementów, conj

Dodawanie elementów (tworzenie zbioru z dodanymi nowymi elementami) można zrealizować z użyciem funkcji conj.

Użycie:

  • (conj zbiór element & element…).

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

Przykład użycia funkcji conj
1
2
(conj #{1,2,3} 5 6 1)
; => #{1 6 3 2 5}

Usuwanie elementów, disj

Usuwanie elementu (tworzenie zbioru z usuniętymi wybranymi elementami) polega na wywołaniu 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…).
Przykład użycia funkcji disj
1
2
(disj #{1 2 3 4} 1)
; => #{4 3 2}

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).
Przykład użycia funkcji clojure.set/join
1
2
3
4
5
6
7
8
9
10
11
12
13
14
(clojure.set/join #{                             ; pierwszy zbiór par
                    {:cena 1, :nazwa "sok"  }    ; klucz–wartość
                    {:cena 1, :nazwa "chleb"}
                    {:cena 2, :nazwa "chleb"}
                    {:cena 3, :nazwa "masło"}}
                  #{                             ; drugi zbiór par
                    {:nazwa "sok",   :waga 8}
                    {:nazwa "masło", :waga 4}
                    {:nazwa "chleb", :waga 6}})

; => #{ {:cena 2, :waga 6, :nazwa "chleb"}
; =>    {:cena 3, :waga 4, :nazwa "masło"}
; =>    {:cena 1, :waga 6, :nazwa "chleb"}
; =>    {:cena 1, :waga 8, :nazwa "sok"  } }

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).
Przykład użycia funkcji clojure.set/join w wersji trójargumentowej
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(clojure.set/join #{                            ; pierwszy zbiór par
                    {:cena 1, :nazwa "sok"  }   ; klucz–wartość
                    {:cena 1, :nazwa "chleb"}
                    {:cena 2, :nazwa "chleb"}
                    {:cena 3, :nazwa "masło"}}
                  #{                            ; drugi zbiór par
                    {:towar "sok",   :waga 8}
                    {:towar "masło", :waga 4}
                    {:towar "chleb", :waga 6}}
                  {:nazwa :towar})              ; mapa kluczy

; => #{ {:nazwa "masło", :cena 3, :waga 4, :towar "masło"}
; =>    {:nazwa "chleb", :cena 1, :waga 6, :towar "chleb"}
; =>    {:nazwa "sok",   :cena 1, :waga 8, :towar "sok"  }
; =>    {:nazwa "chleb", :cena 2, :waga 6, :towar "chleb"} }

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).
Przykład użycia funkcji clojure.set/project
1
2
3
4
5
6
7
8
9
10
(clojure.set/project #{
                       {:cena 1, :nazwa "sok"  }   ; klucz–wartość
                       {:cena 1, :nazwa "chleb"}
                       {:cena 2, :nazwa "chleb"}
                       {:cena 3, :nazwa "masło"}}
                     [:nazwa])                     ; klucze do pozostawienia

; => #{ {:nazwa "sok"  }
; =>    {:nazwa "masło"}
; =>    {:nazwa "chleb"} }

Podzbiór, select

Wybieranie podzbioru elementów o wskazanych kryteriach umożliwia select.

Użycie:

  • (clojure.set/select predykat zbiór).
Przykład użycia funkcji clojure.set/select
1
2
(clojure.set/select even? #{1 2 3 4 5 6}) ; operator even? zwraca prawdę dla parzystych
; => #{4 6 2}

Suma zbiorów, union

Użycie:

  • (clojure.set/union & zbiór…).
Przykład użycia funkcji clojure.set/union
1
2
(clojure.set/union #{1 2} #{2 3})
; => #{1 3 2}

Różnica zbiorów, difference

Użycie:

  • (clojure.set/difference zbiór & zbiór-odejmowanych…).
Przykład użycia funkcji clojure.set/difference
1
2
(clojure.set/difference #{1 2} #{2 3})
; => #{1}

Część wspólna zbiorów, intersection

Użycie:

  • (clojure.set/intersection zbiór & inny-zbiór…).
Przykład użycia funkcji clojure.set/intersection
1
2
(clojure.set/intersection #{1 2} #{2 3})
; => #{2}

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).
Przykłady użycia funkcji clojure.set/rename
1
2
3
4
5
6
7
8
(def relacje #{ {:a 1, :b 2}
                {:a 3, :b 4} })

(clojure.set/rename relacje {:a :b, :b :a})  ; zamiana :a z :b
; => #{ {:b 3, :a 4} {:b 1, :a 2} }

(clojure.set/rename relacje {:a :x})         ; zamiana :a na :x
; => #{ {:b 2, :x 1} {:b 4, :x 3} }

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.
Przykłady użycia funkcji clojure.set/index
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
(def ludzie #{ {:imię "Damian",  :wiek 17}
               {:imię "Janina",  :wiek 19}
               {:imię "Zuzanna", :wiek 31}
               {:imię "Zuzanna", :wiek 19} })

;; grupowanie po wieku
(clojure.set/index ludzie [:wiek])

; => { {:wiek 19} #{ {:imię "Janina",  :wiek 19} {:imię "Zuzanna", :wiek 19} },
; =>   {:wiek 31} #{ {:imię "Zuzanna", :wiek 31} },
; =>   {:wiek 17} #{ {:imię "Damian",  :wiek 17}}}

;; grupowanie po imieniu
(clojure.set/index ludzie [:imię])

; => { {:imię "Janina"}  #{ {:imię "Janina",  :wiek 19} },
; =>   {:imię "Zuzanna"} #{ {:imię "Zuzanna", :wiek 31} {:imię "Zuzanna", :wiek 19} },
; =>   {:imię "Damian"}  #{ {:imię "Damian", :wiek 17}}}

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

1
2
3
4
5
6
7
8
9
10
11
12
(def ludzie #{ {:imię "Damian",  :wiek 17, :wzrost 180}
               {:imię "Janina",  :wiek 19, :wzrost 180}
               {:imię "Zuzanna", :wiek 19, :wzrost 210}
               {:imię "Zuzanna", :wiek 19, :wzrost 168}})

;; grupowanie po wieku i imieniu
(clojure.set/index ludzie [:wiek :imię])

; => { {:imię "Janina",  :wiek 19} #{ {:imię "Janina",  :wzrost 180, :wiek 19}},
; =>   {:imię "Damian",  :wiek 17} #{ {:imię "Damian",  :wzrost 180, :wiek 17}},
; =>   {:imię "Zuzanna", :wiek 19} #{ {:imię "Zuzanna", :wzrost 168, :wiek 19}
; =>                                  {:imię "Zuzanna", :wzrost 210, :wiek 19}}}

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

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).
Przykłady użycia funkcji rseq
1
2
3
4
5
6
(rseq (sorted-set :a, :b, :c))
; => (:c :b :a)

;; konwersja do zbioru
(into #{} (rseq (sorted-set :a, :b, :c)))
; => #{:c :b :a}

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ą z asocjacyjnym sposobem dostępu. Jeśli 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, to 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ą: <, <=, >>.

Użycie:

  • (subseq zbiór-sortowany test wartość),
  • (subseq zbiór-sortowany test-start wartość-start test-stop wartość-stop).
Przykłady użycia funkcji subseq
1
2
3
4
5
6
7
;; zakres jednostronny
(subseq (sorted-set :a, :b, :c) <= :b)
; => (:a :b)

;; zakres dookreślony
(subseq (sorted-set :a, :b, :c) > :a <= :b)
; => (:b)

Odwrócona sekw. z zakresu, rsubseq

Funkcja rsubseq działa jak połączenie rseqsubseq, 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ą: <, <=, >>.

Użycie:

  • (rsubseq zbiór-sortowany test wartość),
  • (rsubseq zbiór-sortowany test-start wartość-start test-stop wartość-stop).
Przykłady użycia funkcji rsubseq
1
2
3
4
5
6
7
8
9
10
11
;; zakres jednostronny
(rsubseq (sorted-set :a :b :c) <= :b)
; => (:b :a)

;; zakres dookreślony
(rsubseq (sorted-set :a, :b, :c) > :a <= :c)
; => (:c :b)

;; konwersja do zbioru
(into #{}  (rsubseq (sorted-set :a, :b, :c) <= :b))
; => #{:b :a}

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. 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…).
Przykłady użycia funkcji conj
1
2
3
4
(conj '(1 2 3) 4 5)         ; => (5 4 1 2 3)
(conj  [1 2 3] 4 5)         ; => [1 2 3 4 5]
(conj {:a 1, :b 2} [:c 3])  ; => {:c 3, :b 2, :a 1}
(conj {:a 1, :b 2} {:c 3})  ; => {:c 3, :b 2, :a 1}

Dołączanie elementów, into

Dzięki funkcji into możemy dołączać elementy jednych kolekcji do drugich lub tworzyć nowe kolekcje (jeśli podane istniejące 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).
Przykłady użycia funkcji into
1
2
3
4
(into () '(1 2 3))                   ; => (3 2 1)
(into [] '(1 2 3))                   ; => [1 2 3]
(into (sorted-map) [{:c 3} [:b 2]])  ; => {:b 2, :c 3}
(into '(:a :b :c) [:d])              ; => (:d :a :b :c)

Tworzenie podobnych kolekcji

Podobna kolekcja, empty

Funkcja empty generuje pustą kolekcję tego samego typu, co kolekcja podana jako jej argument.

Użycie:

  • (empty kolekcja).
Przykłady użycia funkcji empty
1
2
(empty [1 2 3])  ; => []
(empty ())       ; => ()

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).
Przykłady użycia funkcji not-empty
1
2
(not-empty ())        ; => nil
(not-empty '(1 2 3))  ; => (1 2 3)

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…).
Przykłady użycia funkcji =
1
2
3
4
(= '(1 2 3) '(1 2 3))           ; => true
(= '(1 3 2) '(1 2 3))           ; => false
(=  [1 2 3]  [1 3 2])           ; => false
(=  [1 2 3]  [1 2 3]  [1 2 3])  ; => true

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).
Przykład użycia funkcji count
1
2
(count '(1 2 3))
; => 3

Testowanie kolekcji

Testowanie typów

Istnieje kilka przydatnych funkcji, które pozwalają sprawdzać z jakiego typu kolekcją mamy do czynienia:

  • (coll?   wartość) – zwraca true, gdy wartość jest kolekcją
  • (list?   wartość) – zwraca true, gdy wartość jest listą
  • (vector? wartość) – zwraca true, gdy wartość jest wektorem
  • (set?    wartość) – zwraca true, gdy wartość jest zbiorem
  • (map?    wartość) – zwraca true, gdy wartość jest mapą
  • (seq?    wartość) – zwraca true, gdy wartość jest sekwencją
  • (record? wartość) – zwraca true, gdy wartość jest rekordem

Testowanie cech

Możemy sprawdzać co cechuje kolekcję, używając jednej z przeznaczonych do tego funkcji:

  • (sequential?  wartość) – zwraca true, gdy można tworzyć sekwencje na bazie kolekcji
  • (associative? wartość) – zwraca true, gdy kolekcja jest asocjacyjna
  • (sorted?      wartość) – zwraca true, gdy kolekcja jest sortowana
  • (counted?     wartość) – zwraca true, gdy kolekcja jest policzalna w skończonym czasie
  • (reversible?  wartość) – zwraca true, gdy można odwracać kolejność elementów kolekcji

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…).
Przykłady użycia funkcji distinct?
1
2
(distinct? '(1 2 3) '(1 3 2) '(1 2 3))  ; => true
(distinct? '(1 2 3) '(1 2 3))           ; => false

Pustość, empty?

Sprawdzanie czy kolekcja jest pusta może być osiągnięte z wykorzystaniem funkcji empty?.

Użycie:

  • (empty? kolekcja).
Przykłady użycia funkcji empty?
1
2
3
4
(empty? ())      ; => true
(empty? [])      ; => true
(empty? nil)     ; => true
(empty [1 2 3])  ; => false

“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).
Przykłady użycia funkcji every?
1
2
3
4
5
(every? even? [1 2 3])      ; czy każdy element jest parzysty?
; => false                  ; nie, nie każdy

(every? even? [2 4 6])      ; czy każdy element jest parzysty?
; => true                   ; tak, każdy

“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).
Przykłady użycia funkcji not-every?
1
2
3
4
5
(not-every? even? [1 2 3])  ; czy choć jeden element nie jest parzysty?
; => true                   ; tak, przynajmniej jeden z nich nie jest

(not-every? even? [2 4 6])  ; czy choć jeden element nie jest parzysty?
; => false                  ; nie, wszystkie są parzyste

“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śli żaden i zwrócona przez podaną funkcję wartość logiczna dla pierwszego napotkanego elementu.

Użycie:

  • (some predykat kolekcja).
Przykłady użycia funkcji some
1
2
3
4
5
(some even? [1 2 3])       ; czy choć jeden element jest parzysty?
; => true                  ; tak, funkcja even? dla 2 zwraca true

(some even? [1 3 5])       ; czy choć jeden element jest parzysty?
; => nil                   ; żaden

“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).
Przykłady użycia funkcji not-any?
1
2
3
4
5
(not-any? even? [1 2 3])  ; czy żaden element nie jest parzysty?
; => false                ; nie, występują nieparzyste

(not-any? even? [1 3 5])  ; czy żaden element nie jest parzysty?
; => true                 ; tak, nie ma ani jednego innego

Obsługa metadanych

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ę metadanych, jeśli kolekcji je przypisano lub nil w przeciwnym razie.

Przykłady użycia funkcji meta
1
2
3
4
5
6
7
(meta '(1 2))            ; => {:column 8 :line 1}
(meta [1 2])             ; => nil
(meta '^:testowa [1 2])  ; => {:testowa true}

(def x ^:flaga {:a :b})
(meta x)
; => {:flaga true}

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 należy funkcji with-meta 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.

Przykłady użycia funkcji with-meta
1
2
3
4
5
(meta (with-meta [1 2]  {:klucz "wartość"}))  ; => {:klucz "wartość"}
(meta (with-meta {:a 1} {:k "v"}))            ; => {:k "v"}
(meta (with-meta #{:a}  {:k "v"}))            ; => {:k "v"}
(meta (with-meta '(1 2) {:k "v"}))            ; => {:k "v"}
(meta (with-meta (list) {:k "v"}))            ; => {:k "v"}

Należy pamiętać o rozróżnieniu metadanych symboli identyfikujących kolekcje od metadanych tych kolekcji.

Wyrażenia metadanowe

W Clojure istnieje również makro czytnika, które wywołuje with-meta na wartości umieszczonej po jego prawej stronie. Możemy użyć go również z kolekcjami.

Użycie:

  • ^:flaga kolekcja,
  • '^:flaga kolekcja,
  • ^{ :klucz wartość … } kolekcja,
  • '^{ :klucz wartość … } kolekcja.

Skorzystanie z niego polega na użyciu znaku akcentu przeciągłego (ang. circumflex), po którym w nawiasach klamrowych następują pary (klucz i wartość) określające metadane. Jeżeli metadana wyraża wartość logiczną true (czyli jest flagą), to klamry i wartość można pominąć, jednak należy pamiętać o dwukropku przed nazwą klucza.

Opcjonalnie zamiast pojedynczego klucza można podać łańcuch znakowy – zostanie wtedy ustawiony klucz :tag z wartością tego łańcucha. W przypadku metadanych zgrupowanych w nawiasach klamrowych kluczami mogą być łańcuchy znakowe, symbole lub słowa kluczowe.

Przykłady korzystania z makra czytnika ustawiającego metadane
1
2
3
4
5
(meta ^:flaga [1 2])  ; => {:flaga true}
(meta ^{:a 1} {1 2})  ; => {:a 1}
(meta ^:flaga #{:a})  ; => {:flaga true}
(meta '^:flaga (12))  ; => {:flaga true :column 8 :line 1}
(meta  ^:flaga   ())  ; => {:flaga true :column 7 :line 1}

Uwaga: W przypadku formuł stałych list, powstałych w wyniku ich zacytowania, makro czytnika należy umieścić po znaczniku cytowania, a przed nazwą symbolu, aby ustawianie metadanych było skuteczne.

Zobacz także:

Ten materiał jest częścią większej całości, którą można zobaczyć odwiedzając poniższy odnośnik:

Komentarze