Poczytaj mi Clojure, cz. 9

Kolekcje

Grafika

Kolekcje to abstrakcyjna klasa struktur danych, służąca do reprezentowania zbiorów elementów o wspólnych cechach bądź wspólnym przeznaczeniu. Umożliwiają one wyrażanie takich zestawów w sposób zgrupowany w pojedynczym obiekcie. W Clojure możemy korzystać z kilku wbudowanych kolekcji, a konkretnie z list, wektorów, map i zbiorów.

Kolekcje

Kolekcja (ang. collection), zwana też niekiedy kontenerem (ang. container), jest nazwą rodziny złożonych struktur danych, służących do grupowania elementów i pozwalających na dostęp do nich w ujednolicony sposób.

Warto mieć na względzie, że kolekcja nie musi być zbiorem danych konkretnego rodzaju, lecz strukturą, która grupuje elementy o pewnych wspólnych właściwościach. Mogą to być cechy implementacyjne (np. podobne typy danych) bądź związane z założeniami logiki aplikacji (np. dane o podobnym przeznaczeniu).

Możemy wyróżnić trzy podstawowe rodzaje kolekcji:

  • liniowe (ang. linear),
  • asocjacyjne (ang. associative),
  • drzewiaste (ang. tree).

Niezależnie od tego, z jakiego konkretnie typu kolekcją mamy do czynienia, w Clojure jesteśmy w stanie korzystać z jednolitego interfejsu dostępu do niej, czyli z zestawu operatorów (funkcji) pozwalających nią zarządzać. Dodatkowo istnieją też funkcje, które umożliwiają przeprowadzanie operacji specyficznych dla pewnych rodzajów kolekcji.

Wbudowane kolekcje języka Clojure to:

Ze względu na charakterystykę map, list i wektorów, mogą one służyć również do wyrażania struktur drzewiastych. Umożliwiającą to cechą jest możliwość zagnieżdżania kolekcji w kolekcjach, ponieważ ich elementy nie muszą być jednakowego typu, a więc mogą również być kolekcjami.

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

Przedstawione dalej działania na kolekcjach są operacjami związanymi z tymi konkretnymi rodzajami kolekcji (z małymi wyjątkami, np. dotyczącymi zmiany kolejności i pozyskiwania fragmentów z użyciem funkcji rseq, subseqrsubseq czy usuwania elementów z wektorów). Nie znajdziemy więc niektórych gotowych konstrukcji realizujących pewne – wydawałoby się podstawowe – czynności, jak np. wydzielanie fragmentów list. Oznacza to, że tego typu operacje nie są pojedynczymi funkcjami, co jest najprawdopodobniej efektem specyfiki konkretnej struktury danych, która może nie być zoptymalizowana pod kątem realizowania niektórych przekształceń. Jeżeli w tym rozdziale trudno będzie znaleźć opis konkretnej operacji na kolekcji, warto zajrzeć do kolejnej części poświęconej sekwencjom, ponieważ wszystkie opisywane tu kolekcje mogą być również traktowane sekwencyjnie.

Listy

Lista (ang. list) to abstrakcyjny typ danych, który pozwala na liniowe porządkowanie elementów. Najprościej do implementowania list wykorzystać strukturę danych zwaną listą połączoną (ang. linked list) – umożliwia ona organizowanie elementów w taki sposób, że dla każdego z nich utrzymywana jest informacja o relacji z elementem następującym po nim (lista jednokierunkowa) lub z elementem następującym po nim oraz poprzedzającym go (lista dwukierunkowa).

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

Przeznaczeniem list jest grupowanie elementów z zachowaniem porządku, który zależy od kolejności ich dodawania do listy (jest w stosunku do niej odwrotny). Zoptymalizowane są pod kątem szybkiego przyłączania nowych elementów do ich czół.

Z list będziemy korzystali przede wszystkim do sterowania procesami związanymi ze szczegółami implementacyjnymi programu (np. do tworzenia stosów), rzadziej do przetwarzania dużych zbiorów danych przynależących do logiki aplikacji. Innym zastosowaniem tej struktury danych jest łączenie innych struktur (np. innych list i pojedynczych wartości) w logicznie powiązane łańcuchy.

Zarówno listy przechowujące dane (formy listowe), jak i reprezentujące w pamięci listowe S-wyrażenia kodu źródłowego (formy wywołania) są reprezentowane strukturami typu clojure.lang.PersistentList, dla których utrzymywana jest informacja o rozmiarze (liczbie elementów). Listy stanowiące elementy drzewa składniowego mogą być też w pewnych okolicznościach reprezentowane przez tzw. leniwe listy, czyli sekwencje połączonych komórek typu clojure.lang.Cons. Dla tego typu list nie będzie przechowywany licznik rozmiaru i nie zadziałają na nich wszystkie operacje, które możemy wykonywać na zwykłych listach.

Tworzenie list

Tworzyć listowe struktury danych możemy na trzy sposoby:

  • wpisując listowe S-wyrażenia – zostaną one odwzorowane w pamięci w kolejności wprowadzania elementów, a pierwszy z nich powinien być formą symbolową identyfikującą podprogram do wykonania (funkcję, makro lub formę specjalną), zaś pozostałe przekazywanymi argumentami – powstają w ten sposób formy wywołania funkcji, makr lub form specjalnych;

  • korzystając z konstrukcji list lub innej, tworzącej formy stałe list (lub po prosu formy listowe) na podstawie przekazanych wartości argumentów, które staną się kolejnymi elementami (w tej samej kolejności, w jakiej je podano, korzystając z list) po przeliczeniu każdego do formy stałej;

  • korzystając z literalnego zapisu w formie zacytowanych, listowych S-wyrażeń – powstają wtedy formy listowe, w których dodawane elementy są formami stałymi S-wyrażeń wyszczególnionych na kolejnych pozycjach i nie są poddawane wartościowaniu (tu również pierwotny porządek elementów będzie taki sam, jak porządek wprowadzania).

Pierwszy sposób będzie służył do zapisywania kodu źródłowego programu przeznaczonego do wykonania, a dwa kolejne do tworzenia użytkowych struktur danych.

Listowe S-wyrażenia

Zacznijmy od przypomnienia listy, która reprezentuje kod programu, a powstaje, gdy skorzystamy z listowego S-wyrażenia. Jego pierwszym elementem powinien być operator, wyrażony na przykład symbolem (formą symbolową) odnoszącym się do funkcji, makra lub formy specjalnej, a kolejnymi argumenty, które zostaną przekazane podczas wywoływania podprogramu tej formy.

Użycie:

  • (operator & operand…).
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
(inc 1) ; => 2 (+ 2 2) ; => 4 (+) ; => 0 (printf "trala %d\n" (+ 1 1)) ; => nil ; >> trala 2

Utworzone w ten sposób listy to formy wywołania, które służą do wyrażania kodu źródłowego, chociaż w niektórych przypadkach można je traktować jak kolekcje i operować na nich (np. gdy zostaną zacytowane lub gdy mamy do czynienia z tzw. makrami). Ich formy (wywołania funkcji, wywołania makra lub specjalne) będą wartościowane do rezultatów wykonania podprogramów.

Wewnętrznie (w drzewie składniowym programu) listy te będą reprezentowane z użyciem obiektów typu clojure.lang.PersistentList.

Lista z argumentów, list

Listowe formy można tworzyć z wykorzystaniem funkcji list. Takie listy są kolekcjami reprezentowanymi obiektami typu clojure.lang.PersistentList, czyli niemutowalnymi, jednokierunkowymi listami, które składają się z połączonych relacją następczości elementów.

Funkcja list przyjmuje zero lub więcej argumentów, których wartości staną się elementami listy. Każdy argument będzie przed umieszczeniem na liście wartościowany, czyli potraktowany jak forma, którą należy przeliczać, aż stanie się formą stałą (wyrażającą wartość własną).

Funkcja list zwraca listę (obiekt typu clojure.lang.PersistentList).

Użycie:

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

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

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

Lista z kolekcji, list*

Podobną do funkcji list jest list* (ze znakiem asterysku na końcu nazwy).

Symbol gwiazdki w nazwie jest umownym sposobem poinformowania programisty o tym, że mamy do czynienia ze spłaszczonymi argumentami (ang. splatted arguments), tzn. poszczególne elementy są ekstrahowane z kolekcji przekazanej jako ostatni argument zanim zostaną dodane do listy.

Użycie:

  • (list* kolekcja),
  • (list* element… kolekcja).

W wersji jednoargumentowej funkcja list* przyjmuje kolekcję, której elementy zostaną dodane do listy. W wersji wieloargumentowej kolekcja powinna być podana jako ostatni argument, opcjonalnie poprzedzony pojedynczymi elementami podanymi jako wartości wcześniejszych argumentów.

Jeżeli nie chcemy przekazywać kolekcji, możemy zamiast niej przekazać jako ostatni argument wartość nil lub pustą kolekcję. Wtedy funkcja list* zachowa się podobnie do list.

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

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)
(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)
;; 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 formy list tym, że nie jest dokonywane obliczanie wartości (wartościowanie) podanych elementów, które nie są formami stałymi. Zamiast tego elementami stają się reprezentacje podanych w kodzie źródłowym S-wyrażeń.

Wspomnianą różnicę możemy zaobserwować, gdy podamy niepoprawne formy (np. formy symbolowe, które nie są powiązane z żadnymi wartościami):

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

Wyrażenie z pierwszej linii generuje błąd, ponieważ w argumentach wywołania pojawia się symbol c, który nie jest powiązany z wartością, a więc nie można potraktować go jak formy symbolowej i uzyskać formy stałej w procesie rozpoznawania nazw i ewaluacji.

Gdybyśmy chcieli przekazać jako argument symbol w formie stałej, a nie identyfikowany jego nazwą obiekt, moglibyśmy użyć cytowania (wskazującego na literalną postać symbolu) lub odpowiedniej funkcji zwracającej symbol jako stałą wartość:

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
(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 wartość jej pierwszego elementu lub wartość nieustaloną nil, jeżeli 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
(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ść nieustaloną nil, jeżeli 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
(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 wartość jej pierwszego elementu lub wartość nieustaloną nil, jeżeli 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
(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żeli nie użyto trzeciego argumentu, zgłaszany 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"
(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 wśród elementów listy możemy zrealizować korzystając z metod Javy.

Metody .indexOf.lastIndexOf

Wyszukiwanie wartości można realizować z wykorzystaniem wbudowanych metod Javy, które przyjmują dwa argumenty: listę i wartość poszukiwanego elementu. Wartością zwracaną jest numer indeksu (licząc od 0), pod którym element o podanej wartości się znajduje. Metody zwracają wartość -1, gdy poszukiwany element nie może być odnaleziony.

Użycie:

  • (.indexOf lista element),
  • (.lastIndexOf lista element).
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
(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 jednokierunkowa lista, jednak ze względów wydajnościowych powinno unikać się takiego zastosowania.

Dodawanie elementów do listy

Dodawanie komórek, cons

Aby dodać do listy nowy element na jej czele, możemy skorzystać z funkcji cons. Przyjmuje ona dwa argumenty: dodawany element i obiekt listy.

Zwracana struktura nie jest stałą listą, ale komórką cons (obiektem typu clojure.lang.Cons), który tworzy tzw. leniwą listę, zawierającą dwie istotne informacje: dodawany 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 bazujących na obiekcie IPersistentList.

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

Preferowanym sposobem dodawania elementu do listy jest użycie funkcji conj (z ang. conjoin). Wykorzystuje ona polimorficzne wywołania odpowiednich metod Javy w zależności od rodzaju kolekcji. W ten sposób dołączany jest element, a na zwracanej strukturze możemy przeprowadzać te same operacje, co na pierwotnej. W przypadku list będzie to obiekt typu clojure.lang.IPersistentList.

Funkcja conj przyjmuje dwa obowiązkowe argumenty. Pierwszym może być lista, a drugim dodawany element. Zwracaną wartością jest nowa lista z dodanym na jej czele elementem przekazanym jako drugi argument.

Opcjonalnie możemy przekazać do wywołania conj więcej argumentów, których wartości zostaną dodane do wynikowej struktury. Pierwszy podawany jako argument element będzie umieszczony na czele listy jako pierwszy, drugi w drugiej kolejności itd. Oznacza to, że ostatni podawany argument stanie się pierwszym elementem zwracanej listy (odwrócona kolejność).

Użycie:

  • (conj lista element & element…).
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
;; 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 z list możliwe jest z wykorzystaniem funkcji poprest. Ponieważ mamy do czynienia z danymi niemutowalnymi, więc oczywiście rezultatami będą nowe listy, które od oryginalnych różnią się jednym elementem, a nie ze strukturami zmodyfikowanymi.

Bez pierwszego elementu, pop

Listę bez pierwszego elementu uzyskamy przez wywołanie funkcji pop. Przyjmuje ona jeden argument, którym powinna być lista, a zwraca obiekt typu PersistentList (nową listę).

Użycie:

  • (pop lista).
Przykłady użycia funkcji pop
1
2
3
(pop '(1 2 3))  ; => (2 3)
(pop '(1))      ; => ()
(pop ())        ; >> IllegalStateException
(pop '(1 2 3)) ; => (2 3) (pop '(1)) ; => () (pop ()) ; >> IllegalStateException

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

Poza pierwszym elementem, rest

Listę złożoną ze wszystkich elementów poza pierwszym uzyskamy z użyciem funkcji rest, która dla podanej jako argument listy zwraca tzw. resztę.

Zwracanym obiektem jest lista (obiekt typu PersistentList).

Użycie:

  • (rest lista).
Przykłady użycia funkcji rest
1
2
3
(rest '(1 2 3))  ; => (2 3)
(rest '(1))      ; => ()
(rest ())        ; => ()
(rest '(1 2 3)) ; => (2 3) (rest '(1)) ; => () (rest ()) ; => ()

Cytowanie i wartościowanie list

Listowe S-wyrażenia mogą być zacytowane z użyciem quote lub lukru składniowego w postaci pojedynczego apostrofu. Są wtedy formami stałymi, a nie formami wywołania:

1
'(+ 2 2)
'(+ 2 2)

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

1
(list '+ 2 2)
(list '+ 2 2)

Zacytowany został tylko symbol plusa, bo w przeciwnym razie byłby on wartościowany do obiektu funkcyjnego identyfikowanego symbolem + (operacja sumowania). Literały liczbowe zostawiliśmy, ponieważ już są formami stałymi (wyrażającymi własne wartości).

Na liście możemy dokonywać operacji i przekształcać ją, korzystając z różnorakich form, aby potem znów zmienić formę listową na złożoną (poddawaną wyliczaniu):

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 (forma stała listy)
 '(3))     ; druga lista    (forma stała listy)
(concat ; ta funkcja połączy poniższe listy w jedną sekwencję '(+ 2 2) ; pierwsza lista (forma stała listy) '(3)) ; druga lista (forma 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)
(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 formę stałą naszej listy w formę wywołania. Będzie ona przez ewaluator traktowana jako kod do wykonania i wartościowana. Możemy to osiągnąć posługując się konstrukcją eval:

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

W wyniku otrzymaliśmy formę stałą prezentowaną przez REPL jako atomowe S-wyrażenie (literał liczbowy będący atomem), które wyraża wartość liczbową 7. W skrócie: otrzymaliśmy wartość 7.

Widzimy więc, jak łatwo zapisać kod jako dane, a następnie dane przetworzyć tak, aby stały się kodem. W Lispie granica między nimi jest płynna, a wraz z mechanizmem tworzenia makr pozwala budować metajęzyki i języki dziedzinowe przeznaczone do konkretnych zastosowań, jak też tworzyć programy, które są szczęśliwymi programami.

Programs that write programs are the happiest programs in the world.  
 
— Andrew Hume

Wektory

Wektor (ang. vector) to liniowa struktura danych, która przypomina jednowymiarową tablicę, lecz różni się od niej tym, że możemy dynamicznie dodawać i usuwać elementy. Wektory służą do przechowywania danych w sposób uporządkowany, opatrując elementy numerami indeksów. Dostęp do takich danych jest swobodny, tzn. nie trzeba przeszukiwać całego wektora, aby odnieść się do elementu o znanej pozycji – można po prostu zażądać odczytu wartości umieszczonej pod danym indeksem.

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

Wektory możemy budować korzystając z odpowiednich funkcji bądź wyrażać literalnie, używając wektorowych S-wyrażeń. W przeciwieństwie do symbolicznych wyrażeń listowych nie będą one tworzyły form wywołania, lecz formy wektorowe (do przechowywania danych) bądź powiązaniowe (do wytwarzania powiązań symboli z wartościami). Podane elementy form wektorowych zapisanych jako wektorowe S-wyrażenia będą przeliczane przed utworzeniem struktury danych, aż każdy z nich będzie formą stałą.

Jeżeli chodzi o przetwarzanie danych związanych z logiką aplikacji, wektory nadają się do tego celu bardziej niż listy. W programach będzie to więc struktura używana do zarządzania uporządkowanymi zbiorami informacji, w których istotny jest szybki dostęp do elementów o znanych pozycjach. Dzięki wewnętrznemu zastosowaniu zagnieżdżonych drzew bazujących na funkcjach mieszających optymistyczna czasowa złożoność obliczeniowa dla operacji odczytu, aktualizacji, pobierania fragmentów i dodawania elementów do wektorów jest bliska stałej – O(1); chociaż złożoność oczekiwana będzie logarytmiczna – Θ(log n).

Tworzenie wektorów

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

Wektor z argumentów, vector

Funkcja vector przyjmuje zero lub więcej argumentów, których wartości (po przeliczeniu do form stałych) staną się elementami wektora.

Wartością zwracaną jest wektor (obiekt typu clojure.lang.PersistentVector).

Użycie:

  • (vector & element…).

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)        ; => []
(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.

Zwracaną wartością jest wektor (obiekt typu clojure.lang.PersistentVector).

Typ danych możemy wyrazić słowem kluczowym, które powinno należeć do z góry określonego zbioru: :int, :long, :float, :double, :byte, :short, :char, :boolean.

Stworzony z użyciem vector-of wektor będzie składał się z obiektów konkretnego typu podstawowego (ang. primitive type), a nie typu generycznego, z którego podczas każdej operacji dostępu musiałby być rozpakowywany (ang. unboxed) właściwy obiekt. Ma to pozytywny wpływ na prędkość dostępu do elementów kolekcji.

Użycie:

  • (vector-of typ & element…).

Przykład użycia funkcji vector-of
1
2
(vector-of :int 1 2)
; => [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ę i buduje zawartość na bazie jej kolejnych elementów.

Zwracaną wartością jest wektor (obiekt typu clojure.lang.PersistentVector).

Użycie:

  • (vec napis),
  • (vec sekwencja).
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 ())            ; => []
(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 wyrażać wektory bądź powiązania symboli z wartościami. Składa się z zestawu elementów ujętych w nawiasy kwadratowe lub z samych nawiasów kwadratowych (w przypadku wektorów pustych).

Wektorowe S-wyrażenia mogą przyjmować następujące formy:

Formy wektorowe

Jeżeli podane elementy wektora literalnego (tworzącego formę wektorową) są formami innymi niż stałe, będą wartościowane, a w odpowiednich miejscach zostaną umieszczone rezultaty przeprowadzanych obliczeń.

Użycie:

  • [],
  • [element…].
Przykłady wektorowych S-wyrażeń tworzących formy wektorowe
1
2
3
4
[1 2 3]      ; => [1 2 3]
[1, 2, 3]    ; => [1 2 3]
[1 (+ 2 1)]  ; => [1 3]
[]           ; => []
[1 2 3] ; => [1 2 3] [1, 2, 3] ; => [1 2 3] [1 (+ 2 1)] ; => [1 3] [] ; => []
Formy powiązaniowe

Wektorowe formy powiązaniowe posłużą nam do tworzenia powiązań symboli z wartościami z użyciem różnych form specjalnych oraz makr. Cechowały je będą pary wyrażeń składające się z symboli i towarzyszących im wyrażeń inicjujących odpowiedzialnych za ustawianie wartości początkowych. Wyjątkiem od tej reguły będą wektory parametryczne funkcji, w których podawane są same formy powiązaniowe symboli, ponieważ początkowe wartości będą pochodziły z argumentów przekazywanych podczas wywoływania.

Użycie:

  • [],
  • [symbol…],
  • [para-powiązaniowa…],

gdzie para-powiązaniowa to:

  • symbol wyrażenie-inicjujące.
Przykłady wektorowych S-wyrażeń tworzących wektorowe formy powiązaniowe
1
2
3
4
(let          [a 1 b 2] (+ a b))  ; powiązania leksykalne
(binding      [a 1 b 2] (+ a b))  ; powiązania dynamiczne (przesłanianie)
(defn funkcja [a b]     (+ a b))  ; wektor parametryczny funkcji nazwanej
(fn           [a b]     (+a b))   ; wektor parametryczny funkcji anonimowej
(let [a 1 b 2] (+ a b)) ; powiązania leksykalne (binding [a 1 b 2] (+ a b)) ; powiązania dynamiczne (przesłanianie) (defn funkcja [a b] (+ a b)) ; wektor parametryczny funkcji nazwanej (fn [a b] (+a b)) ; wektor parametryczny funkcji anonimowej

Zacytowane wektorowe S-wyrażenia

Wektorowe S-wyrażenia mogą być również zacytowane. W takim przypadku podawane elementy nie będą wartościowane, nawet jeżeli nie są formami stałymi (niewyrażającymi wartości własnych). Zamiast tego rezultat ewaluacji każdego z wprowadzonych jako kolejne elementy S-wyrażeń znajdzie się w wynikowej strukturze. Tego rodzaju wyrażenia mogą być używane do tworzenia form wektorowych.

Użycie:

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

Dostęp do wektorów

Pobieranie elementu wektora o określonej lokalizacji (o określonym numerze kolejnym indeksu) możliwe jest z użyciem get, nth, first, last, peek, albo przez odwołanie się do obiektu wektora jak do funkcji.

Pierwszy element, first

Funkcja first pobiera pierwszy element wektora. Przyjmuje ona jeden argument, którym powinien być wektor, a zwraca jego pierwszy element lub wartość nil, jeżeli nie znaleziono pierwszego elementu (mamy do czynienia z wektorem pustym).

Użycie:

  • (first wektor).

Przykłady użycia funkcji first
1
2
(first [1 2 3])  ; => 1
(first [])       ; => nil
(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żeli 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
(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ść nieustaloną nil, jeżeli nie znaleziono ostatniego elementu (mamy do czynienia z wektorem pustym).

Zauważmy, że działanie peek na wektorach różni się od działania tej funkcji na listach (tam zwraca ona pierwszy element).

Użycie:

  • (peek wektor).

Przykłady użycia funkcji peek
1
2
(peek [1 2 3])  ; => 3
(peek [])       ; => nil
(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, jego wartość zostanie zwrócona – w przeciwnym razie zwrócona będzie wartość nieustalona 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
(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 numerze indeksu lub wartość podaną jako opcjonalny, trzeci argument, jeżeli indeks nie istnieje.

W przypadku podania numeru kolejnego, który nie odpowiada żadnemu elementowi wektora i jeżeli nie użyto trzeciego argumentu, zgłaszany jest wyjątek IndexOutOfBoundsException.

Użycie:

  • (nth wektor numer-kolejny zastępnik?).
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"
(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

Istnieją funkcje, dzięki którym możemy dokonywać podziału wektorów na mniejsze wektory.

Wydzielanie zakresu, subvec

Funkcja subvec tworzy wektor z elementów istniejącego wektora. Jej pierwszym argumentem powinien być wektor źródłowy, a kolejnym początkowy numer indeksu, od którego zacznie się wydzielanie wektora pochodnego (licząc od 0).

Opcjonalnie można podać trzeci argument, który będzie oznaczał końcowy numer indeksu (nie wchodzący w skład wektora wynikowego).

Funkcja subvec zwraca wektor.

Jeżeli podano numery indeksów dla nieistniejących elementów lub zakres jest niewłaściwie określony, zgłoszony zostanie wyjątek IndexOutOfBoundsException.

Użycie:

  • (subvec wektor początek koniec?).

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)  ; => []
(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, 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]
(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 generycznej funkcji contains? lub wykorzystaniu wektorowej formy przeszukiwania. Z kolei wyszukiwanie wartości możemy zrealizować z wykorzystaniem metod Javy.

Wektor jako funkcja

Obiekty wektorów wyposażone są w funkcyjny interfejs, tzn. są również specyficznymi funkcjami. Listowe S-wyrażenie, którego pierwszym elementem jest wektor, będzie potraktowane jak forma przeszukiwania wektora (ang. vector lookup form). Przyjmuje ona jeden obowiązkowy argument, który oznacza element identyfikowany numerem indeksu, a zwraca jego wartość (podobnie jak w przypadku get). Jeżeli element o podanym kluczu nie istnieje w wektorze, zgłaszany jest wyjątek java.lang.IndexOutofboundsexception.

Użycie:

  • (wektor klucz).

Przykłady użycia wektora jako funkcji
1
2
3
([:a :b :c] 0)                     ; => :a
([:a :b :c] 1000)                  ; => java.lang.IndexOutOfBoundsException
(let [wektor [:a :b]] (wektor 1))  ; => :b
([:a :b :c] 0) ; => :a ([:a :b :c] 1000) ; => java.lang.IndexOutOfBoundsException (let [wektor [:a :b]] (wektor 1)) ; => :b

Wyszukiwanie indeksu, contains?

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

Użycie:

  • (contains? wektor indeks).
Przykłady użycia funkcji contains?
1
2
(contains? [1 2 3] 2)  ; => true
(contains? [1 2 3] 5)  ; => false
(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
(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 wektorów.

Łączenie wektorów

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

Funkcja into

Funkcja into przyjmuje dwa argumenty, którymi 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]
(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]
(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) w ostatniej specyfikowanej indeksami ścieżki strukturze.

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]
(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ść, która 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]
(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
13
14
15
16
17
;; 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]

;; drugi argument to ścieżka indeksów,
;; trzeci funkcja, która zawsze zwraca :x
(update-in [:a [:p :q :r] :b] [1 1] (constantly :x))
[:a [:p :x :r] :b]
;; 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] ;; drugi argument to ścieżka indeksów, ;; trzeci funkcja, która zawsze zwraca :x (update-in [:a [:p :q :r] :b] [1 1] (constantly :x)) [:a [:p :x :r] :b]

Warto zwrócić uwagę na użycie funkcji constantly w powyższym przykładzie. Jest to jedna z tzw. funkcji wyższego rzędu, ponieważ zwracaną przez nią wartością jest inna funkcja. Zadaniem tej ostatniej w przypadku constantly jest zwracanie stałej wartości niezależnie od wartości przekazanego argumentu. Używamy jej, ponieważ update-in wymaga, aby trzecim argumentem była funkcja, a nie wartość, a chcemy uzyskać konkretną wartość, niezależną od poprzedniej. Potrzebujemy więc funkcji, która będzie ją zwracać.

Dodawanie elementów do wektorów

Dodawanie na końcu, conj

Tworzenie wektora pochodnego z dodanym elementem na końcu możliwe jest dzięki generycznej funkcji conj operującej na kolekcjach. Przyjmuje ona dwa obowiązkowe argumenty: wektor i dodawany element, a zwraca nowy wektor.

Opcjonalnie możemy podać jako kolejne argumenty więcej elementów – zostaną one dodane do wynikowego wektora w kolejności ich przekazywania.

Użycie:

  • (conj wektor element & element…).

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] 
(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 jednego lub więcej elementów do wektora jest skorzystanie z funkcji into, ponieważ czasowa złożoność obliczeniowa tej operacji jest liniowa – O(n) (gdzie n jest liczbą dodawanych elementów). Przyjmuje ona dwa argumenty: wektor docelowy i wektor źródłowy, a zwraca nowy wektor, którego zawartość jest złączeniem podanych.

Użycie:

  • (into wektor-docelowy wektor-źródłowy).

Przykład użycia funkcji into
1
2
(into [1] [2 3 4 5])
; => [1 2 3 4 5]
(into [1] [2 3 4 5]) ; => [1 2 3 4 5]

Dodawanie wartości, assoc

Tworzenie wektora pochodnego z dodanym elementem zrealizujemy również z użyciem funkcji assoc. Przyjmuje ona trzy argumenty: wektor, numer indeksu i wartość dla elementu o podanym numerze kolejnym (licząc od 0). Musi to być numer o jeden większy, niż numer ostatniego elementu. Podanie błędnego numeru indeksu spowoduje zgłoszenie wyjątku IndexOutOfBoundsException.

Funkcja assoc zwraca wektor.

Użycie:

  • (assoc wektor indeks wartość).

Przykład użycia funkcji assoc
1
2
(assoc [1 2 3 4] 4 5)
; => [1 2 3 4 5]
(assoc [1 2 3 4] 4 5) ; => [1 2 3 4 5]

Dodawanie zagnieżdżonej, assoc-in

Tworzenie wektora pochodnego z dodanym elementem pochodzącym z zagnieżdżonej struktury zrealizujemy z użyciem funkcji assoc-in. Przyjmuje ona trzy argumenty: wektor, ścieżkę z numerami indeksów (lub kluczami innych struktur pośrednich) dla kolejno napotykanych struktur i wartość elementu o podanym numerze kolejnym (licząc od 0), która zostanie umieszczona w ostatniej strukturze określonej indeksami ścieżki. Ponieważ chcemy dodawać elementy, a nie je zastępować, musi to być numer o jeden większy, niż numer ostatniego elementu specyfikowanej struktury. Podanie błędnego numeru indeksu spowoduje zgłoszenie wyjątku IndexOutOfBoundsException.

Funkcja assoc-in zwraca wektor.

Użycie:

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

(assoc-in [:a [:p :q :r] :b] [1 3] :s)
; => [:a [:p :q :r :s] :b]

(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]
(assoc-in [1 2 3 4] [4] 5) ; => [1 2 3 4 5] (assoc-in [:a [:p :q :r] :b] [1 3] :s) ; => [:a [:p :q :r :s] :b] (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, a także zastosowania subvec wraz z funkcją łączącą powstałe wektory.

Bez pierwszego elementu, pop

Wektor bez pierwszego elementu uzyskamy przez wywołanie funkcji pop. Przyjmuje ona jeden argument, którym powinien być wektor, a zwraca obiekt typu clojure.lang.PersistentVector (nowy wektor).

Użycie:

  • (pop wektor).

Przykłady użycia funkcji pop
1
2
3
(pop [1 2 3])  ; => (2 3)
(pop [1])      ; => ()
(pop [])       ; >> IllegalStateException
(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

Sekwencję elementów wektora poza pierwszym uzyskamy z wykorzystaniem funkcji rest, która dla podanego jako argument wektora zwraca tzw. resztę.

Wartością zwracaną jest sekwencja utworzona na bazie wektora. Jeżeli potrzebujemy rezultatu w postaci wektora, możemy przekształcić wynik z użyciem funkcji vec, albo zamiast z rest skorzystać z pop.

Użycie:

  • (rest wektor).
Przykłady użycia funkcji rest
1
2
3
(rest [1 2 3])  ; => (2 3)
(rest [1])      ; => ()
(rest [])       ; => ()
(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)
(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]
(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))))
(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 (przekazywana funkcja) musi przyjmować tyle argumentów, ile wektorów zostało podanych. Wtedy będzie wywoływana dla zbioru wartości stanowiących kolejne elementy każdego z wektorów.

Użycie:

  • (mapv operator wektor & wektor…).
Przykład użycia funkcji mapv
1
2
(mapv + [1 2 3 4] [10 20 30 40])
; => [11 22 33 44]
(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żeli istnieje taka potrzeba:

1
2
(mapv inc [1 2 3 4])
; => [2 3 4 5]
(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 wartości każdego kolejnego elementu przekazanego wektora. Jeżeli zwracana przez nią wartość będzie różna od false i różna od nil, dany element zostanie umieszczony w zwracanym wektorze wyjściowym.

Użycie:

  • (filterv predykat wektor).

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

Redukowanie z indeksem, reduce-kv

Funkcja reduce-kv użyta w odniesieniu do wektorów działa podobnie do reduce, z tą różnicą, że wywołuje podany jako pierwszy argument operator dla każdego elementu i jego numeru indeksu (poza akumulatorem i wartością aktualnie przetwarzanego elementu). Funkcja przyjmuje też wartość początkową akumulatora jako argument na drugiej pozycji.

Funkcja zwraca rezultat ostatniego wywołania operatora na wartości akumulatora, ostatnio przetwarzanym elemencie wektora i jego numerze kolejnym (licząc od 0).

Użycie:

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

Funkcja reduce-kv zalicza się do funkcji operujących na sekwencjach, czyli abstrakcyjnych strukturach reprezentujących kolekcje. Została umieszczona w tym miejscu, ponieważ jest specjalnym wariantem reduce, który operuje na wektorach (i innych kolekcjach indeksowanych numerycznie).

Więcej szczegółów dotyczących redukowania (zwijania) sekwencji wartości można znaleźć w części poświęconej transduktorom.

Odwracanie kolejności, rseq

Na podstawie wektorów można tworzyć sekwencje o odwróconej kolejności elementów. Służy do tego funkcja rseq. W stałym czasie zwraca ona sekwencję na bazie podanego wektora. Przyjmuje jeden argument, którym powinien być wektor, a zwraca leniwą sekwencję o kolejności elementów odwróconej względem kolejności w wektorze.

Gdybyśmy chcieli na bazie sekwencji znów uzyskać stałą strukturę pamięciową, możemy stworzyć wektor, korzystając z funkcji vec lub wprowadzić elementy do istniejącego wektora (funkcja into).

Użycie:

  • (rseq wektor).
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]
;; 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żeli to możliwe, ze słów kluczowych lub wypakowanych (ang. unboxed) typów numerycznych.

Mapy przystosowane są do szybkiego odnajdywania wartości na podstawie kluczy i swobodnego dodawania nowych elementów.

Do oddzielania poszczególnych par klucz–wartość, a także do oddzielania samych kluczy od wartości, w zapisie symbolicznym korzysta się z przecinka i/lub ze znaku spacji:

Przykład symbolicznie wyrażonej mapy
1
{ :a 1, :b 2, :c 3 }
{ :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) }
{ 'raz 1, 'dwa 2, "trzy" 3, :lista '(1 2 3) }

Wartościami map mogą być też inne mapy. Dzięki temu możemy tworzyć zagnieżdżone, drzewiaste 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 }}
{: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ć mapowe formy możemy z użyciem jednej z służących do tego funkcji lub symbolicznych, mapowych wyrażeń.

Mapa haszowana, hash-map

Funkcja hash-map przyjmuje zero lub więcej argumentów, których całkowita liczba powinna być parzysta. Każda wartość przekazanego, nieparzystego argumentu będzie kluczem, a następującego po nim skojarzoną z nim w mapie wartością.

Funkcja zwraca mapę (obiekt typu clojure.lang.PersistentHashMap), a dla map pustych mapę tablicową (obiekt typu clojure.lang.PersistentArrayMap), która zachowuje kolejność wprowadzanych elementów.

Użycie:

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

gdzie klucz–wartość to:

  • klucz wartość.

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)                  ; => {}
(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)                  ; => {}
(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 wartości pochodzącej z pierwszego argumentu powinna być mniejsza, równa czy większa od pozycji wartości pochodzącej z argumentu drugiego.

Do funkcji sorted-map-by można też przekazać opcjonalne argumenty, których liczba powinna być parzysta. Każda wartość przekazanego, nieparzystego argumentu będzie kluczem, a następującego po nim skojarzoną z nim w mapie wartością.

Funkcja zwraca mapę sortowaną (obiekt typu clojure.lang.PersistentTreeMap), która zachowuje porządek sortowania wprowadzanych elementów, a kryterium sortowania jest określone przekazanym komparatorem.

Użycie:

  • (sorted-map-by komparator & klucz–wartość…),

gdzie klucz–wartość to:

  • klucz wartość.
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)                               ; => {}
(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)                  ; => {}
(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)        ; => {}
(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 wartościach
(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]}
;; grupuj nieparzyste elementy wektora (group-by odd? [1 2 3 4 5]) ; => {true [1 3 5], false [2 4]} ;; grupuj po wartościach (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)             ; => {}
(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}
(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ą formami innymi niż stałe, będą wartościowane, a w mapie zostaną umieszczone rezultaty obliczeń.

Rezultatem obliczenia mapowego S-wyrażenia, które reprezentuje formę mapową, jest mapa (obiekt typu clojure.lang.PersistentHashMap) lub mapa tablicowa (obiekt typu clojure.lang.PersistentArrayMap), jeżeli podano mniej niż 9 par klucz–wartość.

Mapowe S-wyrażenia znajdują również zastosowanie w formach powiązaniowych, np. w procesie obsługi argumentów nazwanych funkcji i przy mapowych wyrażeniach powiązaniowych oraz inicjujących, wykorzystywanych w procesie dekompozycji (destrukturyzacji).

Użycie:

  • {},
  • {klucz–wartość…},

gdzie klucz–wartość to:

  • klucz wartość.
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}
{}                      ; => {}
{: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} {} ; => {}

Wyrażenia mapowe mogą również być używane w formach powiązaniowych symboli, aby przypisywać im metadane. Mówimy wtedy o wyrażeniach metadanowych.

Użycie:

  • ^{klucz–wartość…},

gdzie klucz–wartość to:

  • klucz wartość.
Przykłady mapy metadanowej
1
2
3
4
5
(def ^{:a "taki" :b "inna"} x 5)
(meta #'x)
; => {:a "taki", :b "inna",
; =>  :line 1, :column 1, :file "NO_SOURCE_PATH",
; =>  :name x, :ns #object[clojure.lang.Namespace 0x1dab9dd6 "user"]}
(def ^{:a "taki" :b "inna"} x 5) (meta #'x) ; => {:a "taki", :b "inna", ; => :line 1, :column 1, :file "NO_SOURCE_PATH", ; => :name x, :ns #object[clojure.lang.Namespace 0x1dab9dd6 "user"]}

Zacytowane mapowe S-wyrażenia

Mapowe S-wyrażenia mogą zostać zacytowane. W takim przypadku podawane wartości i klucze nie będą wartościowane, nawet jeżeli są formami, które nie wyrażają wartości własnych, lecz w mapie zostaną umieszczone struktury danych odpowiadające umieszczanym S-wyrażeniom.

Użycie:

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

gdzie klucz–wartość to:

  • klucz wartość.

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}
'{} ; => {} '{: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ść nieustaloną nil, jeżeli 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
(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 prezentowana 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
(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ść nieustaloną nil, jeżeli 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
(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 generycznej funkcji get. Przyjmuje ona dwa obowiązkowe argumenty: mapę i klucz wskazujący na element, którego wartość chcemy pobrać.

Funkcja get zwraca wartość elementu o podanym kluczu lub wartość nieustaloną nil, jeżeli klucza nie znaleziono. Gdy podano trzeci, opcjonalny argument, zamiast nil zwrócona będzie jego wartość.

Użycie:

  • (get mapa klucz wartość-domyślna?).

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

Użycie:

  • (get-in mapa wektor-ścieżki wartość-domyślna?).

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
(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 mapy umożliwia funkcja val.

Użycie:

  • (val element-mapy).

Funkcja przyjmuje jeden argument, którym powinien być element mapy (obiekt typu clojure.lang.MapEntry), a zwraca przechowywaną tam wartość.

Przykład użycia funkcji val
1
2
(val (first {:a 1, :b 2}))
; => 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)
(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
(key (first {:a 1, :b 2})) ; => :b

Wszystkie klucze, keys

Na pobieranie wszystkich kluczy mapy 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)
(keys {:a 1, :b 2, :c 3}) ; => (:c :b :a)

Przeszukiwanie map

Mapa jako funkcja

Obiekty map wyposażone są w funkcyjny interfejs, tzn. są również specyficznymi funkcjami. Możemy umieścić mapę jako pierwszy element listowego S-wyrażenia i będzie ono wtedy formą przeszukiwania mapy. Przyjmuje ona jeden obowiązkowy argument, który oznacza element identyfikowany kluczem, a zwraca jego wartość (podobnie jak w przypadku get). Jeżeli element o podanym kluczu nie istnieje w mapie, zwracana jest wartość nieustalona nil, chyba że jako drugi argument podano wartość domyślną (w takim przypadku zwracana jest właśnie ona).

Użycie:

  • (mapa klucz).
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
({: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 (przeszukiwania map) znajdują zastosowanie przy pobieraniu wartości elementów – oczywiście pod warunkiem, że kluczami tych map są słowa kluczowe.

Słowa kluczowe występujące w formie przeszukiwania mapy (umieszczone na pierwszym miejscu listowego S-wyrażenia) przyjmują jeden obowiązkowy argument (mapę), a zwracają wartość elementu mapy, który jest identyfikowany danym słowem kluczowym (w roli klucza). Jeżeli w mapie nie istnieje element o podanym kluczu, zwracana jest wartość nieustalona nil, chyba że przekazano drugi argument – w takim przypadku zwracana jest jego wartość.

Użycie:

  • (słowo-kluczowe mapa wartość-domyślna?).

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

Wyszukiwanie elementu, find

Pobieranie elementu (klucza i wartości) dla podanego klucza można zrealizować z użyciem funkcji find.

Użycie:

  • (find mapa klucz).

Funkcja przyjmuje dwa argumenty: pierwszym powinna być mapa, a drugim wartość klucza identyfikującego poszukiwany element.

Wartością zwracaną jest element mapy (obiekt typu clojure.lang.MapEntry) lub wartość nieustalona nil, jeżeli element nie został znaleziony.

Przykłady użycia funkcji find
1
2
(find {:chleb 1, :mleko 2} :mleko)    ; => [:mleko 2]
(find {:chleb 1, :mleko 2} :cytryna)  ; => nil
(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ć generycznej funkcji contains?. Przyjmuje ona dwa obowiązkowe argumenty: mapę i poszukiwany klucz, a zwraca wartość true, jeżeli element o podanym kluczu istnieje. W przeciwnym razie zwraca wartość false.

Użycie:

  • (contains? mapa klucz).

Przykład użycia funkcji contains?
1
2
(contains? { :klucz "wartość" } :klucz)         ; czy mapa zawiera :klucz?
; => true                                       ; tak, zawiera
(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ść"}
(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ą, 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}}
(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}
(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}}
(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"}
(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}}
(assoc-in {:zakupy {:cukier 1}} ; zmiana wartości klucza [:zakupy :cukier] 2) ; określonego ścieżką kluczy ; => {:zakupy {:cukier 2}}

Jeżeli klucze struktury pośredniej nie istnieją, zostaną utworzone.

Funkcja assoc-in potrafi też operować na bardziej skomplikowanych strukturach, na przykład wektorach zawierających mapy. W takich przypadkach pierwszym z kluczy w ścieżce będzie numer indeksu:

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"}]
(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"}]

W powyższym przykładzie ścieżką kluczy jest 1 :wiek, a więc w wektorze zostanie pobrany element o indeksie 1 (czyli drugi w kolejności), a następnie dla jego wartości, którą jest mapa, zostanie wybrany do aktualizacji element o kluczu :wiek.

Aktualizacja wartości, update

Aktualizację wartości elementu struktury wskazanego kluczem umożliwia funkcja update. Jako pierwszy argument przyjmuje ona mapę, kolejnym powinna być wartość klucza zmienianego elementu, a ostatnim funkcja, która przyjmuje jeden argument i zwraca wartość. Wartość ta zostanie użyta do aktualizacji wartości elementu.

Funkcja update zwraca nową mapę.

Użycie:

  • (update mapa klucz operator).

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}
(update {:imię "Ruprecht" :wiek 19} ; aktualizowana struktura :wiek inc) ; określenie klucza i operacji ; => {:imię "Ruprecht", :wiek 20}

Aktualizacja zagnieżdżonej, update-in

Aktualizację wartości elementu zagnieżdżonej struktury wskazanego ścieżką kluczy, umożliwia funkcja update-in. Jako pierwszy argument przyjmuje ona mapę, kolejnym powinna być sekwencja określająca ścieżkę kluczy wiodących do elementu, a ostatnim funkcja, która przyjmuje jeden argument i zwraca wartość. Wartość ta zostanie użyta do aktualizacji wartości wskazanego elementu struktury.

Funkcja update-in zwraca nową mapę.

Użycie:

  • (update-in mapa sekwencja-ścieżki operator).

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}]
(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 biorą pod uwagę porządek struktury. 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, a wartością zwracaną jest sekwencja elementów mapy (obiektów typu clojure.lang.PersistentTreeMap$RedVal).

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}
(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 brakiem indeksowania na bazie kluczy. Jeżeli więc zależy nam na zachowaniu struktury, możemy przekształcić rezultat rseq do mapy, na przykład korzystając z funkcji into.

Sekwencja z zakresu, subseq

Funkcja subseq pozwala na tworzenie sekwencji zawierającej elementy określone zakresem wyznaczonym operatorami i wartościami przekazanymi jako argumenty. Przyjmuje ona 3 lub 5 argumentów.

W wersji trójargumentowej należy podać mapę sortowaną, funkcję testującą i wartość przekazywaną jako drugi argument funkcji testującej (pierwszym będzie klucz kolejno przetwarzanego elementu podczas porównywania). Jeżeli na przykład podamy > 2, w wynikowej sekwencji znajdą się wyłącznie elementy, których klucze są wartościami większymi od 2.

W wersji pięcioargumentowej należy również podać mapę, jednak kolejne 4 argumenty to pary określające zakresy: funkcja testująca i wartość dla dolnej granicy zakresu, a następnie funkcja testująca i wartość dla górnej granicy zakresu.

Funkcja zwraca sekwencję zawierającą pary klucz–wartość wyrażone obiektami typu clojure.lang.PersistentTreeMap$BlackVal.

Najczęściej stosowanymi funkcjami testującymi do określania granic zakresów są: <, <=, >>.

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])
;; zakres jednostronny (subseq (sorted-map :a 1, :b 2, :c 3) &lt;= :b) ; =&gt; ([:a 1] [:b 2]) ;; zakres dookreślony (subseq (sorted-map :a 1, :b 2, :c 3) &gt; :a &lt;= :b) ; =&gt; ([: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}
;; zakres jednostronny (rsubseq (sorted-map :a 1, :b 2, :c 3) &lt;= :b) ; =&gt; ([:b 2] [:a 1]) ;; zakres dookreślony (rsubseq (sorted-map :a 1, :b 2, :c 3) &gt; :a &lt;= :c) ; =&gt; ([:c 3] [:b 2]) ;; konwersja do mapy (into {} (rsubseq (sorted-map :a 1, :b 2, :c 3) &lt;= :b)) ; =&gt; {: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}
(clojure.set/rename-keys {:a 1, :b 2} {:a :x}) ; =&gt; {: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}
(map-invert {:a 1, :b 2, :c 1}) ; =&gt; {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}
(merge {:a 1 :b 2} {:b 10 :c 3}) ; =&gt; {:c 3, :b 10, :a 1}

Złączenie z przesłanianiem, merge-with

Złączenie map z lewostronnym przesłanianiem wartości w przypadku takich samych kluczy i z przemianowywaniem wartości przez podany operator możliwe jest dzięki funkcji merge-with.

Przyjmuje ona jeden obowiązkowy argument (funkcję operującą), która powinna przyjmować tyle argumentów, ile podano map, a następnie dokonywać łączenia podanych wartości w oczekiwany sposób (np. przez złączenie kolekcji czy sumowanie liczb). Kolejnymi, opcjonalnymi argumentami funkcji są mapy, które będą używane jako źródło danych.

Funkcja zwraca mapę będącą złączeniem map podanych jako argumenty.

Użycie:

  • (merge-with operator & mapa…).
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}
(merge-with str {:a &#34;a&#34;, :b &#34;b&#34;} {:c &#34;c&#34;}) ; =&gt; {:c &#34;c&#34;, :b &#34;b&#34;, :a &#34;a&#34;} (merge-with str {1 8, :a &#34;a&#34;, :b &#34;b&#34;} {:c &#34;c&#34;, :a &#34;X&#34;} {2 2}) ; =&gt; {2 2, :c &#34;c&#34;, 1 8, :b &#34;b&#34;, :a &#34;aX&#34;} (merge-with + {:a 1, :b 2} {:c 3, :a 9}) ; =&gt; {: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}
(select-keys {:a 1, :b 2, :c 3, :d 4} [:a :d]) ; =&gt; {: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 (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)
; => #<[email protected]>
(create-struct :jabłka :banany :chleby :mleka) ; =&gt; #&lt;[email protected]&gt;

Definiowanie struktur, defstruct

Funkcja defstruct tworzy strukturę i umieszcza odniesienie do niej w zmiennej globalnej. Pierwszym argumentem powinna być nazwa zmiennej wyrażona niezacytowanym symbolem, a kolejnymi nazwy kluczy. Wartością zwracaną jest zmienna globalna (obiekt typu Var) odnosząca się do struktury bazowej (typu clojure.lang.PersistentStructMap).

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

Użycie:

  • (defstruct symbol klucz & klucz…).

Przykład użycia funkcji defstruct
1
2
(defstruct zakupy :jabłka :banany :chleby :mleka)
; => #'user/zakupy
(defstruct zakupy :jabłka :banany :chleby :mleka) ; =&gt; #&#39;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 obiekt struktury i argumenty opcjonalne w postaci par wyrażających początkowe wartości poszczególnych pól. Pierwszymi elementami par powinny być klucze o nazwach takich samych, jak nazwy pól, zaś drugimi nadawane wartości.

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

Użycie:

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

gdzie klucz–wartość to:

  • klucz wartość.

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}
(defstruct zakupy :jabłka :banany :chleby :mleka) (struct-map zakupy :jabłka 2 :banany 4 :chleby 2) ; =&gt; {: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}
(defstruct zakupy :jabłka :banany :chleby :mleka) (struct zakupy 2 4 2) ; =&gt; {: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 mapach, włączając w to dynamiczne dodawanie kluczy, które nie zostały zdefiniowane we wzorcowej strukturze. Jest tylko jeden wyjątek: nie można usuwać elementów, których klucze zdefiniowano.

Poza interfejsem mapowym mapy strukturalizowane wyposażone są w szybkie akcesory dla zdefiniowanych pól.

Dostęp do pól, accessor

Funkcja accessor zwraca funkcję, której można użyć do szybkiego odczytu wybranego pola konkretnej instancji struktury. Wewnętrznie zawiera ona wywołanie metody odczytującej odpowiedni atrybut obiektu.

Jako pierwszy argument należy podać obiekt struktury, a jako drugi wyrażoną słowem kluczowym nazwę pola. Funkcja zwraca obiekt funkcyjny.

Użycie:

  • (accessor struktura klucz).
Przykład użycia funkcji accessor
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
;; Definiujemy strukturę.
(defstruct zakupy :jabłka, :banany :chleby :mleka)

;; Tworzymy funkcję akcesora dla pola jabłka.
(def liczba-jabłek (accessor zakupy :jabłka))

;; Tworzymy mapę na bazie struktury.
(def wtorkowe-zakupy (struct-map zakupy :jabłka 2 :banany 4))
; => {:jabłka 2, :banany 4, :chleby nil, :mleka nil}

;; Odczytujemy liczbę jabłek z mapy.
(liczba-jabłek wtorkowe-zakupy)
; => 2
;; Definiujemy strukturę. (defstruct zakupy :jabłka, :banany :chleby :mleka) ;; Tworzymy funkcję akcesora dla pola jabłka. (def liczba-jabłek (accessor zakupy :jabłka)) ;; Tworzymy mapę na bazie struktury. (def wtorkowe-zakupy (struct-map zakupy :jabłka 2 :banany 4)) ; =&gt; {:jabłka 2, :banany 4, :chleby nil, :mleka nil} ;; Odczytujemy liczbę jabłek z mapy. (liczba-jabłek wtorkowe-zakupy) ; =&gt; 2

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
(set [1 2 3 4 :a :b :c]) ; jeden argument, który jest kolekcją (set &#39;(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 &#34;abcdef&#34;) ; 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
(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
(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ć mniejsza, równa czy większa od pozycji argumentu drugiego.

Opcjonalne argumenty przyjmowane przez sorted-set-by to wartości, które staną się elementami tworzonego zbioru. Funkcja zwraca zbiór sortowany.

Użycie:

  • (sorted-set-by komparator & element…).
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)  ; => #{}
(sorted-set-by &gt; 1 8 4) ; =&gt; #{1 4 8} (sorted-set-by &gt; 1 8 4) ; =&gt; #{}

Zbiorowe S-wyrażenia

Zbiorowe S-wyrażenie (ang. set S-expression) to element składniowy pozwalający w przejrzysty sposób tworzyć zbiory. Składa się z zestawu wartości ujętych w nawiasy klamrowe poprzedzone symbolem kratki lub z samych nawiasów klamrowych poprzedzonych tym symbolem (w przypadku zbiorów pustych). Separatorem wartości jest znak spacji lub przecinka, albo obydwa te znaki.

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

Użycie:

  • #{},
  • #{element…}.
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
#{} ; 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ć zacytowane. W takim przypadku podawane elementy nie będą wartościowane, nawet jeżeli są formami, które nie wyrażają wartości własnych.

Użycie:

  • '#{},
  • '#{element…},
  • (quote #{}),
  • (quote #{element…}).

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}
&#39;{} ; =&gt; {} &#39;{:a x, :b y} ; =&gt; {:a x, :b y} (quote {}) ; =&gt; {} (quote {:a x, :b y}) ; =&gt; {:a x, :b y}

Dostęp do zbiorów

Pobieranie elementu, get

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

Użycie:

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

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

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
(get #{1,2,3} 2) ; =&gt; 2 (get #{1,2,3} 5) ; =&gt; nil (get #{1,2,3} 5 :nic) ; =&gt; :nic

Przeszukiwanie zbiorów

Zbiór jako funkcja

Obiekty zbiorów wyposażone są w funkcyjny interfejs, tzn. są również specyficznymi funkcjami. Na poziomie semantycznym mogą stawać się formami przeszukiwania zbiorów. Przyjmują wtedy jeden argument wyrażający wartość poszukiwanego elementu (podobnie jak get), a zwracają tę wartość, jeżeli znajduje się ona w zbiorze. W przeciwnym przypadku zwracana jest wartość nieustalona nil.

Użycie:

  • (zbiór wartość).
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
(#{1,2,3} 2) ; =&gt; 2 (#{1,2,3} 5) ; =&gt; nil (let [zbiór #{1 2}] (zbiór 1)) ; =&gt; 1

Słowo kluczowe jako funkcja

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

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

Użycie:

  • (słowo-kluczowe zbiór wartość-domyślna?).

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
(:a #{:a :b :c}) ; =&gt; :a (:e #{:a :b :c}) ; =&gt; nil (:e #{:a :b :c} :x) ; =&gt; :x (let [zbiór :a #{:a}] (:a zbiór)) ; =&gt; :a

Wyszukiwanie elementu, contains?

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

Użycie:

  • (contains? zbiór wartość).

Przykłady użycia funkcji contains?
1
2
(contains? #{1,2,3} 2)  ; => true
(contains? #{1,2,3} 5)  ; => false
(contains? #{1,2,3} 2) ; =&gt; true (contains? #{1,2,3} 5) ; =&gt; 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 generycznej funkcji conj.

Użycie:

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

Funkcja conj przyjmuje dwa obowiązkowe argumenty: zbiór źródłowy i element, który powinien zostać dodany do zbioru. Opcjonalnie można podać więcej elementów jako kolejne argumenty.

Wartością zwracaną jest nowy zbiór z dodanymi elementami.

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

Usuwanie elementów, disj

Usuwanie elementu (tworzenie zbioru z usuniętymi wybranymi elementami) polega na wywołaniu generycznej funkcji disj. Przyjmuje ona jeden obowiązkowy argument (zbiór źródłowy) i zero lub więcej argumentów określających wartości usuwanych elementów. Funkcja zwraca nowy zbiór z usuniętymi wybranymi elementami.

Użycie:

  • (disj zbiór & element…).
Przykład użycia funkcji disj
1
2
(disj #{1 2 3 4} 1)
; => #{4 3 2}
(disj #{1 2 3 4} 1) ; =&gt; #{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"  } }
(clojure.set/join #{ ; pierwszy zbiór par {:cena 1, :nazwa &#34;sok&#34; } ; klucz–wartość {:cena 1, :nazwa &#34;chleb&#34;} {:cena 2, :nazwa &#34;chleb&#34;} {:cena 3, :nazwa &#34;masło&#34;}} #{ ; drugi zbiór par {:nazwa &#34;sok&#34;, :waga 8} {:nazwa &#34;masło&#34;, :waga 4} {:nazwa &#34;chleb&#34;, :waga 6}}) ; =&gt; #{ {:cena 2, :waga 6, :nazwa &#34;chleb&#34;} ; =&gt; {:cena 3, :waga 4, :nazwa &#34;masło&#34;} ; =&gt; {:cena 1, :waga 6, :nazwa &#34;chleb&#34;} ; =&gt; {:cena 1, :waga 8, :nazwa &#34;sok&#34; } }

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"} }
(clojure.set/join #{ ; pierwszy zbiór par {:cena 1, :nazwa &#34;sok&#34; } ; klucz–wartość {:cena 1, :nazwa &#34;chleb&#34;} {:cena 2, :nazwa &#34;chleb&#34;} {:cena 3, :nazwa &#34;masło&#34;}} #{ ; drugi zbiór par {:towar &#34;sok&#34;, :waga 8} {:towar &#34;masło&#34;, :waga 4} {:towar &#34;chleb&#34;, :waga 6}} {:nazwa :towar}) ; mapa kluczy ; =&gt; #{ {:nazwa &#34;masło&#34;, :cena 3, :waga 4, :towar &#34;masło&#34;} ; =&gt; {:nazwa &#34;chleb&#34;, :cena 1, :waga 6, :towar &#34;chleb&#34;} ; =&gt; {:nazwa &#34;sok&#34;, :cena 1, :waga 8, :towar &#34;sok&#34; } ; =&gt; {:nazwa &#34;chleb&#34;, :cena 2, :waga 6, :towar &#34;chleb&#34;} }

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"} }
(clojure.set/project #{ {:cena 1, :nazwa &#34;sok&#34; } ; klucz–wartość {:cena 1, :nazwa &#34;chleb&#34;} {:cena 2, :nazwa &#34;chleb&#34;} {:cena 3, :nazwa &#34;masło&#34;}} [:nazwa]) ; klucze do pozostawienia ; =&gt; #{ {:nazwa &#34;sok&#34; } ; =&gt; {:nazwa &#34;masło&#34;} ; =&gt; {:nazwa &#34;chleb&#34;} }

Podzbiór, select

Wybieranie podzbioru elementów o wskazanych kryteriach umożliwia funkcja 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}) ; even? zwraca true dla parzystych
; => #{4 6 2}
(clojure.set/select even? #{1 2 3 4 5 6}) ; even? zwraca true dla parzystych ; =&gt; #{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}
(clojure.set/union #{1 2} #{2 3}) ; =&gt; #{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}
(clojure.set/difference #{1 2} #{2 3}) ; =&gt; #{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}
(clojure.set/intersection #{1 2} #{2 3}) ; =&gt; #{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} }
(def relacje #{ {:a 1, :b 2} {:a 3, :b 4} }) (clojure.set/rename relacje {:a :b, :b :a}) ; zamiana :a z :b ; =&gt; #{ {:b 3, :a 4} {:b 1, :a 2} } (clojure.set/rename relacje {:a :x}) ; zamiana :a na :x ; =&gt; #{ {: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
19
20
(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}}}
(def ludzie #{ {:imię &#34;Damian&#34;, :wiek 17} {:imię &#34;Janina&#34;, :wiek 19} {:imię &#34;Zuzanna&#34;, :wiek 31} {:imię &#34;Zuzanna&#34;, :wiek 19} }) ;; grupowanie po wieku (clojure.set/index ludzie [:wiek]) ; =&gt; { {:wiek 19} #{ {:imię &#34;Janina&#34;, :wiek 19} ; =&gt; {:imię &#34;Zuzanna&#34;, :wiek 19} }, ; =&gt; {:wiek 31} #{ {:imię &#34;Zuzanna&#34;, :wiek 31} }, ; =&gt; {:wiek 17} #{ {:imię &#34;Damian&#34;, :wiek 17}}} ;; grupowanie po imieniu (clojure.set/index ludzie [:imię]) ; =&gt; { {:imię &#34;Janina&#34;} #{ {:imię &#34;Janina&#34;, :wiek 19} }, ; =&gt; {:imię &#34;Zuzanna&#34;} #{ {:imię &#34;Zuzanna&#34;, :wiek 31} ; =&gt; {:imię &#34;Zuzanna&#34;, :wiek 19} }, ; =&gt; {:imię &#34;Damian&#34;} #{ {:imię &#34;Damian&#34;, :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
13
14
15
16
17
18
19
20
(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}}}
(def ludzie #{ {:imię &#34;Damian&#34;, :wiek 17, :wzrost 180} {:imię &#34;Janina&#34;, :wiek 19, :wzrost 180} {:imię &#34;Zuzanna&#34;, :wiek 19, :wzrost 210} {:imię &#34;Zuzanna&#34;, :wiek 19, :wzrost 168}}) ;; grupowanie po wieku i imieniu (clojure.set/index ludzie [:wiek :imię]) ; =&gt; { {:imię &#34;Janina&#34;, :wiek 19} #{ {:imię &#34;Janina&#34; ; =&gt; :wzrost 180 ; =&gt; :wiek 19}}, ; =&gt; {:imię &#34;Damian&#34;, :wiek 17} #{ {:imię &#34;Damian&#34; ; =&gt; :wzrost 180 ; =&gt; :wiek 17}}, ; =&gt; {:imię &#34;Zuzanna&#34;, :wiek 19} #{ {:imię &#34;Zuzanna&#34; ; =&gt; :wzrost 168 ; =&gt; :wiek 19}, ; =&gt; {:imię &#34;Zuzanna&#34; ; =&gt; :wzrost 210 ; =&gt; :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}
(rseq (sorted-set :a, :b, :c)) ; =&gt; (:c :b :a) ;; konwersja do zbioru (into #{} (rseq (sorted-set :a, :b, :c))) ; =&gt; #{: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ą ze swobodnym rodzajem dostępu. Jeżeli więc zależy nam na zachowaniu struktury, możemy przekształcić rezultat rseq do zbioru, na przykład korzystając z funkcji into.

Sekwencja z zakresu, subseq

Funkcja subseq pozwala na tworzenie sekwencji zawierającej elementy określone zakresem wyznaczonym operatorami i wartościami przekazanymi jako argumenty. Przyjmuje ona 3 lub 5 argumentów.

W wersji trójargumentowej należy podać zbiór sortowany, funkcję testującą i wartość przekazywaną jako drugi argument funkcji testującej (pierwszym będzie klucz kolejno przetwarzanego elementu podczas porównywania). Jeżeli na przykład podamy > 2, w wynikowej sekwencji znajdą się wyłącznie elementy, których klucze są wartościami większymi od 2.

W wersji pięcioargumentowej należy również podać zbiór, jednak kolejne 4 argumenty to pary określające zakresy: funkcja testująca i wartość dla dolnej granicy zakresu, a następnie funkcja testująca i wartość dla górnej granicy zakresu.

Funkcja zwraca sekwencję wartości z określonego zakresem fragmentu zbioru.

Najczęściej stosowanymi funkcjami testującymi do określania granic zakresów są: <, <=, >>.

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)
;; zakres jednostronny (subseq (sorted-set :a, :b, :c) &lt;= :b) ; =&gt; (:a :b) ;; zakres dookreślony (subseq (sorted-set :a, :b, :c) &gt; :a &lt;= :b) ; =&gt; (: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}
;; zakres jednostronny (rsubseq (sorted-set :a :b :c) &lt;= :b) ; =&gt; (:b :a) ;; zakres dookreślony (rsubseq (sorted-set :a, :b, :c) &gt; :a &lt;= :c) ; =&gt; (:c :b) ;; konwersja do zbioru (into #{} (rsubseq (sorted-set :a, :b, :c) &lt;= :b)) ; =&gt; #{: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. Funkcja przyjmuje dwa obowiązkowe argumenty: kolekcję i dodawany element. Opcjonalnie można podać kolejne elementy wyrażone wartościami kolejnych argumentów.

Umiejscowienie dodawanych elementów zależy od konkretnej kolekcji. Na przykład w przypadku list będą to ich czoła, a w przypadku wektorów końce.

Funkcja zwraca kolekcję z dodanymi elementami.

Użycie:

  • (conj kolekcja element & element…).

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}
(conj &#39;(1 2 3) 4 5) ; =&gt; (5 4 1 2 3) (conj [1 2 3] 4 5) ; =&gt; [1 2 3 4 5] (conj {:a 1, :b 2} [:c 3]) ; =&gt; {:c 3, :b 2, :a 1} (conj {:a 1, :b 2} {:c 3}) ; =&gt; {: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żeli podane są puste). Przyjmuje ona dwa obowiązkowe argumenty: kolekcję docelową i kolekcję źródłową. Wartością zwracaną jest kolekcja tego samego typu, co kolekcja docelowa z dołączonymi elementami pochodzącymi z kolekcji źródłowej.

Użycie:

  • (into kolekcja-docelowa kolekcja-źródłowa).

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)
(into () &#39;(1 2 3)) ; =&gt; (3 2 1) (into [] &#39;(1 2 3)) ; =&gt; [1 2 3] (into (sorted-map) [{:c 3} [:b 2]]) ; =&gt; {:b 2, :c 3} (into &#39;(:a :b :c) [:d]) ; =&gt; (:d :a :b :c)

Tworzenie podobnych kolekcji

Podobna kolekcja, empty

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

Użycie:

  • (empty kolekcja).

Przykłady użycia funkcji empty
1
2
(empty [1 2 3])  ; => []
(empty ())       ; => ()
(empty [1 2 3]) ; =&gt; [] (empty ()) ; =&gt; ()

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)
(not-empty ()) ; =&gt; nil (not-empty &#39;(1 2 3)) ; =&gt; (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
(= &#39;(1 2 3) &#39;(1 2 3)) ; =&gt; true (= &#39;(1 3 2) &#39;(1 2 3)) ; =&gt; false (= [1 2 3] [1 3 2]) ; =&gt; false (= [1 2 3] [1 2 3] [1 2 3]) ; =&gt; 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
(count &#39;(1 2 3)) ; =&gt; 3

Testowanie kolekcji

Testowanie typów

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

  • (coll?   wartość)true, gdy wartość jest kolekcją;
  • (list?   wartość)true, gdy wartość jest listą;
  • (vector? wartość)true, gdy wartość jest wektorem;
  • (set?    wartość)true, gdy wartość jest zbiorem;
  • (map?    wartość)true, gdy wartość jest mapą;
  • (record? wartość)true, gdy wartość jest rekordem;
  • (seq?    wartość)true, gdy wartość jest (również) sekwencją.

Warto zauważyć, że spośród głównych kolekcji jedynie listę (typu clojure.lang.PersistentList) można nazwać sekwencją, ponieważ implementuje ona interfejs clojure.lang.ISeq.

Testowanie cech

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

  • (seqable?     wartość)true, gdy możliwa sekwencyjna reprezentacja;
  • (sequential?  wartość)true, gdy ma sekwencyjny interfejs;
  • (associative? wartość)true, gdy jest asocjacyjna;
  • (sorted?      wartość)true, gdy jest sortowana;
  • (counted?     wartość)true, gdy jest policzalna w skończonym czasie;
  • (reversible?  wartość)true, gdy można odwracać kolejność elementów.

Warto w tym miejscu krótko omówić różnicę między kolekcją, którą można reprezentować sekwencyjnie, a taką, która ma sekwencyjny interfejs. Reprezentowanie kolekcji w postaci sekwencji polega na użyciu względem niej funkcji seq. Zwraca ona obiekt pełnoprawnej sekwencji skojarzonej z elementami podanej kolekcji. Gdy na takim obiekcie wywołamy funkcję seq?, zwróci ona prawdę. Zanim dokonamy przekształcenia kolekcji w sekwencję, możemy sprawdzić, czy taka operacja jest możliwa, wywołując właśnie seqable?.

Z kolei sequential? pozwala sprawdzić, czy kolekcja implementuje Sequential – interfejs Javy używany w roli znacznika i dodawany do kolekcji, dla których możliwy jest następczy dostęp do ich elementów (np. z użyciem funkcji firstrest). Nie oznacza to jednak, że takie kolekcje są sekwencjami. Przykładem może być tu obiekt wektora – będzie on sekwencyjny (można uzyskiwać dostęp do kolejnych elementów), lecz nie będzie sekwencją. Możemy jednak wytworzyć sekwencję bazującą na wektorze (z użyciem funkcji seq).

Różność, distinct?

Sprawdzanie czy dwie lub więcej kolekcji jest od siebie różnych możliwe jest dzięki funkcji distinct?.

Użycie:

  • (distinct? kolekcja & inna-kolekcja…).

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
(distinct? &#39;(1 2 3) &#39;(1 3 2) &#39;(1 2 3)) ; =&gt; true (distinct? &#39;(1 2 3) &#39;(1 2 3)) ; =&gt; 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
(empty? ()) ; =&gt; true (empty? []) ; =&gt; true (empty? nil) ; =&gt; true (empty [1 2 3]) ; =&gt; 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
(every? even? [1 2 3]) ; czy każdy element jest parzysty? ; =&gt; false ; nie, nie każdy (every? even? [2 4 6]) ; czy każdy element jest parzysty? ; =&gt; 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
(not-every? even? [1 2 3]) ; czy choć jeden element nie jest parzysty? ; =&gt; true ; tak, przynajmniej jeden z nich nie jest (not-every? even? [2 4 6]) ; czy choć jeden element nie jest parzysty? ; =&gt; 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żeli ż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
(some even? [1 2 3]) ; czy choć jeden element jest parzysty? ; =&gt; true ; tak, funkcja even? dla 2 zwraca true (some even? [1 3 5]) ; czy choć jeden element jest parzysty? ; =&gt; 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
(not-any? even? [1 2 3]) ; czy żaden element nie jest parzysty? ; =&gt; false ; nie, występują nieparzyste (not-any? even? [1 3 5]) ; czy żaden element nie jest parzysty? ; =&gt; true ; tak, nie ma ani jednego innego

Obsługa metadanych

Kolekcje, podobnie jak symbole i obiekty referencyjne, można opcjonalnie wyposażać w metadane – pomocnicze mapy, które przechowują dodatkowe informacje.

Odczytywanie metadanych, meta

Aby pobrać metadane dla kolekcji, należy skorzystać z funkcji meta.

Użycie:

  • (meta kolekcja).

Funkcja meta przyjmuje kolekcję, a zwraca mapę metadanową, jeżeli kolekcji przypisano jakieś metadane lub wartość nieustaloną nil w przeciwnym razie.

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}
(meta &#39;(1 2)) ; =&gt; {:column 8 :line 1} (meta [1 2]) ; =&gt; nil (meta &#39;^:testowa [1 2]) ; =&gt; {:testowa true} (def x ^:flaga {:a :b}) (meta x) ; =&gt; {:flaga true}

Zauważmy, że dla zacytowanego, listowego S-wyrażenia niektóre metadane będą ustawiane automatycznie (np. kolumna i linia pliku źródłowego). Dzieje się tak dlatego, że listowe S-wyrażenia są elementami strukturalizującymi kod źródłowy programu.

Dodawanie metadanych, with-meta

Aby ustawić własne metadane dla kolekcji, trzeba użyć funkcji with-meta.

Użycie:

  • (with-meta kolekcja metadane).

Jako pierwszy argument funkcji with-meta należy przekazać kolekcję, a jako drugi mapę zawierającą klucze i przypisane do nich wartości metadanych, które powinny być przypisane kolekcji.

Wartością zwracaną jest kolekcja z ustawionymi metadanymi.

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"}
(meta (with-meta [1 2] {:klucz &#34;wartość&#34;})) ; =&gt; {:klucz &#34;wartość&#34;} (meta (with-meta {:a 1} {:k &#34;v&#34;})) ; =&gt; {:k &#34;v&#34;} (meta (with-meta #{:a} {:k &#34;v&#34;})) ; =&gt; {:k &#34;v&#34;} (meta (with-meta &#39;(1 2) {:k &#34;v&#34;})) ; =&gt; {:k &#34;v&#34;} (meta (with-meta (list) {:k &#34;v&#34;})) ; =&gt; {:k &#34;v&#34;}

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

Zobacz także:

Jesteś w sekcji

comments powered by Disqus