Poczytaj mi Clojure – cz. 9: Sekwencje

Sekwencje to przypominający iteratory mechanizm operowania na wieloelementowych strukturach (najczęściej kolekcjach). Są niemutowalnymi, abstrakcyjnymi listami, dzięki którym możliwy jest następczy dostęp do danych. W programach pisanych w Clojure często się z nich korzysta.

Sekwencje

Sekwencja (ang. sequence, skr. seq) jest specjalnym, abstrakcyjnym rodzajem listy, która pozwala na następczy dostęp do elementów powiązanej z nią struktury danych (np. kolekcji), a czasem nawet na wyliczanie wartości elementów dopiero wtedy, gdy pojawia się żądanie takiego dostępu (w przypadku tzw. leniwych sekwencji).

Sekwencja nie zawiera konkretnych elementów, lecz jest sposobem dostępu do tych, które istnieją już w jakiejś kolekcji lub są stwarzane przez jakąś funkcję. Kolekcje, które chcą być w niego wyposażone, powinny implementować interfejs ISeq.

Następczy sposób oznacza, że istnieją specyficzne dla sekwencji funkcje, pozwalające na pozyskiwanie i przeliczanie wartości kolejnych elementów źródłowej struktury w ujednolicony sposób.

Zaznajomieni z paradygmatem imperatywnym mogą zauważyć, że dostęp sekwencyjny przypomina korzystanie z pętli typu foreach czy while, znanych z innych języków programowania. Z kolei programiści obiektowi mogą zainteresować się sekwencjami, rozwiązując problemy, w których skorzystaliby z iteratorów (ang. iterators). Warto pamiętać, że sekwencje różnią się od nich ważnym szczegółem – są trwałe (ang. persistent), tzn. podczas modyfikowania zawsze tworzona jest ich nowa wersja, a więc są w praktyce niemutowalne (ang. immutable). Oznacza to, że nie będziemy mieli do czynienia np. z zapamiętywaniem bieżącej pozycji tzw. kursora wewnątrz obiektu sekwencji, gdyż nie ma w niej miejsca, w którym dałoby się taką informację przechować. Nic nie stoi jednak na przeszkodzie, aby wytworzyć sekwencję pochodną, której pierwszym i zarazem bieżącym elementem będzie któryś z kolei element sekwencji bazowej. Dzięki wspomnianym cechom sekwencje są bezpieczne w kontekście przetwarzania współbieżnego.

Większość funkcji operujących na sekwencjach realizuje dostęp do rezultatów w sposób zwłoczny, czyli są to tzw. funkcje leniwe (ang. lazy). Oznacza to, że przeliczanie wartości kolejnych elementów wyrażanych sekwencją odbywa się na żądanie (gdy następuje próba ich odczytu), a nie dochodzi do generowania struktur z wynikami dla każdej wartości z zestawu. Dzięki temu leniwe sekwencje mogą być nieskończone, np. reprezentować ciąg liczbowy będący rezultatem zastosowania określonej funkcji.

Sekwencyjny interfejs dostępu może być użyty w odniesieniu do wbudowanych, mutowalnych tablic Javy, których zawartość może ulegać zmianom. Jeśli tak się stanie, to zmieniały będą się wtedy dane wyrażane przez powiązaną z taką strukturą sekwencję. Aby uzyskać niezmienność tej ostatniej, należy stworzyć jej niemutowalną kopię.

Komórki cons

Elementy leniwych sekwencji przypominają znane z Lispa komórki cons (składniki list), jednak w Clojure różnią się one od nich w przeznaczeniu i implementacji. W Lispie główna (pierwsza) komórka listy w swoim slocie car zawiera właściwy element, a w slocie cdr odwołanie do następnej komórki. W przypadku leniwych sekwencji języka Clojure czołowa komórka zawiera pierwszy element oraz funkcję, która pozwala obliczyć wartość następnej komórki na podstawie pierwszej.

Tak naprawdę interfejs sekwencji bazuje na trzech funkcjach: służących do uzyskiwania pierwszego elementu i elementów pozostałych (funkcje firstrest) oraz dodawania nowego elementu do czoła sekwencji (funkcja cons). Gdy użyta jest ta ostatnia funkcja, to powstaje element, który ma dwa sloty: pierwszy zawiera dodawaną wartość, a drugi wskazanie na oryginalną sekwencję (jej pierwszy element). Dzięki temu w Clojure można przezroczyście łączyć użycie kolekcji i sekwencji, tworząc naprzemienne łańcuchy odwołań.

Dokładniej można to wyjaśnić, przyglądając się działaniu funkcji cons, która dla podanego elementu i sekwencji dokonuje dodania tego elementu do jej czoła.

Przykład użycia funkcji cons na sekwencji
1
2
3
4
5
6
7
8
;; nazywamy sekwencję reprezentującą zakres
(def sekwencja (range 1 5))  ; => #'user/sekwencja

;; REPL zmusi sekwencję do przeliczenia
sekwencja                    ; => (1 2 3 4)

;; dodajemy 0 do czoła sekwencji
(cons 0 sekwencja)           ; => (0 1 2 3 4)

Użyta w przykładzie funkcja range tworzy zakres liczb reprezentowany obiektem clojure.lang.LongRange o sekwencyjnym dostępie. Spójrzmy na typy danych, z jakimi mieliśmy do czynienia:

1
2
3
(def  sekwencja (range 1 5))  ; => #'user/sekwencja
(type sekwencja)              ; => clojure.lang.LongRange
(type (cons 0 sekwencja))     ; => clojure.lang.Cons

Widzimy, że po dodaniu nowego elementu do czoła sekwencji, typem uzyskanego obiektu nie jest już LongRange (jak po wywołaniu range), lecz Cons. Czy oznacza to, że nie jest to już sekwencja? Tak naprawdę powinniśmy zapytać, czy zachowuje się jak sekwencja, tzn. czy możemy operować na uzyskanym obiekcie jak na sekwencji.

Obiekty klasy clojure.lang.Cons są budulcem tzw. leniwych list (wspomnianych przy okazji omawiania kolekcji). Wymogiem użycia funkcji cons na kolekcji jest, aby wyposażona była ona w interfejs ISeq, czyli efektywnie była też sekwencją. Również obiekt Cons wyposażony jest w taki interfejs, więc może służyć do tworzenia specjalnego rodzaju list (standardowe listy, konstruowane z użyciem list, mają w Clojure inny typ i przechowują wewnątrz informację o rozmiarze).

Obiekt Cons składa się – podobnie jak w innych Lispach – z dwóch slotów. Pierwszy zawiera zwykle odwołanie do ustalonej wartości, a drugi do takiego obiektu, który również implementuje ISeq. W ten sposób z obiektów typu Cons można budować łańcuchy składające się z różnorakich kolekcji. Właściwość tę wykorzystuje się również do konstruowania leniwych sekwencji, aby nie tworzyć bytów ponad miarę.

Tworzenie odwołań do istniejących sekwencji w odpowiednikach slotów cdr komórek cons jest możliwe, ponieważ obiekty klasy LazySeq (leniwe sekwencje) też wyposażone są w interfejs ISeq. Z kolei obiekt typu LazySeq może zawierać odniesienie do podprogramu generującego wartość danego elementu, który po uruchomieniu znów zwróci obiekt wyposażony w interfejs ISeq. Gdy leniwa sekwencja będzie wartościowana, to właśnie ten wspomniany podprogram zostanie uruchomiony – ale tylko jeden raz! Jeśli dany element został już obliczony, to wskazanie na funkcję obliczającą jest zastępowane wskazaniem na zapamiętany wynik.

Buforowanie rezultatów leniwych sekwencji następuje podczas ich przeliczania. Dla każdego elementu utrzymywany jest w pamięci odpowiedni obiekt z rezultatem. Kolejne odwołanie się do tego samego elementu nie doprowadzi więc do uruchomienia funkcji generującej, ale zwrócenia wyniku z pamięci podręcznej. Zysk wydajnościowy można zaobserwować szczególnie wtedy, gdy mamy do czynienia z dostępem do elementu o dużym numerze kolejnym. Nawet, jeśli jego wartość nie została jeszcze obliczona, to przeliczanie rozpocznie się od ostatnio zapamiętanej.

Jeżeli czoło (ang. head) sekwencji, czyli jej pierwsza komórka, zostanie powiązana z jakimś obiektem referencyjnym (np. zmienną globalną), to z powodu wzajemnej zależności wszystkich kolejnych elementów rezultaty przechowywane w przypisanych do nich podręcznych buforach nie zostaną zwolnione przez Garbage Collectora. Mówimy wtedy o przytrzymywaniu czoła sekwencji (ang. holding the head of the sequence), zwanego też retencją czołową (ang. head retention). Prowadzi ono do zachowania całej przeliczonej sekwencji w pamięci. Niektóre z funkcji operujących na sekwencjach mogą z tego korzystać, żeby w pamięci pozostawały zbuforowane wartości elementów do ponownego użycia. To, czy jest to pożądane, zależy od konkretnego zastosowania. Są na przykład takie sekwencje, których wartości elementów nie powinny być zapamiętywane, ponieważ ich liczba może doprowadzić do zajęcia całej dostępnej pamięci. Są też takie, które chcemy buforować, ale tylko w określonym zasięgu leksykalnym – możemy wtedy np. użyć let i powiązać sekwencję z jakimś symbolem na czas trwania obliczeń. W takim przypadku cała sekwencja będzie zapisana w pamięci, ale tylko przez określony czas. Wybór konkretnej strategii podyktowany jest aktualną potrzebą.

Wracając do szczegółów konstrukcyjnych: często będziemy mieli do czynienia z sytuacją, w której leniwa sekwencja (LazySeq) będzie zawierała odniesienia do tworzonych przez jej funkcję generującą obiektów Cons (implementujących interfejs ISeq), które z kolei będą przechowywały odwołania do kolejnych obiektów typu LazySeq. Powstanie więc pewnego rodzaju łańcuch referencji (LazySeq -> Cons -> LazySeq -> Cons itd.).

Przykład funkcji zwracającej leniwą sekwencję
1
2
3
4
5
6
7
8
9
10
11
12
(defn potęga [x]    ; definiujemy funkcję,
  (* x x))          ; która potęguje podaną liczbę

(defn potęgi [x]    ; definiujemy funkcję,
  (lazy-seq         ; która zwraca leniwą sekwencję, gdzie
   (cons            ;  generatorem jest funkcja tworząca obiekt Cons
    (potęga x)      ;  · złożony z rezultatu potęgowania (lewy slot)
    (potęgi         ;  · i z sekwencji będącej rezultatem
     (inc x)))))    ;    rekurencyjnego wywołania dla x+1 (prawy slot) 

(take 3 (potęgi 1)) ; pobieramy pierwsze 3 elementy
; => (1 4 9)

Sposobem na uniknięcie niedogodności związanych z klasami obiektów zwracanymi przez cons (a także z faktem, że elementy typu Cons nie są policzalne w stałym czasie, w przeciwieństwie np. do list, które zawierają odpowiednie pole określające rozmiar), może być korzystanie z funkcji conj.

Tworzenie sekwencji

Do tworzenia sekwencji służy szereg funkcji, które mogą być użyte w zależności od tego, co będzie wykorzystane jako źródło danych. Tym ostatnim mogą być:

  • kolekcje,
  • inne sekwencje,
  • ustalone wartości,
  • funkcje generujące,
  • inne obiekty.

Większość funkcji zwraca tzw. leniwe sekwencje (ang. lazy sequences).

Z kolekcji

Funkcja generyczna, seq

Do tworzenia sekwencji bazującej na kolekcji służy funkcja seq. Jej argumentem powinna być kolekcja. Jeśli argument pominięto zwracaną wartością jest nil.

Rezultat zwłoczny: (zależy od wejścia).

Użycie:

  • (seq kolekcja).
Przykłady użycia funkcji seq
1
2
3
(seq (list 1 2 3))  ; => (1 2 3)
(seq [1 2 3])       ; => (1 2 3)
(seq {:a 1, :b 2})  ; => ([:b 2] [:a 1])

Warto zauważyć, że w REPL sekwencje przedstawiane są z użyciem symbolicznie wyrażonych list, chociaż naprawdę nie są one listami w ścisłym rozumieniu. Można się o tym przekonać, sprawdzając typ:

1
2
(type (seq [1 2 3]))
; => clojure.lang.PersistentVector$ChunkedSeq

Funkcja seq potrafi tworzyć sekwencje na bazie dowolnych obiektów Javy, które implementują interfejs Iterable. Jeżeli obiekt ten działa w sposób zwłoczny, to zwrócona zostanie leniwa sekwencja z obiektem Cons na czele.

Z map, vals

Funkcja vals pozwala tworzyć sekwencję na bazie wartości mapy.

Rezultat zwłoczny: NIE.

Użycie:

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

Pokrewną funkcją jest keys, która robi to samo, ale dla kluczy.

Rezultat zwłoczny: NIE.

Użycie:

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

Z odwróconych kolekcji, rseq

Funkcja rseq w stałym czasie zwraca sekwencję na bazie podanej jako argument kolekcji, która może być wektorem, mapą sortowaną lub sortowanym zbiorem. Sekwencję cechuje odwrócona kolejność elementów względem struktury bazowej. Funkcja zwraca leniwą sekwencję.

Rezultat zwłoczny: TAK.

Użycie:

  • (rseq kolekcja-uporządkowana).
Przykłady użycia funkcji rseq
1
2
3
(rseq [1 2 3 4])                            ;= > (4 3 2 1)
(rseq (sorted-map :a 1, :b 2, :c 3, :d 4))  ; => ([:d 4] [:c 3] [:b 2] [:a 1])
(rseq (sorted-set 1 2 3 4))                 ; => (4 3 2 1)

Z zakresu kolekcji, subseq

Funkcja subseq pozwala na bazie kolekcji utworzyć sekwencję zawierającą pewien zakres elementów struktury bazowej, określony operatorami przekazanymi jako argumenty. Pierwszy podany argument musi być kolekcją sortowaną (np. sortowaną mapą lub zbiorem), a drugi funkcją porównującą (zwaną komparatorem).

Jeżeli jako argumenty podano tylko jeden komparator i jedną wartość, będą one użyte do wybrania elementów spełniających podany warunek. Jeżeli podano dwa komparatory i dwie wartości, to dla wybieranych elementów oba warunki będą musiały być spełnione równocześnie. Można używać tego sposobu, aby wyrażać pełne, obustronnie określone zakresy elementów.

Podczas dokonywania porównań badane są klucze, jeśli podana kolekcja jest kolekcją asocjacyjną (np. mapą).

Funkcja subseq zwraca leniwą sekwencję.

Rezultat zwłoczny: TAK.

Użycie:

  • (subseq  kolekcja-sortowana komparator wartość),
  • (subseq  kolekcja-sortowana komp-start wartość-start komp-stop wartość-stop).
Przykłady użycia funkcji subseq
1
2
3
4
5
(subseq (sorted-set 1 2 3 4) <= 3)                       ; => (1 2 3)
(subseq (sorted-set 1 2 3 4) > 1 <= 3)                   ; => (2 3)

(subseq (sorted-map :a 1, :b 2, :c 3, :d 4) <= :b)       ; => ([:a 1] [:b 2])
(subseq (sorted-map :a 1, :b 2, :c 3, :d 4) > :a <= :c)  ; => ([:b 2] [:c 3])

Odwrócone z zakresu, rsubseq

Funkcja rsubseq, zachowuje się jak połączenie rseqsubseq, tzn. umożliwia tworzenie sekwencji z zakresu elementów kolekcji, a dodatkowo ich kolejność jest odwracana. Przyjmuje dwa obowiązkowe argumenty: pierwszym powinna być mapa sortowana, a drugim funkcja porównująca (zwana komparatorem).

Jeśli jako argumenty podano tylko jeden komparator i jedną wartość, będą one użyte do wybrania elementów spełniających podany warunek. Jeżeli podano dwa komparatory i dwie wartości, to dla wybieranych elementów oba warunki będą musiały być spełnione równocześnie. Można używać tego sposobu, aby wyrażać pełne, obustronnie określone zakresy elementów.

Podczas dokonywania porównań badane są klucze, jeśli podana kolekcja jest kolekcją asocjacyjną (np. mapą).

Funkcja zwraca leniwą sekwencję.

Rezultat zwłoczny: TAK.

Użycie:

  • (rsubseq mapa-sortowana komparator wartość),
  • (rsubseq mapa-sortowana komp-start wartość-start komp-stop wartość-stop).
Przykłady użycia funkcji rsubseq
1
2
(rsubseq (sorted-map :a 1, :b 2, :c 3, :d 4) <= :b)       ; => ([:b 2] [:a 1])
(rsubseq (sorted-map :a 1, :b 2, :c 3, :d 4) > :a <= :c)  ; => ([:c 3] [:b 2])

Z ustalonych wartości

Leniwe sekwencje mogą być tworzone również na bazie stałej wartości lub zakresu wartości. Przydaje się to w przypadku konieczności powielenia lub powtórzenia tego samego elementu, albo sekwencyjnego udostępnienia zakresu elementów.

Powtarzana wartość, repeat

Funkcja repeat tworzy leniwą sekwencję, której elementy są podaną jako argument wartością. Jeśli wywoływana jest z dwoma argumentami, to pierwszy oznacza liczbę elementów sekwencji.

Rezultat zwłoczny: TAK.

Użycie:

  • (repeat wartość),
  • (repeat elementów wartość).
Przykłady użycia funkcji repeat
1
2
(take 3 (repeat :a))  ; => (:a :a :a)
(repeat 3 :a)         ; => (:a :a :a)

Zakres wartości, range

Funkcja range zwraca leniwą sekwencję, której elementy określono zakresem. Wywołana bez argumentów zwraca nieskończoną sekwencję liczb całkowitych, poczynając od 0.

Rezultat zwłoczny: TAK.

Użycie:

  • (range),
  • (range koniec),
  • (range początek koniec krok?).

Wywołana z jednym argumentem, który powinien być typem numerycznym, zwraca zakres liczb całkowitych od 0 do podanego elementu (z wyłączeniem go).

Wywołana z dwoma argumentami zwraca sekwencję liczb całkowitych dla podanego zakresu początkowego (włączając element o podanej wartości) i końcowego (z wyłączeniem elementu o podanej wartości).

W końcu, jeśli użyjemy wersji trójargumentowej, to ostatnia przekazana wartość będzie oznaczała krok, czyli liczbę, która zostanie dodana do bieżącego elementu, aby uzyskać następny.

Wartością zwracaną jest obiekt typu clojure.lang.LongRange lub clojure.lang.Range (jeżeli zastosowano rzutowanie jednego z argumentów do typu numerycznego krótszego, niż Long) o sekwencyjnym interfejsie dostępu.

Przykłady użycia funkcji range
1
2
3
4
(take  3 (range))  ; => (0 1 2)
(range 3)          ; => (0 1 2)
(range 3 5)        ; => (3 4)
(range 10 20 5)    ; => (10 15)

Z form generujących

Funkcje i makra generujące sekwencje pozwalają tworzyć wirtualne zestawy danych, które zależą od wyników obliczeń i/lub są wieloskładnikowe, jeśli chodzi o źródło.

Pamięć podręczna, lazy-seq

Do tworzenia leniwych sekwencji zapamiętujących elementy służy makro lazy-seq. Przyjmuje ono opcjonalny argument, który powinien być strukturą danych wyposażoną w interfejs ISeq (na bazie której można tworzyć sekwencje), a zwraca obiekt typu Sequable. Ten ostatni ma taką właściwość, że użyje dostępu do każdego z elementów przekazanej struktury tylko raz, podczas pierwszej próby wykonania takiej operacji, a wartość zapamięta w podręcznym obiekcie. Kolejne próby dostępu do elementów będą korzystały z tych zachowanych wcześniej wartości.

Rezultat zwłoczny: TAK.

Użycie:

  • (lazy-seq & sekwencer…).
Przykłady użycia funkcji lazy-seq
1
2
3
4
5
(defn co-dwa-od [x]
  (cons x (lazy-seq (co-dwa-od (+ x 2)))))

(take 5 (co-dwa-od 10))
; => (10 12 14 16 18)

W powyższym przykładzie najpierw tworzymy funkcję co-dwa-od, która wywołuje cons, aby do czoła sekwencji dodać przekazaną jako argument wartość. Wartość ta jest zachowywana w pierwszym slocie obiektu typu Cons, którego drugi slot zawiera odniesienie do leniwej sekwencji generowanej z użyciem seq dla rekurencyjnie wywoływanej funkcji co-dwa-od. Funkcja tworzy sekwencję, której kolejny dodawany element jest większy o 2 od poprzedniego.

Łatwo zauważyć, że nie ma tu żadnego warunku zakończenia rekurencji. Nie jest on potrzebny, ponieważ lazy-seq zwraca sekwencję, która nie będzie od razu obliczana – funkcja co-dwa-od nie będzie wywoływana, dopóki nie wystąpi próba dostępu do kolejnych elementów leniwej sekwencji.

Użyta później funkcja take przyjmuje leniwą sekwencję (zaczynającą się od elementu o wartości 10) i dokonuje pobrania pierwszych pięciu elementów. Oznacza to, że nastąpi tylko kilka rekurencyjnych wywołań co-dwa-od.

Warto pamiętać, że użyta wcześniej funkcja cons służy właśnie do uzupełniania sekwencji o nowe elementy, umieszczane na początku. Obiekty Cons są praktycznie leniwymi sekwencjami. Ich pierwszy slot zawiera dodawany element, natomiast drugi referencję do sekwencji, która jest rozszerzana.

Wywołania funkcji, repeatedly

Funkcja repeatedly służy do generowania leniwej sekwencji na podstawie rezultatów zwracanych przez przekazaną, bezargumentową funkcję, która będzie wywoływana tyle razy, ile pojawi się żądań dostępu do elementu. Opcjonalnie można podać dodatkowy argument (jako pierwszy!), który będzie oznaczał liczbę elementów sekwencji.

Ten sposób tworzenia sekwencji jest wykorzystywany tam, gdzie wymagane jest powtarzalne podawanie tej samej wartości jakiejś funkcji lub konieczne jest generowanie efektów ubocznych określoną liczbę razy.

Rezultat zwłoczny: TAK.

Użycie:

  • (repeatedly funkcja),
  • (repeatedly elementów funkcja).
Przykład użycia funkcji repeatedly
1
2
(take 3 (repeatedly #(rand-int 100)))  ; => (92 53 8)
(repeatedly 3 #(rand-int 100))         ; => (65 81 10)

Zauważmy użycie składni wyrażającej funkcję anonimową.

Wywołania funkcji sprzężonej, iterate

Funkcja iterate, jak sugeruje jej nazwa, służy do iterowania, czyli powtarzania dostępu do kolejnych elementów sekwencji.

Rezultat zwłoczny: TAK.

Użycie:

  • (iterate operator początek).

Funkcja przyjmuje dwa argumenty: pierwszy to jednoargumentowa funkcja, a drugi wartość początkowa, która stanie się jednocześnie pierwszym elementem.

Dla podanej wartości wywoływana jest funkcja, a zwracana przez nią wartość staje się kolejnym elementem sekwencji i jednocześnie wartością podawaną jako argument przy następnym wywołaniu funkcji.

Podawana jako argument funkcja nie powinna mieć efektów ubocznych.

Wartością zwracaną jest obiekt typu clojure.lang.Iterate o sekwencyjnym interfejsie dostępu.

Przykłady użycia funkcji iterate
1
2
(take 3 (iterate inc 5))        ; => (5 6 7)
(take 3 (iterate #(*' % %) 5))  ; => (5 25 625)

W linii nr 2 użyliśmy anonimowej funkcji, lecz tym razem skorzystaliśmy dwukrotnie z argumentu (symbolizowanego znakiem procenta) i operatora mnożenia. W ten sposób stworzyliśmy funkcję, która dokonuje podniesienia do kwadratu każdego kolejnego elementu sekwencji.

Z innych obiektów

Leniwe sekwencje mogą być tworzone nie tylko jako reprezentacja elementów kolekcji czy generowane przez funkcje, ale również powstawać na bazie innych obiektów.

Linie z czytnika, line-seq

Funkcja line-seq przyjmuje obiekt czytnika (musi implementować java.io.BufferedReader), dla którego każda z kolejnych odczytywanych linii staje się kolejnym elementem zwracanej sekwencji. Czytnik to mechanizm Javy pozwalający w ujednolicony sposób uzyskiwać dostęp do plików, gniazd sieciowych, dokumentów HTTP i innych danych, które da się odczytywać strumieniowo.

Rezultat zwłoczny: TAK.

Użycie:

  • (line-seq czytnik).
Przykład użycia funkcji line-seq
1
2
(with-open [czytnik (clojure.java.io/reader "http://google.pl/")]
  (printf "%s\n" (clojure.string/join "\n" (line-seq czytnik))))

Mapy strukt. z rezultatów, resultset-seq

Mapy strukturalizowane to mapy, które mają z góry określony zestaw kluczy. Zaznajomieni z językiem C mogą sobie je wyobrazić jako tablicę, której każdy element jest strukturą (struct), wyposażoną dodatkowo w funkcję transformacji kluczowej, aby dostęp do elementów był szybki.

W Javie istnieje interfejs java.sql.ResultSet, którego używa się do zwracania wyników zapytania do bazy typu SQL. Funkcja resultset-seq pozwala na dostęp do rezultatu zapytania. Zwraca ona leniwą sekwencję, której każdy element to przekształcony do postaci mapy strukturalizowanej rząd (ang. row) rezultatu.

Rezultat zwłoczny: TAK.

Użycie:

  • (resultset-seq rezultaty).
Przykład użycia funkcji resultset-seq
1
2
3
4
5
6
7
(with-connection
  db (with-query-results
       rs ["select enum_range(null::costam)"]
       (get (first (resultset-seq 
                    (.getResultSet (get (first(doall rs))
                                        :enum_range))))
            "VALUE")))

Wyrażenia regularne, re-seq

Dzięki funkcji re-seq możliwe jest tworzenie leniwej sekwencji, której elementy są kolejnymi dopasowaniami wzorca do łańcucha tekstowego przeprowadzonymi z użyciem metody java.util.regex.Matcher.find() i przetworzonymi z użyciem re-groups. Funkcja przyjmuje dwa argumenty. Pierwszy to wyrażenie regularne, a drugi dopasowywany tekst.

Rezultat zwłoczny: TAK.

Użycie:

  • (re-seq regex).
Przykłady użycia funkcji re-seq
1
2
3
4
5
(re-seq #"\w+" "Jestem bosym mleczarzem.")
; => ("Jestem" "bosym" "mleczarzem")

(map last (re-seq #"((.*?)[ \.]+)" "Jestem bosym mleczarzem."))
; => ("Jestem" "bosym" "mleczarzem") 

Drzewa, tree-seq

Funkcja tree-seq zwraca leniwą sekwencję, której kolejne elementy są węzłami drzewa podanego jako ostatni argument. Pierwszym argumentem powinien być predykat, który zwraca wartość true, jeżeli podany mu jako argument węzeł może mieć węzły potomne (gałęzie). Z kolei drugim argumentem jednoargumentowa funkcja, która generuje sekwencję węzłów potomnych względem podanego.

Rezultat zwłoczny: TAK.

Użycie:

  • (tree-seq predykat gen-potomnych korzeń).
Przykład użycia funkcji tree-seq
1
2
3
4
5
6
7
8
9
(def drzewko { :korzeń
              {
               :gałązka1 nil,
               :gałązka2 { :listek1 nil },
               :gałązka3 { :listek1 nil, :listek2 nil }}})
(tree-seq
 #(or (map? %) (val %))
 #(if (map? %) (val (first %)) (val %))
 drzewko)

Pliki z katalogu, file-seq

Dzięki funkcji file-seq można rekurencyjnie sekwencjonować zawartości katalogów. Przyjmuje ona jeden argument, który powinien być plikiem lub katalogiem (java.io.Files), a zwraca leniwą sekwencję z zawartością katalogu, w której każdy element jest typu java.java.io.Filesio.File.

Rezultat zwłoczny: TAK.

Użycie:

  • (file-seq pliki).
Przykład użycia funkcji file-seq
1
(file-seq (clojure.java.io/file "/tmp"))

Dokumenty XML, xml-seq

Funkcja xml-seq pozwala na tworzenie leniwych sekwencji reprezentujących strukturę dokumentów XML. Przyjmuje ona jeden argument, który powinien być korzeniem drzewa DOM.

Rezultat zwłoczny: TAK.

Użycie:

  • (xml-seq korzeń).
Przykład użycia funkcji xml-seq
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
;; Definiujemy jakiś mini-dokument XML.
(def nasz-xml
     "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
      <korzeń klucz=\"wartość\">
        <pień>
          <gałązka>listek1</gałązka>
          listek2
       </pień>
      </korzeń>")

;; Funkcja clojure.xml/parse wymaga, aby argumentem był obiekt
;; reprezentujący strumień danych. Musimy więc łańcuch tekstowy
;; udostęppnić jako strumień (javovy obiekt ByteArrayInputStream).
(defn jako-strumień [tekst]
  (java.io.ByteArrayInputStream.
   (.getBytes tekst)))

(xml-seq
 (clojure.xml/parse (jako-strumień nasz-xml)))

; => ({:tag :korzeń,
; =>        :attrs   {:klucz "wartość"},
; =>        :content [{:tag :pień,
; =>                   :attrs nil,
; =>                   :content [{:tag :gałązka,
; =>                              :attrs nil,
; =>                              :content ["listek1"]}
; =>                             "\n      listek2\n    "]}]}
; =>       {:tag :pień,
; =>        :attrs nil,
; =>        :content [{:tag :gałązka,
; =>                   :attrs nil,
; =>                   :content ["listek1"]}
; =>                  "\n      listek2\n    "]}
; =>       {:tag :gałązka,
; =>        :attrs nil,
; =>        :content ["listek1"]}
; =>       "listek1"
; =>       "\n      listek2\n    ")

Iteratory Javy, iterator-seq

Dzięki funkcji iterator-seq możemy tworzyć sekwencje powiązane z iteratorami Javy (obiektami implementującymi interfejs java.util.Iterator).

Rezultat zwłoczny: (zależy od wejścia).

Użycie:

  • (iterator-seq iterator).
Przykład użycia funkcji iterator-seq
1
2
(iterator-seq (.iterator [1 2 3]))
; => 1 2 3

Powyższy przykład jest nieco abstrakcyjny, ponieważ uzyskujemy w nim dostęp do obiektu iteratora kolekcji, która jest wektorem. Na bazie wektorów, a także większości innych wbudowanych kolekcji języka Clojure, można tworzyć sekwencje bezpośrednio, bez konieczności odwoływania się do metod Javy.

Enumeratory Javy, enumeration-seq

Java wyposażona jest w interfejs enumeracji (java.util.Enumeration), który jest starszym wariantem interfejsu Iterator. Nazwa może być nieco myląca, ponieważ nie chodzi tu o typowe enumeratory, które w inżynierii oprogramowania oznaczają po prostu warstwę abstrakcji służącą do dostarczania danych metodzie odpowiedzialnej za reaktywną obsługę strumienia danych. W Javie interfejs Enumeration to uboższa wersja interfejsu Iterator, która różni się tym, że ma dłuższe nazwy metod i nie obsługuje operacji usuwania.

Dzięki funkcji enumerator-seq możemy tworzyć sekwencje powiązane z obiektami implementującymi interfejs Enumeration.

Rezultat zwłoczny: (zależy od wejścia).

Użycie:

  • (enumeration-seq enumerator).
Przykład użycia funkcji enumeration-seq
1
2
(enumeration-seq (java.util.StringTokenizer. "raz dwa trzy"))
; => ("raz" "dwa" "trzy")

Dostęp do elementów

Istnieje wiele funkcji, które pozwalają na dostęp do elementów wyrażanych sekwencyjnie. Dostęp ten może być zarówno następczy (operowanie na kolejnych elementach jeden po drugim), jak i swobodny (operowanie na elementach o wskazanej pozycji). Są też sposoby na używanie elementów sekwencji jako argumentów podawanych do funkcji.

Wartościowanie leniwych sekwencji

Leniwe sekwencje cechuje to, że wartości konkretnych elementów nie są obliczane od razu po ich stworzeniu, lecz w momencie, gdy zażąda się do nich dostępu (skorzysta się z nich). Oczywiście poza wartością uzyskiwanego elementu będą też obliczone wartości wszystkich go poprzedzających, ponieważ leniwe sekwencje są w istocie wartością początkową oraz funkcją, która wytwarza kolejne na jej bazie.

Czasem może zdarzyć się tak, że wystąpi potrzeba wymuszenia obliczenia (ang. force evaluation) wartości wszystkich elementów sekwencji w celu operowania na jej elementach lub powstania efektów ubocznych (np. wyświetlenia wartości na ekranie). Można wtedy skorzystać odpowiednich makr lub funkcji:

  • dla więcej niż jednej sekwencji:
    • z makra doseq – gdy zależy nam na wartościach elementów podanych sekwencji, ale zamierzamy ich użyć w celu powstania efektów ubocznych w podanym wyrażeniu (wywoływanym dla każdego elementu);
  • dla dokładnie jednej sekwencji:
    • z funkcji dorun – gdy nie zależy nam na wartościach elementów podanej sekwencji, ale dla każdego z nich chcemy wywołać funkcję generującą w celu powstania jej efektów ubocznych;
    • z funkcji doall – gdy zależy nam na wartościach elementów podanej sekwencji i chcemy z nich skorzystać w podanym wyrażeniu (wywoływanym dla każdego elementu).

Makro doseq i funkcja dorun nie przytrzymują czoła sekwencji, a więc nie dochodzi do zajęcia pamięci. Funkcja doall dokonuje retencji czołowej, a zbuforowane rezultaty są zwracane.

Warto na wstępie przestrzec, że wymienionych konstrukcji nie należy stosować na sekwencjach o nieskończonej długości, ponieważ (jak łatwo się domyślić) spowoduje to zawieszenie pracy bieżącego wątku i konsumpcję dostępnej pamięci, aż do awaryjnego zakończenia działania. Jeśli chodzi o dorundoall, to można w takich przypadkach zawęzić żądany zbiór wyników do niezbędnego minimum, podając dodatkowy, opcjonalny argument.

Wymuszone wartościowanie, doseq

Makro doseq służy do uruchomienia funkcji generującej dla każdego elementu leniwej sekwencji w celu poznania kolejnych wartości i wywołania efektów ubocznych.

Jako pierwszy argument doseq przyjmuje wektor zawierający pary powiązań symboli z sekwencjami, których kolejne wartości będą wskazywane przez te symbole. Będzie można ich używać w wyrażeniach podanych jako opcjonalne argumenty makra. Wyrażenia będą przeliczane tyle razy, ile jest elementów.

Jeżeli w wektorze powiązaniowym umieścimy więcej niż jedną parę powiązaniową, to dla każdego elementu pierwszej sekwencji będzie wykonane przejście przez wszystkie elementy kolejnej itd. Wtedy wyrażenia wykonane będą tylokrotnie, ile wynosi iloraz liczb elementów wszystkich podanych sekwencji.

Zwracaną wartością jest nil.

Użycie doseq nie powoduje zachowywania rezultatów w pamięciach podręcznych.

Rezultat zwłoczny: NIE.

Użycie:

  • (doseq wektor-powiązaniowy & wyrażenie…).
Przykład użycia funkcji doseq
1
2
3
4
5
6
(doseq [s (lazy-seq [1 2 3 4])] (println s))
; => nil
; >> 1
; >> 2
; >> 3
; >> 4
Przykład użycia funkcji doseq z wieloma wyrażeniami
1
2
3
4
5
6
7
8
(doseq [s (lazy-seq [:a :b]) d (list 1 2 3)] (println s d))
; => nil
; >> :a 1
; >> :a 2
; >> :a 3
; >> :b 1
; >> :b 2
; >> :b 3

W powyższym przykładzie widać co się dzieje, gdy podamy więcej niż jedno wyrażenie sekwencyjne. Poza tym w przypadku drugiej pary korzystamy z sekwencyjnego interfejsu listy, a nie tworzymy sekwencji z użyciem lazy-seq.

Generowanie efektów ubocznych, dorun

Funkcja dorun wymusza wartościowanie elementów sekwencji podanej jako jej ostatni argument, lecz bez odczytywania ich wartości. Służy do wywoływania funkcji generującej, która może mieć efekty uboczne.

Pierwszym, opcjonalnym argumentem dorun, może być liczba elementów sekwencji, które zostaną obsłużone.

W przypadku sekwencji, których funkcja generująca kolejne elementy ma efekty uboczne, dorun sprawia, że są one emitowane dla każdego z nich. W normalnych warunkach efekty uboczne funkcji generującej są emitowane dopiero w momentach pobierania kolejnych wartości.

Funkcja dokonuje przejścia po elementach i zawsze zwraca wartość nil.

Użycie dorun nie powoduje zachowywania rezultatów w pamięciach podręcznych.

Rezultat zwłoczny: NIE.

Użycie:

  • (dorun liczba-elementów? sekwencja).
Przykład użycia funkcji dorun
1
2
3
4
5
6
7
8
9
;; map zwraca leniwą sekwencję, której funkcja po prostu wypisuje
;; kolejne elementy powiązanej kolekcji
(def powitania (map #(println "witaj" %) ["matko" "ojcze" "bracie"]))

(dorun powitania)
; => nil
; >> witaj matko
; >> witaj ojcze
; >> witaj bracie

Efekty uboczne i wartości, doall

Funkcja doall podobnie jak dorun wymusza wartościowanie elementów sekwencji podanej jako jej ostatni argument, lecz pozwala operować na ich wartościach. Pierwszym, opcjonalnym argumentem, może być liczba elementów, które chcemy pobrać.

W przypadku sekwencji, których funkcja generująca kolejne elementy ma efekty uboczne, doall sprawia, że są one emitowane dla każdego z nich. W normalnych warunkach efekty uboczne funkcji generującej są emitowane dopiero w momentach pobierania kolejnych wartości.

Funkcja dokonuje przejścia po elementach i po ich przeliczeniu zwraca wartości w postaci leniwej sekwencji.

Czołowy element sekwencji jest zachowywany w pamięci, podobnie jak wszystkie zbuforowane rezultaty wynikające z jej rozwinięcia.

Rezultat zwłoczny: TAK.

Użycie:

  • (doall liczba-elementów? sekwencja).
Przykład użycia funkcji doall
1
2
3
4
5
6
7
8
9
10
;; map zwraca leniwą sekwencję,
;; dla której funkcja wypisuje kolejne elementy
(def powitania (map #(do (println "witaj" %) %) ["matko" "ojcze" "bracie"]))

(doall powitania)
; => ("matko" "ojcze" "bracie")

witaj matko
witaj ojcze
witaj bracie

Dostęp następczy

Pobieranie kolejnych elementów, zwane też iterowaniem po strukturze, można w kolekcjach implementujących sekwencyjny interfejs i sekwencjach zrealizować, korzystając z odpowiednich funkcji.

Pierwszy element, first

Za pobieranie pierwszego elementu odpowiada funkcja first. Jako argument przyjmuje sekwencję, a zwraca wartość pierwszego elementu lub nil, jeśli podany argument ma także wartość nil lub pierwszy element nie istnieje.

Rezultat zwłoczny: NIE.

Użycie:

  • (first sekwencja).
Przykłady użycia funkcji first
1
2
3
(first (seq ["ab" 2 3]))  ; => "ab"
(first      ["ab" 2 3])   ; => "ab"
(first       "abcdef")    ; => \a

Pierwszy pierwszego, ffirst

Funkcja ffirst jest skróconym odpowiednikiem dwukrotnego wywołania first. Zwraca pierwszy element sekwencji lub kolekcji powstałej w wyniku uprzedniego pobrania pierwszego elementu sekwencji. Jako argument przyjmuje sekwencję, a zwraca wartość pierwszego elementu sekwencji stanowiącej pierwszy element lub nil, jeśli podany argument ma także wartość nil lub element nie istnieje.

Rezultat zwłoczny: NIE.

Użycie:

  • (ffirst sekwencja).
Przykład użycia funkcji ffirst
1
2
(ffirst ["ab" 2 3])
; => \a

Uwaga: Wywołanie ffirst na sekwencji, której pierwszy element nie jest sekwencją spowoduje zgłoszenie wyjątku.

Następne elementy, next

Każdy element sekwencji jest powiązany z kolejnym. Dzięki funkcji next możemy podążać za tą relacją i uzyskiwać wartości wszystkich elementów umieszczonych za pierwszym w podanej jako argument sekwencji. Zwracaną wartością jest sekwencja. Jeśli poza pierwszym elementem nie istnieją już inne, to zwrócona będzie wartość nil.

Rezultat zwłoczny: NIE.

Użycie:

  • (next sekwencja).
Przykład użycia funkcji next
1
2
(next '(1 2 3 4))
; => (2 3 4)

Następne po następnym, nnext

Funkcja nnext jest idiomem zagnieżdżonego, dwukrotnego wywołania next. Zwracaną wartością jest sekwencja zawierająca elementy sekwencji podanej jako pierwszy argument poza dwoma pierwszymi. Jeśli nie ma wystarczającej liczby elementów, zwrócona będzie wartość nil.

Rezultat zwłoczny: NIE.

Użycie:

  • (nnext sekwencja).
Przykład użycia funkcji nnext
1
2
3
;; odpowiednik (next (next sekwencja))
(nnext '(1 2 3 4))
; => (3 4)

Pierwszy z następnych, fnext

Funkcja fnext działa podobnie do next, lecz wywołuje first na rezultacie jej wywołania. Przyjmuje sekwencję, a zwraca pojedynczy element, który jest elementem następującym po pierwszym.

Rezultat zwłoczny: NIE

Użycie:

  • (fnext sekwencja).
Przykład użycia funkcji fnext
1
2
3
;; odpowiednik (first (next sekwencja))
(fnext '(1 2 3 4))
; => 2

Kolejne od podanego, nthnext

Funkcja nthnext służy do uzyskiwania sekwencji elementów, poczynając od pozycji o podanym numerze (licząc od zera). Przyjmuje dwa argumenty: sekwencję i numer kolejny elementu, od którego należy zacząć pobieranie wszystkich następnych. Zwracaną wartością jest sekwencja.

W przypadku podania numeru większego niż liczba elementów, zwracana jest wartość nil.

Rezultat zwłoczny: NIE.

Użycie:

  • (nthnext sekwencja numer-kolejny).
Przykład użycia funkcji nthnext,
1
2
(nthnext '(1 2 3 4) 2)
; => (3 4)

Następne pierwszego, nfirst

Funkcja nfirst jest skróconym odpowiednikiem wywołania next na rezultacie zwracanym przez first. Zwraca sekwencję zawierającą kolejne (poza pierwszym) elementy kolekcji lub sekwencji stanowiącej pierwszy element sekwencji podanej jako argument. Jeśli podany argument ma także wartość nil lub element nie istnieje, zwrócona zostanie wartość nil.

Rezultat zwłoczny: NIE.

Użycie:

  • (nfirst sekwencja).
Przykład użycia funkcji nfirst
1
2
(nfirst ["abcd" 2 3])
; => (\b \c \d)

Uwaga: Wywołanie nfirst na sekwencji, której pierwszy element nie jest sekwencją spowoduje zgłoszenie wyjątku.

Drugi element, second

Uzyskanie dostępu do drugiego elementu sekwencji umożliwia funkcja second.

Rezultat zwłoczny: NIE.

Użycie:

  • (second sekwencja).
Przykład użycia funkcji second
1
2
(second ["ab" 2 3])
; => 2

Element o wskazanej pozycji, nth

Dzięki funkcji nth można pobrać element sekwencji o wskazanej pozycji. Pozycja liczona jest od 0 (pierwszy element).

Warto zauważyć, że aby uzyskać dostęp do konkretnego elementu sekwencji, konieczne jest uzyskanie dostępu, a często też obliczenie wartości wszystkich elementów poprzedzających. Wynika to ze specyficznego rodzaju uzyskiwania wartości elementów sekwencji, gdzie na poziomie operacyjnym kolejne zależą od poprzednich.

Rezultat zwłoczny: NIE.

Użycie:

  • (nth sekwencja pozycja),
  • (nth sekwencja pozycja wartość-domyślna).
Przykłady użycia funkcji nth
1
2
(nth ["ab" 2 3] 2)         ; => 3
(nth ["ab" 2 3] 5 "brak")  ; => "brak"

Ostatni element, last

Ostatni element sekwencji możemy pobrać z użyciem funkcji last. Przyjmuje ona jeden argument, którym powinna być sekwencja, a zwraca wartość jej ostatniego elementu.

Rezultat zwłoczny: NIE.

Użycie:

  • (last sekwencja).
Przykład użycia funkcji last
1
2
(last ["ab" 2 3])
; => 3

Losowy element, rand-nth

Dzięki funkcji rand-nth można pobierać losowy element z kolekcji wyposażonej w sekwencyjny interfejs dostępu. Przyjmuje ona jeden argument, którym powinna być sekwencja, a zwraca wartość jej losowo wybranego elementu.

Rezultat zwłoczny: NIE.

Użycie:

  • (rand-nth sekwencja).
Przykład użycia funkcji rand-nth
1
2
(rand-nth ["ab" 2 3])
; => 2

Pierwszy jako powiązanie, when-first

Makro when-first przyjmuje jeden obowiązkowy argument, który powinien być wektorem powiązaniowym zawierającym dokładnie jedną parę powiązaniową (dwa elementy). Pierwszym elementem pary musi być symbol (zostanie on użyty do identyfikacji powiązania), natomiast drugim sekwencja lub kolekcja z sekwencyjnym interfejsem dostępu. Pierwszy element tej ostatniej zostanie powiązany leksykalnie (z użyciem let) z podanym symbolem i będzie można go użyć w wyrażeniu podanym jako drugi argument.

Jeśli pierwszego elementu nie da się uzyskać (np. mamy do czynienia z pustą sekwencją lub z wartością nil), to zwrócona zostanie wartość nil. W przeciwnym przypadku zwrócona będzie wartość wyrażenia podanego jako drugi argument.

Rezultat zwłoczny: (zależy od wejścia).

Użycie:

  • (when-first wektor-powiązaniowy & ciało…).
Przykład użycia makra when-first
1
2
(when-first [x '(1 2 3)] x)
; => 1

Zobacz także:

Konwertowanie sekwencji

W Clojure istnieją funkcje, dzięki którym możliwe jest budowanie kolekcji czy zestawów argumentów na bazie sekwencji.

Do argumentów funkcji, apply

W Clojure znajdziemy funkcję apply, która pozwoli nam używać elementów sekwencji jako kolejnych argumentów wywoływanej funkcji. Przyjmuje ona minimum dwa argumenty – pierwszy powinien być funkcją, która zostanie wywołana, a drugi sekwencją, której elementy będą przekazane jako argumenty tej funkcji. Opcjonalnie możemy po funkcji przekazać dodatkowe argumenty, które zostaną użyte w wywołaniu jako pierwsze (przed argumentami pochodzącymi z sekwencji). Funkcja zwraca rezultat wykonania przekazanej funkcji.

Rezultat zwłoczny: NIE.

Użycie:

  • (apply funkcja sekwencja),
  • (apply funkcja argument-funkcji… sekwencja).
Przykłady użycia funkcji apply
1
2
(apply str "a" "b" "c" [1 2 3])  ; => "abc123"
(apply str             [1 2 3])  ; => "123"

W powyższym przykładzie skorzystaliśmy z funkcji str, która przyjmuje dowolną liczbę argumentów i zwraca łańcuch tekstowy na podstawie złączenia tekstowych reprezentacji ich wartości.

Zobacz także:

Do kolekcji

Umieszczanie w kolekcji

Funkcja into pozwala umieścić wszystkie elementy podanej jako drugi argument sekwencji w kolekcji, która została przekazana jako pierwszy argument. Zwraca pochodną kolekcję z dodanymi elementami.

Rezultat zwłoczny: NIE.

Użycie:

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

Budowanie wektora, vec

Funkcja vec, która służy do tworzenia wektorów, może również korzystać z sekwencji, aby na tej podstawie wypełnić strukturę elementami.

Rezultat zwłoczny: NIE.

Użycie:

  • (vec sekwencja).
Przykład użycia funkcji vec
1
2
(vec '(1 2 3 4))
; => [1 2 3 4]

Budowanie zbioru, set

Analogicznie do vec działa funkcja set, która tworzy zbiory. Można użyć sekwencji do zainicjowania nowej struktury.

Rezultat zwłoczny: NIE.

Użycie:

  • (set sekwencja).
Przykład użycia funkcji set
1
2
(set '(1 2 3 4))
; => #{1 4 3 2}

Budowanie tablicy, into-array

Funkcja into-array pozwala tworzyć tablice Javy na bazie sekwencji.

Rezultat zwłoczny: NIE.

Użycie:

  • (into-array sekwencja),
  • (into-array typ sekwencja).

W wariancie jednoargumentowym przyjmuje sekwencję, a w wariancie dwuargumentowym typ danych oraz sekwencję. Ów typ, jeśli go podano, wskazuje jakiej klasy elementy są dostępne za pośrednictwem sekwencyjnego interfejsu przekazywanego obiektu.

Przykład użycia funkcji into-array
1
2
(into-array '(1 2 3 4))               ; => #<Long[] [Ljava.lang.Long;@210dfbcd>
(into-array Integer/TYPE '(1 2 3 4))  ; => #<int[] [[email protected]>

Budowanie tablicy dwuwymiarowej, to-array-2d

Funkcja to-array-2d pozwala tworzyć dwuwymiarowe tablice Javy na bazie sekwencji. Elementy sekwencji muszą być egzemplarzami klasy Object lub pochodnymi, a każdy może również zawierać sekwencję lub kolekcję.

Rezultat zwłoczny: NIE.

Użycie:

  • (to-array-2d sekwencja).
Przykład użycia funkcji to-array-2d
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
;; tworzymy tablicę i wiążemy z obiektem Var
(def tablice (to-array-2d '("sok" "marchwiowy")))

;; sprawdzamy co jest wewnątrz
tablice
; => #<Object[][] [[Ljava.lang.Object;@5279effd>

;; pobieramy pierwszy element
(first tablice)
; => #<Object[] [Ljava.lang.Object;@1076b5a6>

;; pobieramy pierwszy element pierwszego elementu
(ffirst tablice)
; => \s

;; przekształcamy pierwszy element do łańcucha znakowego
(apply str (first tablice))
; => "sok"

;; przekształcamy do łańcucha znakowego
(clojure.string/join " " (map #(apply str %) tablice))
; => "sok marchwiowy"

;; przekształcamy do wektora wektorów
(vec (map vec tablice))
; => [[\s \o \k] [\m \a \r \c \h \w \i \o \w \y]]

Budowanie tablicy częstości, frequencies

Dzięki funkcji frequencies możemy dowiedzieć się jaka jest częstotliwość występowania poszczególnych elementów sekwencji. Funkcja ta przyjmuje jeden argument, a zwraca mapę zawierającą znalezione, unikatowe elementy jako klucze i przypisane do nich liczby całkowite, które wyrażają jak często występowały w sekwencji.

Rezultat zwłoczny: NIE.

Użycie:

  • (frequencies sekwencja).
Przykład użycia funkcji frequencies
1
2
(frequencies '(Zuzia Ruzia Jadzia Zuzia))
; => {Zuzia 2, Ruzia 1, Jadzia 1}

Budowanie mapy

Funkcja zipmap pozwala utworzyć mapę na podstawie dwóch sekwencji podanych jako argumenty. Wartości elementów pierwszej staną się kluczami mapy, a drugiej wartościami.

Użycie:

  • (zipmap sekwencja-kluczy sekwencja-wartości).
Przykład użycia funkcji zipmap
1
2
(zipmap '(:a :b) '(1 2))
; => {:b 2, :a 1}

Grupowanie, group-by

Funkcja group-by umożliwia tworzenie map, których wartościami są wektory elementów powstałych na bazie sekwencji podanej jako drugi argument, natomiast kluczami rezultaty wykonania funkcji, którą podano jako pierwszy argument. Pozwala ona grupować elementy sekwencji z użyciem podanego operatora.

Rezultat zwłoczny: NIE.

Użycie:

  • (group-by funkcja sekwencja-wartości).
Przykład użycia funkcji group-by
1
2
3
4
(group-by #(if (> % 9) :wielocyfrowe :jednocyfrowe)
          '(1 2 30 40 50))

; => {:jednocyfrowe [1 2], :wielocyfrowe [30 40 50]}

Element z kryterium, some

Funkcja some zwraca wartość funkcji podanej jako pierwszy argument dla pierwszego elementu podanej jako drugi argument sekwencji, dla którego przekazana funkcja zwraca wartość różną od nil i różną od false.

Jeśli dla żadnego elementu sekwencji przekazana funkcja nie zwróci wartości prawdziwej, zwracana jest wartość nil.

Rezultat zwłoczny: NIE.

Użycie:

  • (some warunek sekwencja).
Przykłady użycia funkcji some
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
;; pierwszy test parzystości elementu o pozytywnym wyniku
(some even? '(1 2 3 4 5 6 7 8))
; => true

;; pierwszy parzysty element
(some #(if (even? %) %) '(1 2 3 4 5 6 7 8))
; => 2

;; pierwszy element, który jest większy niż 10
(some #(if (> % 10) %) '(1 2 3 4 5 6 7 8))
; => nil

;; pierwszy element, który jest mniejszy niż 5
(some #(if (< % 5) %) '(1 2 3 4 5 6 7 8))
; => 1

;; pierwszy element, który jest większy niż 3
(some #(if (> % 3) %) '(1 2 3 4 5 6 7 8))
; => 4

;; pierwszy element należący do zbioru
(some #{1 2 7} '(3 4 5 6 7))
; => 7

Redukowanie do wartości, reduce

Funkcja reduce jako drugi argument przyjmuje sekwencję, a jako pierwszy dwuargumentowy operator, który powinien być użyty względem kolejnych elementów tej sekwencji w taki sposób, że wynik zastosowania operatora na poprzednim elemencie jest wcześniej akumulowany, a następnie używany jako pierwszy argument wywołania operatora, gdy drugim argumentem operatora jest aktualnie przetwarzany element sekwencji. Funkcja zwraca wartość ostatniego wywołania operatora.

W swojej trójargumentowej wersji funkcja pozwala podać początkową wartość akumulatora jako drugi argument, a sekwencję jako trzeci.

Rezultat zwłoczny: NIE.

Użycie:

  • (reduce operator sekwencja),
  • (reduce operator wartość sekwencja).
Przykład użycia funkcji reduce
1
2
(reduce +    '(1 2 3 4))  ; => 10
(reduce + 20 '(1 2 3 4))  ; => 30

Modyfikowanie sekwencji

Istnieje wiele funkcji, które pozwalają przekształcać sekwencje w taki sposób, że będą one zawierały więcej lub mniej elementów. Zazwyczaj kryterium selekcji będzie bazowało na wartościach, chociaż zdarzają się operacje pozwalające korzystać np. z numerów kolejnych.

Usuwanie elementów

Bez pierwszego elementu, rest

Pierwszy element sekwencji możemy usunąć z użyciem funkcji rest, która jest podstawową funkcją sekwencyjnego interfejsu (jest operacją wymaganą, aby nazywać sposób dostępu sekwencyjnym). Przyjmuje ona sekwencję i zwraca sekwencyjną strukturę (np. listę, obiekt typu ChunkedCons czy LazySeq), która zawiera bądź wyraża wszystkie elementy sekwencji poza pierwszym.

Rezultat zwłoczny: (zależy od wejścia).

Użycie:

  • (rest sekwencja).
Przykłady użycia funkcji rest
1
2
3
4
5
6
7
8
9
10
11
12
13
14
(rest [1 2 3])                   ; => (2 3)
(type (rest [1 2 3]))            ; => clojure.lang.PersistentVector$ChunkedSeq

(rest '(1 2 3))                  ; => (2 3)
(type (rest '(1 2 3)))           ; => clojure.lang.PersistentList

(rest (seq '(1 2 3)))            ; => (2 3)
(type (rest (seq '(1 2 3))))     ; => clojure.lang.PersistentList

(rest (range 1 5))               ; => (2 3 4)
(type (rest (range 1 5)))        ; => clojure.lang.ChunkedCons

(take 5 (rest (iterate inc 1)))  ; => (2 3 4 5 6)
(type (rest (iterate inc 1)))    ; => clojure.lang.LazySeq

Usuwanie początkowych elementów, drop

Funkcja drop przyjmuje dwa argumenty. Pierwszym powinna być liczba całkowita, a drugim sekwencja. Zwrócona zostanie sekwencja na bazie przekazanej bez pierwszych elementów o podanej liczbie.

Rezultat zwłoczny: TAK.

Użycie:

  • (drop liczba sekwencja).
Przykład użycia funkcji drop
1
2
(drop 3 '(1 2 3 4 5 6 7))
; => (4 5 6 7)

Warunkowe usuwanie początkowych, drop-while

Funkcja drop-while jako pierwszy argument przyjmuje predykat, a jako drugi sekwencję. Przekazana funkcja jest wywoływana dla każdego elementu i dopóki zwraca wartość różną od false i różną od nil, to elementy są pomijane. Gdy choć raz funkcja zwróci logiczną prawdę, to przestaje być wywoływana, a element aktualnie przetwarzany i wszystkie kolejne są umieszczane w sekwencji wynikowej.

Rezultat zwłoczny: TAK.

Użycie:

  • (drop-while predykat sekwencja).
Przykład użycia funkcji drop-while
1
2
(drop-while #(not= 5 %) '(1 2 3 4 5 6 7))
; => (5 6 7)

Pomijanie końcowych elementów, drop-last

Funkcja drop-last przyjmuje dwa argumenty. Pierwszym powinna być liczba całkowita, a drugim sekwencja. Zwrócona zostanie leniwa sekwencja na bazie przekazanej bez końcowych elementów o podanej liczbie. W wariancie jednoargumentowym pomijany jest tylko ostatni element.

Rezultat zwłoczny: TAK.

Użycie:

  • (drop-last        sekwencja),
  • (drop-last liczba sekwencja).
Przykłady użycia funkcji drop-last
1
2
(drop-last 3 '(1 2 3 4 5 6 7))  ; => (1 2 3 4)
(drop-last   '(1 2 3 4 5 6 7))  ; => (1 2 3 4 5 6)

Usuwanie końcowych elementów, butlast

Funkcja butlast działa podobnie do drop-last, lecz przyjmuje tylko jeden argument, którym powinna być sekwencja. Zwrócona zostanie sekwencja na bazie przekazanej bez ostatniego elementu.

Funkcja ta jest ok. 20% szybsza od drop-last, ale nie zwraca sekwencji leniwej.

Rezultat zwłoczny: NIE.

Użycie:

  • (butlast sekwencja).
Przykład użycia funkcji butlast
1
2
(butlast '(1 2 3 4 5 6 7))
; => (1 2 3 4 5 6)

Pozostawianie pierwszych elementów, take

Funkcja take pozwala usunąć wszystkie elementy poza ich podaną liczbą, licząc od czoła sekwencji. Przyjmuje ona dwa argumenty: liczbę całkowitą (określającą ile elementów pozostawić) i sekwencję. Zwraca sekwencję pochodną.

Funkcja take, podobnie jak drop, ma również swój odpowiednik warunkowy. Dopóki podana funkcja zwraca logiczną prawdę (nie false i nie nil), elementy będą wybierane do wynikowej sekwencji.

Rezultat zwłoczny: TAK.

Użycie:

  • (take       liczba   sekwencja),
  • (take-while predykat sekwencja).
Przykłady użycia funkcji take i take-while
1
2
3
4
5
(take 3 '(1 2 3 4 5 6 7))
; => (1 2 3)

(take-while #(not= 5 %) '(1 2 3 4 5 6 7))
; => (1 2 3 4)

Pozostawianie ostatnich elementów, take-last

Funkcja take-last przyjmuje dwa argumenty. Pierwszym powinna być liczba całkowita, a drugim sekwencja. Zwrócona zostanie sekwencja na bazie przekazanej z usuniętymi wszystkimi elementami poza podaną liczbą końcowych elementów, które należy zachować.

Rezultat zwłoczny: TAK.

Użycie:

  • (take-last liczba sekwencja).
Przykład użycia funkcji take-last
1
2
(take-last 3 '(1 2 3 4 5 6 7))
; => (5 6 7)

Pozostawianie z krokiem

Funkcja take-nth przyjmuje dwa argumenty. Pierwszym powinna być liczba całkowita, a drugim sekwencja. Zwrócona zostanie sekwencja na bazie przekazanej z pozostawionymi wyłącznie tymi elementami, których pozycja jest wielokrotnością (krokiem) podanej liczby, poczynając od pierwszego elementu.

Uwaga: Przekazanie wartości 0 jako pierwszego argumentu prowadzi do zapętlenia funkcji.

Rezultat zwłoczny: TAK.

Użycie:

  • (take-nth krok sekwencja).
Przykład użycia funkcji take-nth
1
2
(take-nth 3 '(1 2 3 4 5 6 7))
; => (1 4 7)

Filtrowanie, filter

Funkcja filter tworzy leniwą sekwencję, składającą się z tych elementów sekwencji podanej jako drugi argument, dla których jednoargumentowa funkcja podana jako pierwszy argument zwraca wartości różne od false i różne od nil.

Rezultat zwłoczny: TAK.

Użycie:

  • (filter predykat sekwencja).
Przykład użycia funkcji filter
1
2
(filter odd? '(1 2 3 4 5))
; => (1 3 5)

Eliminowanie, remove

Funkcja remove tworzy leniwą sekwencję, składającą się z tych elementów sekwencji podanej jako drugi argument, dla których jednoargumentowa funkcja przekazana jako pierwszy argument zwraca wartość false lub nil. Pomijane są elementy, dla których predykat tworzy logiczną prawdę.

Rezultat zwłoczny: TAK.

Użycie:

  • (remove predykat sekwencja).
Przykład użycia funkcji remove
1
2
(remove odd? '(1 2 3 4 5))
; => (2 4)

Niepowtarzalność, distinct

Funkcja distinct zwraca sekwencję, która jest takim przekształceniem sekwencji podanej jako argument, że zawiera wyłącznie elementy unikatowe (niepowtarzalne). Powtórzenia elementów są usuwane.

Rezultat zwłoczny: TAK.

Użycie:

  • (distinct sekwencja).
Przykład użycia funkcji distinct
1
2
(distinct '(1 2 2 5 2))
; => (1 2 5)

Dodawanie elementów

Sekwencje można uzupełniać o nowe elementy z użyciem odpowiednich funkcji.

Dołączanie elementu, cons

Funkcja cons dodaje element do czoła sekwencji, a zwraca leniwą sekwencję z dodanym obiektem Cons, który reprezentuje jej pierwszy element.

Rezultat zwłoczny: TAK.

Użycie:

  • (cons element sekwencja).
Przykład użycia funkcji cons
1
2
(cons 10 '(1 2 3))
; => (10 1 2 3)

Łączenie sekwencji, concat

Dzięki funkcji concat możliwe jest tworzenie sekwencji składającej się z zera lub większej liczby sekwencji podanych jako jej argumenty.

Wariantem concat jest makro lazy-cat, które działa tak samo, ale faktyczne operacje przeliczenia sekwencji nastąpią dopiero przy próbie dostępu do elementów lub wymuszonym wartościowaniu. Każda sekwencja wchodząca w skład sekwencji wynikowej jest przekształcana do postaci leniwej. Jak nietrudno się domyślić rezultatem jest również leniwa sekwencja.

Rezultat zwłoczny:

  • concat: NIE,
  • lazy-cat: TAK.

Użycie:

  • (concat & sekwencja…),
  • (lazy-cat & sekwencja…).
Przykłady użycia funkcji concat i lazy-cat
1
2
3
4
(concat [1 2 3 4] [5 6 7 8])  ; => (1 2 3 4 5 6 7 8)
(concat)                      ; => ()

(do (lazy-cat [1 2] [3 4]))   ; => (1 2 3 4) 

Zapętlanie, cycle

Funkcja cycle generuje leniwą, nieskończoną sekwencję złożoną z powtarzanych elementów sekwencji podanej jako argument.

Rezultat zwłoczny: TAK.

Użycie:

  • (cycle sekwencja).

Wartością zwracaną jest obiekt typu clojure.lang.Cycle o sekwencyjnym interfejsie dostępu.

Przykład użycia funkcji cycle
1
2
(take 10 (cycle '(1 2)))
; => (1 2 1 2 1 2 1 2 1 2)

Przeplatanie, interleave

Dzięki funkcji interleave możemy przeplatać elementy podanych sekwencji. Zwraca ona leniwą sekwencję, która jest połączeniem sekwencji podanych jako argumenty w taki sposób, że w wyniku umieszczane są najpierw pierwsze elementy każdej sekwencji, następnie drugie z każdej itd.

Rezultat zwłoczny: TAK.

Użycie:

  • (interleave & sekwencja…).
Przykłady użycia funkcji interleave
1
2
3
(interleave [:a :b] [1 2])          ; => (:a 1 :b 2)
(interleave [:a :b] [1 2] ['x 'y])  ; => (:a 1 x :b 2 y)
(interleave)                        ; => ()

Wstawianie, interpose

Dzięki funkcji interpose możemy wstawiać podany element między elementy podanej sekwencji. Rezultatem jest leniwa sekwencja, której co drugi element jest podanym jako argument separatorem. Separator nie jest umieszczany za ostatnim elementem wynikowej sekwencji.

Rezultat zwłoczny: TAK.

Użycie:

  • (interpose separator sekwencja).
Przykład użycia funkcji interpose
1
2
(interpose 0 [:a :b :c])
; => (:a 0 :b 0 :c)

Zmiana porządku

Funkcje zmieniające porządek sekwencji pozwalają reorganizować kolejność elementów, jednak przy zachowaniu ich liczby oraz wartości.

Odwracanie kolejności, reverse

Funkcja reverse pozwala odwracać kolejność sekwencji podanej jako argument. Zwraca listę (z sekwencyjnym interfejsem dostępu), której elementy ułożone są odwrotnie, niż sekwencja wejściowa.

Rezultat zwłoczny: NIE.

Użycie:

  • (reverse sekwencja).
Przykład użycia funkcji reverse
1
2
(reverse '(1 2 3 4))
; => (4 3 2 1)

Uwaga: Funkcja reverse tworzy nową strukturę danych i jej użycie może w niektórych przypadkach nie być optymalne pod względem oszczędności pamięci. Jeśli to możliwe, należy korzystać z omówionej wcześniej funkcji rseq lub rsubseq.

Sortowanie, sort

Funkcje sortsort-by zwracają sekwencję bazującą na uporządkowanej tablicy (obiekt klasy clojure.lang.ArraySeq), zawierającej elementy sekwencji podanej jako argument, które zostały posortowane zgodnie z podanymi kryteriami.

Funkcja sort przyjmuje sekwencję lub (w wariancie dwuargumentowym) funkcję porównującą elementy (tzw. komparator) i sekwencję. Komparator powinien być obiektem implementującym java.util.Comparator, a jeśli go nie podano, to użyta będzie funkcja compare. Warto nadmienić, że w Clojure każda definiowana funkcja implementuje interfejs Comparator, więc może być użyta jako operator porównujący.

Funkcja sort-by działa podobnie, ale wymaga podania dodatkowego, początkowego argumentu. Jego wartością powinna być funkcja, która dokona wartościowania każdego elementu zgodnie z intencją programisty. Uzyskana w ten sposób wartość będzie porównywana, aby uporządkować elementy. Ten wariant sortowania wykorzystuje się na przykład w obsłudze struktur asocjacyjnych, gdzie wartość skojarzona z konkretnym kluczem powinna być potraktowana jak kryterium uporządkowania.

Uwaga: Jeżeli zamiast sekwencji zostanie podany obiekt tablicy Javy, to zostanie on zmodyfikowany. Aby się przed tym uchronić, należy korzystać z jego kopii (np. korzystając z funkcji aclone).

Rezultat zwłoczny: NIE.

Użycie:

  • (sort                         sekwencja),
  • (sort              komparator sekwencja),
  • (sort-by ewaluator            sekwencja),
  • (sort-by ewaluator komparator sekwencja).
Przykłady użycia funkcji sort i sort-by
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
;; sortowanie z użyciem wbudowanego komparatora
(sort '(4 3 1 2))
; => (1 2 3 4)

;; operator "większe niż" użyty jako komparator
(sort > '(4 3 1 2))
; => (4 3 2 1)

;; własny komparator
(defn komparator [x y] (> x y))
(sort komparator '(1 2 4 3))
; => (4 3 2 1)

;; funkcja zliczająca elementy (tu znaki) jako ewaluator
(sort-by count '("siała" "baba" "mak"))
; => ("mak" "baba" "siała")

;; sortowanie mapy z funkcją val jako ewaluatorem
(sort-by val {:a 1, :b 2, :c 3})
; => ([:a 1] [:b 2] [:c 3])

;; sortowanie mapy z funkcją val jako ewaluatorem
;; i z operatorem "większe niż" w roli komparatora
(sort-by val < {:a 1, :b 2, :c 3})
; => ([:c 3] [:b 2] [:a 1])

Tasowanie, shuffle

Funkcja shuffle pozwala losowo zmienić kolejność elementów sekwencji podanej jako jej pierwszy argument. Zwraca wektor zawierający losową permutację elementów sekwencji wejściowej.

Rezultat zwłoczny: NIE.

Użycie:

  • (shuffle sekwencja).
Przykład użycia funkcji shuffle
1
2
(shuffle '(1 2 3 4 5))
; => [3 1 5 2 4]

Reorganizacja struktury

Funkcje reorganizujące strukturę sekwencji pozwalają zmienić sposób grupowania elementów z użyciem sekwencji zagnieżdżonych, które powstały w efekcie zastosowania odpowiednich operacji. Mamy tu na myśli zarówno wytwarzanie dodatkowych sekwencji zagnieżdżonych, jak też eliminowanie zagnieżdżenia.

Spłaszczanie, flatten

Dzięki funkcji flatten możemy przekształcić zagnieżdżone sekwencje i inne struktury z sekwencyjnym interfejsem dostępu (np. wektory czy listy) w taki sposób, że wynikowa sekwencja będzie miała tylko jeden wymiar (nie będzie zagnieżdżona).

W przypadku podania wartości nil jako pierwszy argument, zwracana jest sekwencja pusta.

Rezultat zwłoczny: TAK.

Użycie:

  • (flatten sekwencja).
Przykład użycia funkcji flatten
1
2
(flatten '(zakupy (spożywczy (płatki ser) (kiosk szlugi))))
; => (zakupy spożywczy płatki ser kiosk szlugi)

Dzielenie

Podziału sekwencji można dokonać z użyciem funkcji split, split-atsplit-with.

Partycjonowanie, partition

Funkcja partition służy do partycjonowania, czyli dzielenia sekwencji na podgrupy (będące również sekwencjami). Występuje w trzech wariantach ze względu na liczbę przyjmowanych argumentów.

W wersji dwuargumentowej pierwszym argumentem powinna być liczba elementów do umieszczenia w każdej z grup, a drugim sekwencja. Rezultatem będzie leniwa sekwencja złożona z leniwych sekwencji, z których każda będzie zawierała podaną liczbę elementów. Jeżeli elementów sekwencji źródłowej nie da się bez reszty podzielić na grupy i ostatnia grupa byłaby niepełna, to elementy takie zostaną pominięte w rezultatach.

W wersji trójargumentowej pierwszym argumentem powinna być liczba elementów do umieszczenia w każdej z grup, drugim tzw. krok, czyli liczba elementów, które zostaną pominięte po przejściu do generowania kolejnej grupy, a ostatnim sekwencja. Jeżeli jako krok podamy wartość mniejszą niż pierwszy argument, to pewne elementy będą powielone w kolejnych grupach, a jeżeli większą, pomijane (nie pojawią się w żadnej grupie). Wartość kroku równa liczbie elementów (podanej jako pierwszy argument) sprawi, że funkcja zachowa się tak jak jej wariant dwuargumentowy. Krok jest po prosu liczbą elementów, o jaką wykonywany jest przeskok za każdym razem, gdy zakończone jest generowanie grupy i następuje przejście do kolekcjonowania elementów następnej. Określa on względną pozycję, od której zacznie się ich pobieranie. Rezultatem wywołania trójargumentowego wariantu funkcji partition będzie leniwa sekwencja złożona z leniwych sekwencji, z których każda będzie zawierała podaną liczbę elementów. Jeżeli elementów sekwencji źródłowej nie da się bez reszty (uwzględniając też krok) podzielić na grupy i ostatnia grupa byłaby niepełna, to elementy takie zostaną pominięte w rezultatach.

W wersji czteroargumentowej pierwszym argumentem powinna być liczba elementów do umieszczenia w każdej z grup, drugim tzw. krok (liczba elementów, które zostaną pominięte po przejściu do generowania kolejnej grupy), trzecim dopełnienie (sekwencja elementów umieszczanych w ostatniej grupie, jeżeli jest niepełna), a ostatnim sekwencja. Rezultatem wywołania funkcji będzie leniwa sekwencja złożona z leniwych sekwencji, z których każda będzie zawierała podaną liczbę elementów. Jeżeli elementów sekwencji źródłowej nie da się bez reszty (uwzględniając też krok) podzielić na grupy i ostatnia grupa byłaby niepełna, to zostanie ona uzupełniona kolejnymi elementami pochodzącymi z sekwencji dopełniającej (podanej jako przedostatni argument). Jeżeli sekwencja ta zawiera mniejszą liczbę elementów, niż potrzebna do uzupełnienia ostatniej grupy, to będzie ona mniej liczna, niż wszystkie pozostałe.

Rezultat zwłoczny: TAK.

Użycie:

  • (partition liczba                  sekwencja),
  • (partition liczba krok             sekwencja),
  • (partition liczba krok dopełnienie sekwencja).
Przykłady użycia funkcji partition
1
2
3
4
5
6
7
8
(partition 2              [1 2 3 4 5])  ; => ((1 2) (3 4))
(partition 2 2            [1 2 3 4 5])  ; => ((1 2) (3 4))
(partition 2 1            [1 2 3 4 5])  ; => ((1 2) (2 3) (3 4) (4 5))
(partition 2 3            [1 2 3 4 5])  ; => ((1 2) (4 5))
(partition 2 2 [0]        [1 2 3 4 5])  ; => ((1 2) (3 4) (5 0))
(partition 2 2 (repeat 0) [1 2 3 4 5])  ; => ((1 2) (3 4) (5 0))
(partition 2                  "trala")  ; => ((\t \r) (\a \l))
(take 3 (partition 3 0 [1 2 3 4 5]))    ; => ((1 2 3) (1 2 3) (1 2 3))

Uwaga: Podanie 0 jako kroku spowoduje wygenerowanie sekwencji o nieskończonej długości (nieskończonej liczbie grup), ponieważ nie będzie wykonywany przeskok (nawet o jeden element).

Partycjonowanie wszystkich, partition-all

Funkcja partition-all działa podobnie do partition, jednak ostatnia grupa elementów zostanie wygenerowana nawet, jeśli ich liczba będzie mniejsza, niż wymagana. Występuje w dwóch wariantach ze względu na liczbę przyjmowanych argumentów.

W wersji dwuargumentowej pierwszym argumentem powinna być liczba elementów do umieszczenia w każdej z grup, a drugim sekwencja. Rezultatem będzie leniwa sekwencja złożona z leniwych sekwencji, z których każda będzie zawierała podaną liczbę elementów. Ostatnia grupa może zawierać mniej elementów, jeżeli podział bez reszty nie jest możliwy.

W wersji trójargumentowej pierwszym argumentem powinna być liczba elementów do umieszczenia w każdej z grup, drugim tzw. krok, czyli liczba elementów, które zostaną pominięte po przejściu do generowania kolejnej grupy, a ostatnim sekwencja. Krok jest liczbą elementów, o jaką wykonywany jest przeskok, gdy zakończone jest generowanie grupy i następuje kolekcjonowanie elementów wchodzących w skład następnej. Określa on względną pozycję, od której zacznie się grupowanie. Rezultatem wywołania trójargumentowego wariantu funkcji partition-all będzie leniwa sekwencja złożona z leniwych sekwencji, z których każda będzie zawierała podaną liczbę elementów. Ostatnia grupa może zawierać mniej elementów, jeżeli podział bez reszty nie jest możliwy.

Rezultat zwłoczny: TAK.

Użycie:

  • (partition-all liczba      sekwencja),
  • (partition-all liczba krok sekwencja).
Przykłady użycia funkcji partition-all
1
2
3
4
5
6
(partition-all 2            [1 2 3 4 5])  ; => ((1 2) (3 4) (5))
(partition-all 2 2          [1 2 3 4 5])  ; => ((1 2) (3 4) (5))
(partition-all 2 1          [1 2 3 4 5])  ; => ((1 2) (2 3) (3 4) (4 5) (5))
(partition-all 2 3          [1 2 3 4 5])  ; => ((1 2) (4 5))
(partition-all 2                "trala")  ; => ((\t \r) (\a \l) (\a))
(take 3 (partition-all 3 0 [1 2 3 4 5]))  ; => ((1 2 3) (1 2 3) (1 2 3))

Uwaga: Podanie 0 jako kroku spowoduje wygenerowanie sekwencji o nieskończonej długości (nieskończonej liczbie grup), ponieważ nie będzie wykonywany przeskok (nawet o jeden element).

Partycjonowanie operatorem, partition-by

Funkcja partition-by działa podobnie do partition, tzn. dzieli podaną sekwencję na grupy, lecz kryterium decydującym o elementach wchodzących w skład grup nie jest liczba i/lub krok, ale rezultat wykonywania funkcji przekazanej jako pierwszy argument wywołania. Powinna ona przyjmować jeden argument, którym będzie wartość każdego z przetwarzanych elementów i zwracać pojedynczą wartość. Drugim argumentem powinna być źródłowa sekwencja.

Rezultatem wykonania funkcji będzie leniwa sekwencja złożona z leniwych sekwencji, które mogą różnić się liczbą elementów. Do podziału będzie dochodziło wtedy, gdy przekazana funkcja zwróci wartość różną od poprzednio zwracanej.

Rezultat zwłoczny: TAK.

Użycie:

  • (partition-by funkcja sekwencja).
Przykłady użycia funkcji partition-by
1
2
3
4
5
(partition-by #(=    3 %) [1 2 3 4 5])  ; => ((1 2) (3) (4 5))
(partition-by #(not= 3 %) [1 2 3 4 5])  ; => ((1 2) (3) (4 5))
(partition-by identity    [1 2 3 4 5])  ; => ((1) (2) (3) (4) (5))
(partition-by identity    [1 1 2 3 3])  ; => ((1 1) (2) (3 3))
(partition-by #{\a}           "trala")  ; => ((\t \r) (\a) (\l) (\a))

Zauważmy, że w ostatniej linii przykładu korzystamy ze zbioru jako funkcji.

Modyfikowanie elementów sekwencji

Grupa funkcji odpowiedzialna za modyfikowanie wartości elementów sekwencji pozwala tworzyć sekwencje pochodne o różnych elementach.

Przeliczanie

Przeliczaniem sekwencji nazwiemy takie przetwarzanie jej kolejnych elementów, że na bazie przeprowadzanych operacji powstaje sekwencja pochodna, której liczba elementów może być różna od wejściowej, a której wartości elementów są rezultatami wykonywanych obliczeń na elementach sekwencji wejściowej. W kategorii tej znajdą się funkcje, które nie modyfikują każdego elementu z osobna, ale które tworzą nowe sekwencje na podstawie operacji, które uwzględniają więcej niż jeden element źródłowy w tym samym czasie.

Rezultaty redukowania, reductions

Dzięki funkcji reductions możemy poznać kolejne etapy redukcji przeprowadzanej z użyciem reduce, czyli wartości zwracane przez podaną funkcję operatora. Nietrudno więc się domyślić, że reductions działa w podobny sposób, co reduce, ale zamiast pojedynczej wartości tworzy sekwencję wartości pośrednich.

Funkcja jako drugi argument przyjmuje sekwencję, a jako pierwszy dwuargumentowy operator, który powinien być użyty względem kolejnych elementów tej sekwencji w taki sposób, że wynik zastosowania operatora na poprzednim elemencie jest wcześniej akumulowany, a następnie używany jako pierwszy argument następnego wywołania operatora, gdy drugim argumentem operatora jest aktualnie przetwarzany element sekwencji.

W swojej trójargumentowej wersji funkcja pozwala podać początkową wartość akumulatora jako drugi argument, a sekwencję jako trzeci.

Rezultat zwłoczny: TAK.

Użycie:

  • (reductions operator         sekwencja).
  • (reductions operator wartość sekwencja).
Przykład użycia funkcji reductions
1
2
(reductions +    '(1 2 3 4))  ; => (1 3 6 10)
(reductions + 20 '(1 2 3 4))  ; => (20 21 23 26 30)

Transformowanie

Przekształcanie warunkowe, keep-indexed

Funkcja keep działa podobnie jak filter, z tą jednak różnicą, że zamiast predykatu jako pierwszy argument przyjmuje funkcję, która transformuje wartości kolejnych elementów sekwencji podanej jako drugi argument. Wyjątkiem są te wywołania funkcji przekształcającej, dla których zwraca ona wartość nil. Takie elementy nie będą umieszczane w sekwencji wynikowej.

Warto zauważyć, że funkcja wygeneruje wyniki dla wartości truefalse zwróconych przez funkcję transformującą.

Wariantem funkcji keep jest keep-indexed. W jej przypadku funkcja przekształcająca jako pierwszy argument powinna przyjmować numer kolejny elementu w sekwencji (poczynając od 0), a element jako drugi.

Rezultat zwłoczny: TAK.

Użycie:

  • (keep          transformator sekwencja),
  • (keep-indexed  transformator sekwencja).
Przykłady użycia funkcji keep i keep-indexed
1
2
3
4
5
6
(keep inc              '(1 2 3 4 5))  ; => (2 3 4 5 6)
(keep #(if (<= % 3) %) '(1 2 3 4 5))  ; => (1 2 3)
(keep odd?             '(1 2 3 4 5))  ; => (true false true false true)

(keep-indexed #(if (odd?  %1) %2) '(1 2 3 4 5))  ; => (2 4)
(keep-indexed #(if (even? %1) %2) '(a b c d))    ; => (a c)

Odwzorowywanie, map

Dzięki funkcji map możemy przekształcać wartości elementów na podstawie podanej funkcji transformującej. Przyjmuje ona dwa argumenty. Pierwszy z nich to funkcja, która będzie wywoływana dla kolejnych elementów, a drugi to sekwencja poddawana przekształcaniu. Zwracaną przez map wartością jest leniwa sekwencja na bazie podanej ze zmienionymi wartościami elementów.

Gdy podano więcej niż jedną sekwencję, to przekazana funkcja musi być w stanie przyjąć odpowiednio większą liczbę argumentów, ponieważ w takim przypadku przetwarzane będą kolejne elementy z każdej podanej sekwencji jednocześnie.

Odmianą funkcji map, która do funkcji przekształcającej przekazuje dodatkowo numer kolejny elementu (poczynając od 0) jest map-indexed.

Rezultat zwłoczny: TAK.

Użycie:

  • (map         transformator sekwencja & sekwencja…),
  • (map-indexed transformator sekwencja & sekwencja…).
Przykłady użycia funkcji map i map-indexed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(map inc '(1 2 3 4))
; => (2 3 4 5)

(map + [1 2 3] [1 2 3])
; => (2 4 6)

(map #(if (even? %) %) [1 2 3 4])
; => (nil 2 nil 4)

(remove nil? (map #(if (even? %) %) [1 2 3 4]))
; => (2 4)

(map #(str "Witaj, " %) '(świecie matko ojcze))
; => ("Witaj, świecie" "Witaj, matko" "Witaj, ojcze")

(map-indexed #(str %1 ". Witaj, " %2) '(świecie matko ojcze))
("0. Witaj, świecie" "1. Witaj, matko" "2. Witaj, ojcze")

Ciekawym wariantem funkcji map jest pmap. Zasada jej działania jest taka sama jak omówionej poprzedniczki, ale operacje przeprowadzane są równolegle. Zrównoleglaniu podlega wywoływanie funkcji transformującej wartości i dzieje się to dla każdego elementu. Wątki są koordynowane i synchronizowane przed oddawaniem kolejnych rezultatów, więc elementy sekwencji wyjściowej mają zachowaną kolejność.

Rezultat zwłoczny: TAK.

Użycie:

  • (pmap transformator sekwencja & sekwencja…).
Przykłady użycia funkcji pmap
1
2
(pmap inc '(1 2 3 4))       ; => (2 3 4 5)
(pmap +   [1 2 3] [1 2 3])  ; => (2 4 6)

Spójrzmy na różnicę w czasie realizacji między mappmap, gdy podana funkcja trwa dłużej.

Przykład pokazujący różnicę w działaniu między mappmap.
1
2
3
4
5
6
7
8
9
(defn długo [n] (Thread/sleep 1000) n)

(time (doall (map długo [1 2 3])))
; >> "Elapsed time: 3000.512006 msecs"
; => (1 2 3)

(time (doall (pmap długo [1 2 3])))
; >> "Elapsed time: 1003.371378 msecs"
; => (1 2 3)

Zastępowanie, replace

Dzięki funkcji replace możemy zastępować wartości sekwencji innymi podanymi. Występuje ona w dwóch wariantach. Pierwszy bazuje na wektorze podanym jako pierwszy argument. Dla każdego elementu sekwencji (podanej jako drugi argument) zostanie wykonane przeszukanie wektora pod kątem istnienia indeksu o numerze określonym wartością tego elementu. Jeśli element o podanym indeksie znajduje się w wektorze, to zastąpi on numer indeksu będący elementem sekwencji.

Drugi wariant replace jako pierwszy argument przyjmuje mapę, której klucze określają elementy przeznaczone do zamiany, a przypisane do nich wartości mówią o tym co powinno być podstawione w ich miejsce. Jest to transformacja słownikowa.

Oba warianty funkcji replace przyjmują sekwencję jako drugi argument, a zwracają sekwencję z zastąpionymi wartościami elementów.

Rezultat zwłoczny: TAK.

Użycie:

  • (replace wektor sekwencja),
  • (replace mapa   sekwencja).
Przykłady użycia funkcji replace
1
2
(replace [:a :b :c]             '(0 1 1 0))      ; => (:a :b :b :a)
(replace {:a 1, :b 2, :c 'trzy} '(:a :b :c :d))  ; => (1 2 trzy :d)

Zaawansowane przekształcanie, for

Makro for służy do zaawansowanego przekształcania sekwencji z użyciem podanych jako pierwszy argument par powiązaniowych, składających się z symboli i przypisanych im kolekcji o sekwencyjnym interfejsie dostępu. W wektorze powiązaniowym mogą znaleźć się także specyficzne dla for pary modyfikatorów o etykietach wyrażonych słowami kluczowymi:

  • :when  (wywołujący when),
  • :let   (wywołujący let),
  • :while (wywołujący while).

Umieszczenie modyfikatora w wektorze powiązaniowym sprawia, że będzie wywołana formuła specjalna lub makro o odpowiadającej mu nazwie, a znajdujące się po nim wartości zostaną przekazane jako argumenty. W formule tej będzie można korzystać z symbolicznych identyfikatorów ustawionych wcześniej.

Rezultatem wywołania for jest wartość wyrażenia podanego jako jego drugi argument. Wyrażenie to objęte jest leksykalnym zasięgiem symbolicznych identyfikatorów skojarzonych wcześniej z wartościami w wektorze powiązań. Dla każdego przebiegu symbol skojarzony z sekwencją będzie powiązany z jej kolejnym elementem.

W przypadku więcej niż jednej pary powiązaniowej for dokona zagnieżdżonej iteracji dla każdego elementu z każdym, poczynając od elementów powiązania podanego jako pierwsze.

Rezultat zwłoczny: TAK.

Użycie:

  • (for wektor-powiązaniowy wyrażenie).
Przykłady użycia funkcji for
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
(def sekwencja (seq '(1 2 3)))

;; mnożenie przez 2
(for [element1 sekwencja] (* element1 2))
; => (2 4 6)

;; dodawanie każdego elementu do każdego
;; (zagnieżdżone iteracje)
(for [element1 sekwencja
      element2 sekwencja]
  (+ element1 element2))
; => (2 3 4 3 4 5 4 5 6)

;; kilka działań z wykorzystaniem modyfikatorów
(for [element1 sekwencja
      :let   [razy-dwa (* 2 element1)]
      :while (< razy-dwa 5)]
  razy-dwa)
; => (2 4)

;; tworzenie sekwencji wektorów z kombinacjami
;; (zagnieżdżone iteracje)
(for [element1 sekwencja
      element2 '(:a :b)]
  [element2 element1])
; => ([:a 1] [:b 1] [:a 2] [:b 2] [:a 3] [:b 3])

Zobacz także:

Łączenie z transformacją, mapcat

Funkcja mapcat działa jak concat, ale przyjmuje dodatkowy argument, który powinien zawierać funkcję przekształcającą, która zostanie wywołana dla każdego kolejnego argumentu, zanim dokonane będzie połączenie. W tym celu wywołana będzie wewnętrznie funkcja map.

Rezultat zwłoczny: TAK (z ograniczeniami).

Użycie:

  • (mapcat funkcja & sekwencja…).

Przekazywana jako pierwszy argument funkcja powinna zwracać obiekty wyposażone w sekwencyjny interfejs dostępu (np. kolekcję i/lub sekwencję). Wartościami kolejnych, opcjonalnych argumentów powinny być sekwencje lub kolekcje o sekwencyjnym interfejsie dostępu, które będą poddane złączeniu.

Uwaga: Funkcja map będzie wywołana dla każdej sekwencji podanej jako argument, a nie dla każdego elementu sekwencji.

Jeśli przekazana funkcja zwróci nil lub kolekcję pustą, to sekwencja wejściowa zostanie pominięta i nie będzie dołączona do sekwencji wynikowej.

Wartością zwracaną jest leniwa sekwencja.

Przykłady użycia funkcji mapcat
1
2
(mapcat reverse          [[1 2] [3 4]])    ; => (2 1 4 3)
(mapcat #(remove odd? %) [[1 2] [3 4 5]])  ; => (2 4)

Uwaga: Funkcja mapcat korzysta wewnętrznie z funkcji apply, i w pewnych warunkach jej użycie może prowadzić do wygenerowania rezultatów obliczeń zamiast leniwej sekwencji.

Jeżeli użyjemy mapcat do rekurencyjnego przekształcania i łączenia zagnieżdżonych struktur, może zdarzyć się, że wywoływana przez nią funkcja apply wymusi przeliczanie niektórych fragmentów. Aby temu zaradzić, można zamiast mapcat zastosować funkcję, która zabezpiecza nas przed wystąpieniem takiego zjawiska:

Zwłoczna wersja funkcji mapcat
1
2
3
4
5
6
(defn lazy-mapcat
  "W pełni zwłoczny wariant funkcji mapcat."
  [f kol]
  (lazy-seq
   (when (not-empty kol)
     (concat (f (first kol)) (lazy-mapcat f (rest kol))))))

Testy

Istnieje szereg funkcji, dzięki którym możemy sprawdzać właściwości sekwencji oraz ich elementów.

Predykaty

Sprawdzić, czy dana sekwencja spełnia pewne warunki odnośnie jej cech można z użyciem odpowiednich predykatów. Funkcje te zwracają wartość true, gdy dana właściwość występuje, a false, kiedy jest nieobecna.

Rezultat zwłoczny: NIE

Użycie:

  • (every?     predykat sekwencja)true, gdy predykat jest prawdziwy dla każdego elementu;
  • (not-any?   predykat sekwencja)true, gdy predykat jest fałszywy dla każdego elementu;
  • (not-every? predykat sekwencja)true, gdy predykat jest fałszywy dla choć jednego elementu;
  • (empty?              sekwencja)true, gdy sekwencja nie ma żadnych elementów;
  • (realized?           sekwencja)true, gdy sekwencja leniwa została przeliczona;
  • (seq?                  wartość)true, gdy podany argument jest sekwencją.

Niepustość, not-empty

Dzięki funkcji not-empty jesteśmy w stanie upewnić się, że podana jako argument sekwencja nie jest pusta. Jeśli jest, zwrócona będzie wartość nil, a w przeciwnym razie podana sekwencja.

Rezultat zwłoczny: (zależy od wejścia).

Użycie:

  • (not-empty sekwencja).
Przykład użycia funkcji not-empty
1
2
(not-empty (seq '(1 2 3)))  ; => (1 2 3)
(not-empty (seq ()))        ; => nil

Sekwencje jako kolejki

Kolejka (ang. queue) to struktura danych o liniowym charakterze, która wykorzystywana jest do obsługi komunikacji związanej ze zdarzeniami, gdzie wymagane jest buforowanie wymienianych danych z zachowaniem kolejności ich napływania. Przypomina listę, ponieważ również mamy do czynienia z uporządkowanymi elementami, jednak jej interfejs dostępowy jest nieco uboższy. Podstawowymi operacjami jest umieszczenie elementu w kolejce i pobranie elementu.

Obiekty dodawane do kolejki trafiają na jej koniec, natomiast pobierane z niej są te, które znajdują się na początku. Taką podstawową kolejkę nazywamy w skrócie FIFO (z ang. First In, First Out – pierwszy na wejściu, pierwszy na wyjściu).

Istnieją także inne warianty kolejek, np. tzw. kolejki priorytetowe, używane w systemach operacyjnych do szeregowania zadań, które pozwalają na dostęp do elementów z użyciem kluczy określających ich priorytety.

Kolejki pełnią często rolę buforów w operacjach wejścia–wyjścia i w sieciowej transmisji strumieniowej. Ich użycie sprawia, że dane, które napływają bądź są wysyłane z opóźnieniami, mogą być dostarczane w stałym, średnim tempie i/lub w porcjach o jednakowej wielkości.

Kolejki Javy

W Javie można tworzyć kolejki z wykorzystaniem interfejsu Queue. Sprawia on, że na podanej kolekcji można używać metod offer (do wstawiania elementów na początek kolejki), poll (do pobierania elementów z końca kolejki) i peek (do sprawdzania czoła kolejki bez usuwania elementu).

W Javie istnieje też mechanizm tzw. kolejek blokujących (ang. blocking queues). Przydają się one w programowaniu współbieżnym, np. do rozdzielania zadań na poszczególne wątki i do gromadzenia rezultatów, które mają dalej być przetwarzane nierównolegle lub prezentowane.

W interfejsie BlockingQueue znajdziemy te same metody, co w Queue, ale dodatkowo w wariantach, dzięki którym możliwe jest oczekiwanie, aż kolejka nie będzie pusta podczas pobierania elementu, a także oczekiwanie na możliwość dodania nowego elementu, gdy kolejka jest całkowicie zapełniona. Opcjonalnie można podawać maksymalne długości czasów oczekiwań na wykonanie operacji.

Kolejki w Clojure

Język Clojure wyposażono, chociaż jeszcze nie oficjalnie (kwiecień 2015), w obsługę kolejek Javy. Za ich tworzenie odpowiada funkcja clojure.lang.PersistentQueue/EMPTY, która nie doczekała się jeszcze literalnej reprezentacji w postaci makra czytnika czy funkcji opakowującej.

Przykład użycia kolejki Javy
1
2
3
4
(def kolejka (conj (clojure.lang.PersistentQueue/EMPTY) :a :b :c))

(seq kolejka)        ; => (:a :b :c)
(seq (pop kolejka))  ; => (:b :c)

Sekwencje kolejkujące, seque

Dzięki funkcji seque możemy utworzyć sekwencję kolejkującą (ang. queued sequence) na bazie podanej sekwencji. Mechanizmy funkcji zadbają o to, aby podana w argumencie sekwencja była wyliczana, a wytwarzane przez nią rezultaty buforowane z użyciem mechanizmu blokującej kolejki (interfejsy BlockingQueueLinkedBlockingQueue).

Działanie sekwencji kolejkującej pomaga obsługiwać sytuacje, w których sekwencja bazuje na zewnętrznym strumieniu wejściowym (np. gnieździe sieciowym) lub jej funkcja generująca jest nierównomiernie czasochłonna, a istnieje potrzeba buforowania, np. w celu uzyskiwania danych w stałym tempie i/lub przetwarzania stałej liczby elementów za każdym razem.

Funkcja występuje w trzech wariantach. Pierwszy przyjmuje sekwencję, a zwraca leniwą sekwencję powiązaną wewnętrznie z kolejką, której generatorem zawartości będzie sekwencja podana jako argument.

Drugi wariant seque przyjmuje dwa argumenty. Pierwszy z nich powinien określać wielkość bufora kolejkującego (wyrażoną w liczbie elementów), a drugi być sekwencją źródłową.

Trzeci wariant funkcji jest również dwuargumentowy, ale pierwszy argument powinien być obiektem BlockingQueue, który zostanie powiązany z sekwencją.

Rezultat zwłoczny: TAK.

Użycie:

  • (seque                sekwencja),
  • (seque liczba         sekwencja),
  • (seque obiekt-kolejki sekwencja).
Przykład użycia funkcji seque
1
2
3
4
5
6
(def kolejka (seque
              (repeatedly #(Thread/sleep 300))))
(println "zapełnianie bufora...")
(Thread/sleep 3000)
(doseq [x (take 20 kolejka)]
  (println "mam" (new java.util.Date)))

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

Komentarze