Poczytaj mi Clojure – cz. 4: Struktury danych

Struktury danych to, obok algorytmów wyrażanych operacjami, podstawowy składnik każdego programu komputerowego. Budując aplikacje, wybieramy takie struktury, które będą najlepiej nadawały się do przetwarzania informacji.

Struktury danych

Struktury danych (ang. data structures) to sposób w jaki program komputerowy organizuje informacje w pamięci operacyjnej komputera. W zależności od problemu wybieramy – obok odpowiednich algorytmów – takie struktury, które pozwolą najefektywniej go rozwiązać. Przykładem struktury danych dopasowanej do potrzeb może być drzewo binarne wykorzystywane przez algorytm kompresji. Struktury danych są abstrakcyjnymi obiektami o cechach, które mogą być wyrażane z użyciem różnych typów danych.

Typy danych (ang. data types) to z kolei klasy wartości o wspólnych właściwościach, np. możliwych zakresach, wielkości zajmowanej przestrzeni pamięciowej czy sposobie reprezentowania informacji. Również w przypadku typów możemy mieć do czynienia z ich doborem odpowiednim do problemu. Na przykład obliczenia matematyczne ze względów wydajnościowych lepiej przeprowadzać na typach numerycznych, a nie na łańcuchach znakowych reprezentujących liczby (chociaż jest to możliwe i czasem stosowane, np. w rachunkach operujących na naprawdę wielkich wartościach).

W Clojure struktury danych są trwałe (ang. persistent). Oznacza to, że w przypadku modyfikacji zachowywane są ich oryginalne wersje, a dodatkowo tworzone są obiekty pochodne, które wyrażają powstałe zmiany.

Do zmiany nie dochodzi bezpośrednio w strukturze danego obiektu, lecz jest ona albo zapamiętywana jako operacja, którą przy każdej próbie odczytu trzeba wykonać na wartości bazowej, aby uzyskać wartość bieżącą (tzw. podejście zwłoczne, zwane też leniwym), albo jako odniesienia do elementów oryginalnej struktury umieszczane w odpowiednich miejscach struktury wynikowej – nazywamy to współdzieleniem strukturalnym (ang. structural sharing).

Złożone, wbudowane struktury języka Clojure bazują na wspomnianym współdzieleniu, które realizowane jest przez implementację drzew o dużej rozpiętości, wyposażonych w indeksy przyspieszające przeszukiwanie. Pozwala to współdzielić ich niezmienione fragmenty między kolejnymi instancjami i tym samym oszczędza pamięć.

Na bazie trwałych struktur możliwa jest obsługa tzw. danych niemutowalnych (ang. immutable data), czyli takich, które nie zmieniają się w miejscu pamięciowego rezydowania. Wykonywanie operacji na wartościach sprawia, że powstają nowe wartości, a nie dochodzi do modyfikacji istniejących.

Chociaż wewnętrznie mamy do czynienia ze współdzieleniem danych w obrębie struktur trwałych i modyfikacjami odniesień do fragmentów, to procesy te są bezpiecznie izolowane i ukrywane przed programistą – z jego perspektywy operacja wykonywana na strukturze daje w efekcie całkiem nowy obiekt. Przykładem może być tu duży zbiór, do którego dodajemy jeden element. Z punktu widzenia programisty w wyniku takiej operacji powstanie osobny zestaw elementów, wzbogacony o dodaną wartość, jednak “pod maską” mamy do czynienia ze współdzieleniem części drzewiastej struktury między oryginalnym zbiorem, a strukturą pochodną, która różni się od niego pewnym fragmentem i z aktualizacją odpowiednich odniesień oraz liczników.

Poza danymi niemutowalnymi istnieją też w Clojure klasy obiektów, których pamięciową zawartość możemy nadpisywać. Są to tzw. typy referencyjne (ang. reference types). Same nie przechowują wartości, lecz – jak wskazuje nazwa – umożliwiają odnoszenie się do innych danych. Można też powiedzieć, że pozwalają na tworzenie stałych tożsamości, które wyrażają zmieniające się stany. Dzięki typom referencyjnym możemy w Clojure korzystać z mechanizmów wymiany informacji między równolegle wykonywanymi czynnościami, a także globalnie (w całym programie) identyfikować wybrane pamięciowe obiekty (np. ważne ustawienia, dane wejściowe czy funkcje).

W języku Clojure mamy przede wszystkim do czynienia z operowaniem na powiązaniach (ang. bindings) symbolicznych nazw lub typów referencyjnych z wartościami (ang. values), a nie na zmiennych (ang. variables). Wartości są niezmienne, a sposobem tworzenia nowych jest wykonywanie operacji na istniejących. Jeżeli powstałe rezultaty mają być identyfikowane, można nadać im symboliczne nazwy, które będą miały leksykalny, globalny lub dynamiczny zasięg.

Nazwanie wartości polega na powiązaniu (ang. bind) z nią identyfikatora w postaci tzw. symbolu – służą do tego różne formuły języka, które różnią się zasięgiem i widocznością. Do wartości można się też – jak wcześniej zauważyliśmy – odnosić (ang. refer) przez powiązanie z nią jednego z typów referencyjnych. Nic nie stoi na przeszkodzie, aby z jednej strony obiekt referencyjny wskazywał w danym czasie na jakąś wartość, a z drugiej sam był identyfikowany symboliczną nazwą. Jest to często spotykane.

Programując w Clojure, będziemy też mieli do czynienia z sytuacją, gdzie obiekt typu referencyjnego (używany do wyrażania zmieniającego się w czasie stanu, czyli szeregu wartości) będzie odnosił się w danym czasie do jakiejś stałej wartości, a z kolei na niego wskazywał będzie inny obiekt referencyjny (tzw. zmienna globalna), identyfikowany symbolem umieszczonym w widocznym ze wszystkich miejsc programu słowniku zwanym przestrzenią nazw. Zmienna globalna pozwoli odwoływać się do pierwszego obiektu referencyjnego z użyciem nadanej mu symbolicznej nazwy, a sam obiekt do wybranej wartości bieżącej.

Symbole

Symbol (ang. symbol) to typ danych, który pomaga w identyfikowaniu umieszczonych w pamięci wartości przez nadawanie im symbolicznych nazw. W Clojure symbole mogą mieć specjalne znaczenie, ponieważ są przez język używane do obsługi powiązań. Powiązania polegają na kojarzeniu symboli ze stałymi wartościami bądź z obiektami referencyjnymi. Możemy potem korzystać z alfanumerycznych identyfikatorów przechowujących odniesienia do rezultatów jakichś operacji.

Nazwa, stanowiąca jednocześnie identyfikator symbolu, jest łańcuchem znakowym rozpoczynającym się znakiem niebędącym cyfrą i mogącym zawierać znaki alfanumeryczne oraz: *, +, !, -, _ i ?.

Przykład użycia symboli
1
(nazwa 1 drugi 3)

Podczas obliczania wartości powyższego S-wyrażenia pierwszy i trzeci element podanej listy będzie potraktowany jak symbol. Mechanizmy języka zmienią napis nazwa oraz drugi w symbole, aby odszukać identyfikowane nimi powiązania i poznać ich wartości. Symbole zostaną tu więc potraktowane jako tzw. formuły symbolowe.

Nic nie stoi na przeszkodzie, aby programista korzystał z symboli do oznaczania własnych informacji, które są ważne z punktu widzenia logiki przyjętej w programie. W takich zastosowaniach symbole nie są traktowane jak elementy powiązań, ale jak wartości stałe, których już się nie przelicza.

Formy symboli

Istnieją trzy podstawowe formuły bazujące na symbolach:

  1. Symbol niezacytowany będzie tworzył tzw. formułę symbolową (formę symbolową), wyrażającą odniesienie do jakiejś wartości. Na przykład:

    • nazwa,
    • (nazwa inna-nazwa).
  2. Symbol zacytowany lub wygenerowany z użyciem funkcji symbol będzie tworzył formułę stałą (formę stałą), reprezentującą sam obiekt symbolu, a nie identyfikowaną nim wartość. Na przykład:

    • 'nazwa,
    • '(nazwa inna-nazwa),
    • (quote nazwa),
    • (symbol "nazwa").
  3. W pewnych konstrukcjach (np. w: formułach specjalnych let oraz def, makrze defn czy wektorze parametrycznym funkcji) niezacytowane symbole będą tworzyły tzw. formuły powiązaniowe. Dzięki nim możliwe jest nazywanie umieszczonych w pamięci obiektów (np. rezultatów wykonywanych operacji lub wartości wyrażonych literalnie). Na przykład:

    • let – powiązanie leksykalne: (let [nazwa 5] nazwa),
    • def – powiązanie zmiennej globalnej: (def nazwa 5),
    • defn – powiązanie zmiennej globalnej z funkcją: (defn nazwa []),
    • wektor parametryczny funkcji: (fn [nazwa] nazwa).

W przeciwieństwie do symboli znanych z innych języków programowania i do typu danych zwanego kluczami, symbole w Clojure nie są automatycznie internalizowane. Oznacza to, że mogą istnieć dwa symbole o takiej samej nazwie, reprezentowane przez dwa różne obiekty pamięciowe. W praktyce jednak nie będzie miało to znaczenia, ponieważ w porównaniach dwa symbole o takich samych nazwach i łańcuchach znakowych określających przestrzenie nazw będą uznawane za takie same. Warto jednak fakt ten uwzględniać, używając symboli jako indeksów dużych struktur asocjacyjnych – lepiej wtedy skorzystać z kluczy.

Przykład dwóch tak samo nazwanych symboli
1
2
3
4
5
(identical? 'a 'a)   ; czy te dwa symbole są tym samym obiektem?
; => false           ; nie są

(= 'a 'a)            ; czy te dwa symbole są równe?
; => true            ; tak, są 

Budowa symboli

Wewnętrznie symbole są obiektami składającymi się z:

  • etykiety tekstowej (nazwy), wyrażonej łańcuchem znakowym o wspomnianych wcześniej właściwościach;

  • opcjonalnego łańcucha znakowego oznaczającego przestrzeń nazw (ang. namespace), do której powinny być przydzielone niektóre identyfikowane symbolami obiekty (jeśli z przestrzeni nazw korzystają).

Sam symbol nie jest w momencie tworzenia umieszczany w żadnej mapie reprezentującej przestrzeń nazw, ale można oznaczyć go w taki sposób, żeby później skorzystały z tej informacji inne konstrukcje języka (np. omówione w dalszych rozdziałach obiekty typu Var).

Symbole, które zawierają informację o przestrzeni nazw, nazywamy symbolami z dookreśloną przestrzenią nazw (ang. namespace-qualified symbols), czasem też można spotkać się z żargonowym określeniem symbole w pełni kwalifikowane (ang. fully-qualified symbols).

Przykłady symboli z dookreśloną przestrzenią nazw
1
2
przestrzeń/nazwa
'inna/inna-nazwa

Symbole same nie przechowują odniesień do wartości, które z ich pomocą są identyfikowane. Ich użyteczność w tym zakresie polega na tym, że formuły symbolowe są podczas przeliczania wyrażeń traktowane jak identyfikatory. Dochodzi wtedy do przeszukania różnych (w zależności od kontekstu) obszarów, w których mogą znajdować się odwzorowania symboli na wartości. Symbole w Clojure nie mogą być więc nazwane typem referencyjnym.

Użytkowanie symboli

Dzięki specjalnemu traktowaniu symboli przez czytnik możemy stwarzać je, po prostu umieszczając ich nazwy w kodzie. W takiej formie (zwanej formułą symbolową) będą reprezentowały wartości, z którymi wcześniej je skojarzono. Jesteśmy również w stanie posługiwać się symbolami tak, jakby same były wartościami, korzystając z cytowania lub odpowiednich funkcji.

Formuły symbolowe

Formuły symbolowe powstają, gdy w odpowiednich miejscach kodu źródłowego pojawiają się niezacytowane napisy, które spełniają warunki potrzebne, aby uznać je za nazwy symboli.

Użycie:

  • symbol,
  • przestrzeń-nazw/symbol.

Dokładniej rzecz ujmując, formuły symbolowe mogą być użyte do identyfikowania:

Gdy tekst kodu źródłowego jest analizowany składniowo przez czytnik, symbole są wykrywane na podstawie warunków, które muszą spełniać reprezentujące je napisy. Zaraz po tym są tworzone ich pamięciowe reprezentacje w postaci obiektów typu clojure.lang.Symbol – powstają symbole o odpowiadających im nazwach. To tej postaci będzie dalej używał ewaluator, aby odnaleźć wskazywane symbolami wartości. Algorytm jest następujący:

  • Jeżeli symbol ma dookreśloną przestrzeń nazw, to następuje próba poznania wartości, na którą wskazuje zmienna globalna powiązana z symbolem o takiej samej nazwie w podanej przestrzeni.

  • Jeżeli symbol zawiera określenie pakietu Javy, następuje próba odwołania się do klasy o nazwie takiej, jak nazwa symbolu.

  • Jeżeli nazwa symbolu jest taka sama, jak nazwa formuły specjalnej, to zwracany jest jej obiekt.

  • Jeżeli w bieżącej przestrzeni nazw istnieje przyporządkowanie symbolu do klasy Javy, zwracany jest obiekt tej klasy.

  • Jeżeli mamy do czynienia z zasięgiem leksykalnym (w ciele funkcji lub w konstrukcji let lub podobnej), przeszukiwany jest stos w celu znalezienia tam odwzorowania symbolicznej nazwy na obiekt umieszczony na stercie.

  • W końcu dokonywane jest przeszukanie bieżącej przestrzeni nazw w celu znalezienia tam przyporządkowania symbolu o takiej samej nazwie do zmiennej globalnej i poznania aktualnej wartości wskazywanej przez tą zmienną.

Jeżeli po wykonaniu powyższych czynności nadal nie zostanie znalezione powiązanie żadnego pamięciowego obiektu z symbolem o nazwie tożsamej z nazwą podanego, zgłoszony zostanie wyjątek java.lang.RuntimeException z komunikatem Unable to resolve symbol.

Formuły stałe symboli

Symbole w formie stałej (formuły stałe symboli) są wartościami własnymi. Możemy z nich korzystać do reprezentowania dowolnych danych mających znaczenie w kontekście logiki przyjętej w aplikacji: nazywanie elementów konfiguracji, sterowanie przepływem, gromadzenie danych itd.

Użycie:

  • 'symbol,
  • (quote symbol),
  • (symbol nazwa-symbolu),
  • (symbol nazwa-przestrzeni nazwa-symbolu).

Istnieją dwa sposoby tworzenia form stałych symboli: cytowanieużycie funkcji symbol.

Cytowanie polega na użyciu formuły specjalnej quote lub skorzystaniu z apostrofu poprzedzającego nazwę symbolu, która jest makrem czytnika rozwijanym do wywołania tej formuły. Pojedynczy, zacytowany z użyciem quote symbol w fazie analizy syntaktycznej stanie się liściem abstrakcyjnego drzewa składniowego, który zostanie oznaczony jako nieprzeznaczony do wartościowania. Gdy ewaluator będzie rekurencyjnie obliczał wartości wyrażeń drzewa, to dla takiego fragmentu zwróci po prostu obiekt symbolu, bez prób odnajdywania obiektów, które mogłyby być tym symbolem identyfikowane.

Funkcja symbol pozwala stwarzać symbole wyrażone podanymi nazwami. Przyjmuje ona jeden obowiązkowy argument, którym powinna być nazwa symbolu wyrażona łańcuchem znakowym. Opcjonalnie możemy też przekazać jej jako pierwszy argument wyrażoną w ten sam sposób nazwę przestrzeni nazw, jeżeli chcemy utworzyć symbol z dookreśloną przestrzenią. W dwuargumentowej wersji nazwę symbolu musimy wtedy podać jako drugi argument. Zwracaną wartością jest obiekt symbolu, z którego możemy w programie korzystać tak, jak z każdej innej wartości.

Efektywnie skorzystanie z funkcji symbol i formuły specjalnej quote pozwoli nam uzyskać obiekt symbolu. Różnica między nimi polega na fazie przetwarzania programu, w której dojdzie do wytworzenia tego obiektu. W przypadku cytowania obiekt powstanie już w momencie analizy składniowej, a w przypadku użycia symbol w chwili powrotu z funkcji.

Przykłady tworzenia symboli w formach stałych
1
2
3
4
(symbol        "abc")  ; tworzenie symbolu abc
(symbol "user" "abc")  ; tworzenie symbolu abc z przestrzenią user
(quote           abc)  ; cytowanie stwarza symbole, jeśli nazwa jest odpowiednia
'abc                   ; lukier składniowy dla quote

Pamiętajmy, że dookreślenie przestrzeni nazw nie umieszcza symbolu w żadnej przestrzeni, lecz wpisuje weń po prostu odpowiednią informację, z której mogą korzystać potem mechanizmy czyniące użytek z przestrzeni.

Korzystanie z symboli w formach stałych nie różni się od używania innych wartości:

Przykłady użycia formy stałej symboli
1
2
3
(list 'chleb 'mleko 'ser)  ; lista symboli
(quote (chleb mleko ser))  ; zacytowana lista symboli
'(chleb mleko ser)         ; zacytowana lista symboli

Warto zaobserwować, że quote i pojedynczy apostrof nie służą tylko do tworzenia stałych form symboli. Cytowanie wyłącza wartościowanie dowolnych konstrukcji, które normalnie byłyby obliczane. W przypadku S-wyrażeń wieloelementowych (np. listowych czy wektorowych) oznacza to, że nie będą rekurencyjnie obliczane wartości ich poszczególnych elementów (którymi mogą być również symbole).

Przykłady cytowania
1
2
3
4
5
'2        ; wyraża literał liczbowy 2
'[1 2 3]  ; wyraża wektor [1 2 3]
'()       ; wyraża pustą listę
'(1 2 3)  ; wyraża niepustą listę
'(a b c)  ; wyraża listę symboli (a b c)

Formuły powiązaniowe symboli

Niezacytowane symbole znajdują również zastosowanie w wyrażaniu formuł powiązaniowych, tzn. podczas tworzenia powiązań (np. leksykalnych czy w parametrach funkcji). Mówimy wtedy o wyrażeniach powiązaniowych, do których zaliczamy:

Formuły powiązaniowe znajdziemy również w definicjach zmiennych globalnych oraz funkcji.

Przykłady formuł powiązaniowych symboli
1
2
3
4
5
6
7
8
9
10
11
12
13
;; definicje zmiennych globalnych i funkcji:
(def  a 5)                  ; nazwanie zmiennej globalnej powiązanej z wartością
(def  a (fn [] (+ 2 2)))    ; nazwanie zmiennej globalnej powiązanej z funkcją
(defn a     [] (+ 2 2))     ; nazwanie zmiennej globalnej powiązanej z funkcją

;; wektory parametryczne:
(fn     [a b] (list a b))   ; nazwanie argumentów funkcji anonimowej
(defn f [a b] (list a b))   ; nazwanie argumentów funkcji nazwanej

;; wektory powiązaniowe:
(let             [a 5]  a)  ; nazwanie powiązania leksykalnego
(binding         [a 5]  a)  ; nazwanie powiązania dynamicznego
(with-local-vars [a 5] @a)  ; nazwanie zmiennej lokalnej

Tworzenie unikatowych symboli, gensym

Czasem zachodzi konieczność stworzenia symbolu, który będzie globalnie unikatowy, tzn. jego nazwa będzie niepowtarzalna. Służy do tego funkcja gensym.

Użycie:

  • (gensym przedrostek?).

Funkcja gensym przyjmuje jeden opcjonalny argument, który powinien być łańcuchem znakowym, a zwraca symbol. Jeśli łańcucha nie podano, to jest generowany symbol o losowej nazwie z przedrostkiem G__. Gdy podano argument, to jest on używany jako przedrostek nazwy.

Przykłady użycia funkcji gensym
1
2
3
4
5
6
7
;; całkowicie unikatowa nazwa
(gensym)
; => G__2862

;; unikatowa nazwa z podanym przedrostkiem
(gensym "siefca")
; => siefca2865

Testowanie typu, symbol?

Możemy sprawdzić, czy dany obiekt na pewno jest symbolem (formułą stałą symbolu), korzystając z predykatu symbol?.

Użycie:

  • (symbol? wartość).

Funkcja przyjmuje jeden argument, a zwraca wartość true (jeżeli podana wartość jest symbolem) lub false (jeżeli podana wartość nie jest symbolem).

Przykład użycia funkcji symbol?
1
2
3
4
5
(symbol? 'test) ; czy test jest symbolem?
; => true       ; tak, jest

(symbol? test)  ; czy obiekt identyfikowany symbolem test też jest symbolem?
; => false      ; nie, nie jest

Symbole jako funkcje

Symbole w formach stałych mogą być używane jako funkcje. Przyjmują wtedy jeden obowiązkowy argument, którym powinna być mapa lub zbiór.

Użycie:

  • (symbol kolekcja wartość-domyślna?).

W podanej strukturze zostanie przeprowadzone wyszukanie elementu, którego kluczem jest podany symbol, a jeśli nie zostanie on odnaleziony, zwrócona będzie wartość nil lub wartość podana jako drugi, opcjonalny argument.

W przypadku znalezienia elementu w mapie funkcja symbolowa zwraca wartość skojarzoną z tym symbolem, a w przypadku znalezienia elementu w zbiorze jego wartość.

Przykłady użycia symbolu jako funkcji
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
('a   {'a 1, 'b 2} "brak")  ; => 1
('a   {'x 1, 'b 2} "brak")  ; => "brak"
('a   {'x 1, 'b 2})         ; => nil

('a  '{a 1,   b 2} "brak")  ; => 1
('a  '{x 1,   b 2} "brak")  ; => "brak"
('a  '{x 1,   b 2})         ; => nil

((quote a) '{a 1} "brak")   ; => 1
((quote a) '{x 1} "brak")   ; => "brak"
((quote a) '{x 1})          ; => nil

('a  #{'a 'b} "brak")       ; => a
('a  #{'x 'b} "brak")       ; => "brak"
('a  #{'x 'b})              ; => nil

('a '#{a  b} "brak")        ; => a
('a '#{x  b} "brak")        ; => "brak"
('a '#{x  b})               ; => nil

Metadane

Symbole (ale również kolekcje) mogą być opcjonalnie wyposażone w metadane (ang. metadata). Są to informacje pozwalające dokonywać pewnych adnotacji, czyli kojarzyć z obiektami dodatkowe, pomocnicze wartości, które mogą być potem wykorzystane do sterowania zachowaniem programu.

Niektóre metadane są rozpoznawane i użytkowane przez wbudowane mechanizmy języka. Przykładem ich wykorzystania w identyfikowanych nimi i tworzonych z ich pomocą zmiennych globalnych, są między innymi:

Metadanych o samodzielnie nazwanych kluczach, które nie kolidują z wbudowanymi, programista może używać do własnych celów.

Metadane przechowywane są w mapach, a reprezentowane w postaci par klucz–wartość. Kluczami mogą być dowolne obiekty, lecz na zasadzie konwencji stosuje się najczęściej słowa kluczowe.

Metadane nie są składnikami wartości obiektów, do których je dołączono. Porównując dwa tożsame pod względem wartości obiekty, które mają różne metadane, uzyskamy logiczną prawdę.

Podczas tworzenia tzw. zmiennych globalnych, które identyfikowane są symbolami, metadane umieszczone w tych ostatnich są kopiowane do obiektów typu Var.

Odczytywanie metadanych, meta

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

Użycie:

  • (meta symbol).

Funkcja meta przyjmuje symbol, a zwraca mapę metadanych, jeśli symbolowi je przypisano lub nil w przeciwnym razie.

Przykłady użycia funkcji meta
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
;; tworzenie zmiennej globalnej (referencji do wartości 5)
(def x 5)

;; brak metadanych dla wartości 5 (wskazywanej symbolem)
(meta x)
; => nil

;; brak metadanych dla symbolu x
(meta 'x)
; => nil

;; są metadane dla zmiennej globalnej
(meta #'x)
; => { :ns #<Namespace user>,
; =>   :name x, :file "NO_SOURCE_PATH",
; =>   :column 1,
; =>   :line 1 }

;; wyrażenie metadanowe, są metadane
(meta '^:testowa y)
; => {:testowa true}

Dodawanie metadanych, with-meta

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

Użycie:

  • (with-meta symbol metadane).

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

Przykład użycia funkcji with-meta
1
2
(with-meta 'nazwa {:klucz "wartość"})
; => nazwa

Warto mieć na względzie, że tak ustawione metadane będą obecne wyłącznie w symbolu zwracanym przez to konkretne wyrażenie i nie zostaną dołączone do symbolu, na którym operujemy, ponieważ podobnie jak inne wartości jest on niemutowalny.

Istotną cechą obsługi symboli opatrzonych metadanymi jest to, że niektóre identyfikowane nimi obiekty kopiują je podczas tworzenia powiązania. Przykładem takiego zachowania są wspomniane zmienne globalne.

Aby nie popaść w zakłopotanie, należy pamiętać o rozróżnieniu metadanych symboli identyfikujących obiekty od metadanych tych obiektów, a nawet od metadanych obiektów wskazywanych przez obiekty (w przypadku typów referencyjnych, które będą dokładniej omówione w dalszych rozdziałach).

Całkiem możliwą i powszechną sytuacją jest, że symbol nie jest wyposażony w metadane, ale już identyfikowany nim obiekt ma je przypisane.

Wyrażenia metadanowe

W Clojure istnieje również makro czytnika, które wywołuje with-meta na wartości umieszczonej po jego prawej stronie.

Użycie:

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

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

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

Przykłady korzystania z makra czytnika ustawiającego metadane
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
(def ^:testowa x 5)  ; tworzenie zmiennej globalnej (referencji do wartości 5)
                     ; z ustawioną w symbolu metadaną wyrażającą ustawioną flagę
; => #'user/x

(meta #'x)
; => { :ns #<Namespace user>,
; =>   :name x, :file "NO_SOURCE_PATH",
; =>   :column 1,
; =>   :line 1,
; =>   :testowa true}

(def ^{ :testowa "napis", :druga 123 } x 5)
; => #'user/x

(meta #'x)
; => { :ns #<Namespace user>,
; =>   :name x, :file "NO_SOURCE_PATH",
; =>   :column 1,
; =>   :line 1,
; =>   :testowa "napis",
; =>   :druga 123 }

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

Zobacz także:

Klucze

Klucze (ang. keys) lub słowa kluczowe (ang. keywords) to w Clojure typ danych przypominający symbole. Podobnie jak one, klucze służą do identyfikowania innych danych, jednak jeśli nie zostały umieszczone na pierwszej pozycji listowego S-wyrażenia są formułą stałą (wyrażają wartości własne). Klucze nie są w sposób specjalny traktowane przez czytnik i nie identyfikują innych danych w sposób przezroczysty.

Słów kluczowych często używa się do etykietowania pewnych opcji lub flag, a także jako kluczy w asocjacyjnych strukturach danych. Dwa klucze o takiej samej nazwie są tożsame.

Internalizacja kluczy

W przeciwieństwie do symboli klucze są internalizowane, tzn. dwa tak samo nazwane klucze będą wewnętrznie reprezentowane przez ten sam obiekt pamięciowy.

Przykład dowodzący internalizowania kluczy
1
2
3
4
5
(identical? :a :a)      ; czy te dwa klucze są tym samym obiektem?
; => true               ; tak są

(= :a :a)               ; czy te dwa klucze są równe?
; => true               ; tak, są

Powyższej zademonstrowana cecha pozwala nam używać kluczy na przykład jako indeksów w strukturach asocjacyjnych. Po pierwsze mamy pewność odnośnie testów porównywania, a po drugie wiemy, że w pamięci nie powstanie zbyt wiele obiektów zawierających tekstowy identyfikator – będziemy mieli do czynienia z automatyczną kompresją słownikową dla takich samych kluczy.

Przestrzenie nazw kluczy

Słowa kluczowe mogą opcjonalnie zawierać informacje o przypisaniu do konkretnej przestrzeni nazw. Możemy wtedy mówić o słowach kluczowych z dookreśloną przestrzenią nazw (ang. namespace-qualified keywords).

Z przestrzeni nazw warto korzystać wtedy, gdy pisany przez nas kod może być użytkowany przez innych, a tworzenie kluczy mogłoby zaburzyć pracę ich programów. Jeśli na przykład sprawdzane jest samo istnienie obiektu klucza (stworzonego przez przynajmniej jednokrotne jego użycie) i zależy od tego logika działania programu, to tworząc bibliotekę, która intensywnie korzysta z kluczy, a której użyje autor takiej aplikacji, potencjalnie moglibyśmy zaburzyć jej poprawną pracę (abstrahując od tego, że nie powinno się polegać na tym, czy klucz został już użyty).

Użytkowanie kluczy

Tworzenie kluczy, keyword

Słowa kluczowe możemy tworzyć, korzystając z funkcji keyword.

Użycie:

  • (keyword przestrzeń? klucz).

W wariancie jednoargumentowym funkcja przyjmuje łańcuch znakowy określający nazwę klucza, a w wariancie dwuargumentowym dwa łańcuchy znakowe: nazwę przestrzeni nazw i nazwę klucza.

Funkcja zwraca obiekt słowa kluczowego, który jest internalizowany (jeśli nie istniał, jest tworzony, a jeśli już istniał, zwracana jest jego instancja).

Przykład użycia funkcji keyword
1
2
(keyword "klucz")           ; => :klucz
(keyword "przestrzeń" "a")  ; => :przestrzeń/a

Wyrażenia kluczowe

Tworzyć słowa kluczowe możemy z wykorzystaniem kluczowych S-wyrażeń (ang. keyword S-expressions) w postaci zapisu z wiodącym dwukropkiem.

Użycie:

  • :klucz,
  • :przestrzeń/klucz,
  • ::klucz.

Przed nazwą klucza możemy umieścić nazwę przestrzeni nazw oddzieloną znakiem ukośnika. Możemy też przed nazwą umieścić dwa dwukropki i w ten sposób stworzyć klucz z dookreśloną przestrzenią nazw, którą jest przestrzeń bieżąca.

Przykłady wyrażeń kluczowych
1
2
3
:klucz             ; => :klucz
:przestrzeń/klucz  ; => :przestrzeń/klucz
::klucz            ; => :user/klucz

Sprawdzane typu, keyword?

Sprawdzanie czy obiekt jest kluczem jest możliwe z wykorzystaniem z predykatu keyword?.

Użycie:

  • (keyword? wartość).

Jeżeli podana wartość jest słowem kluczowym, zwrócona będzie wartość true, a w przeciwnym razie false.

Przykład użycia funkcji keyword?
1
2
(keyword? "klucz")  ; => false  (nie jest)
(keyword? :klucz)   ; => true   (jest)

Wyszukiwanie kluczy, find-keyword

Możemy sprawdzić, czy dane słowo kluczowe zostało internalizowane, posługując się funkcją find-keyword.

Użycie:

  • (find-keyword klucz).

Funkcja pozwala odszukać klucz, który wcześniej utworzono, np. przez odwołanie się do niego.

Przykład użycia funkcji find-keyword
1
2
3
4
5
6
(find-keyword "słowo")  ; istnieje?
; => nil                ; nie

:słowo                  ; pierwsze użycie internalizuje klucz
(find-keyword "słowo")  ; istnieje?
; => :słowo             ; tak

Można również dokonywać wyszukiwania kluczy z dookreślonymi przestrzeniami nazw:

Przykłady użycia find-keyword z przestrzeniami nazw
1
2
3
4
5
6
(find-keyword "przestrzeń" "słowo")  ; istnieje?
; => nil                             ; nie

:przestrzeń/słowo                    ; pierwsze użycie internalizuje klucz
(find-keyword "przestrzeń" "słowo")  ; istnieje?
; => :słowo                          ; tak

Klucze jako funkcje

Słowa kluczowe mogą być używane jako funkcje. Przyjmują wtedy jeden obowiązkowy argument, którym powinna być mapa lub zbiór.

Użycie:

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

W podanej strukturze zostanie przeprowadzone wyszukanie elementu, którego kluczem jest podane słowo kluczowe, a jeśli nie zostanie on odnaleziony, zwrócona będzie wartość nil lub wartość podana jako drugi, opcjonalny argument.

W przypadku znalezienia elementu w mapie funkcja kluczowa zwraca wartość skojarzoną z tym kluczem, a w przypadku znalezienia elementu w zbiorze jego wartość.

Przykład użycia klucza jako funkcji
1
2
3
4
5
6
(:a  {:a 1, :b 2} "brak")  ; => 1
(:a  {:x 1, :b 2} "brak")  ; => "brak"
(:a  {:x 1, :b 2})         ; => nil
(:a #{:a :b} "brak")       ; => :a
(:a #{:x :b} "brak")       ; => "brak"
(:a #{:x :b})              ; => nil

Wartości logiczne

Obsługa wartości logiki dwuwartościowej polega na użyciu typu Boolean (java.lang.Boolean), którego obiekty mogą wyrażać wartości true lub false. Te dwa symbole w formach symbolowych wartościowane są do obiektów typu Boolean, oznaczających odpowiednio logiczną prawdę i logiczny fałsz.

Obiekty typu Boolean, mimo że wyrażają tylko dwa stany, zajmują w pamięci 8 bitów.

Użytkowanie wartości logicznych

Wartości logiczne są na zasadzie konwencji zwracane przez funkcje, których nazwy zakończone są pytajnikiem. Funkcje takie nazywamy predykatami (ang. predicates).

Poza tym istnieją funkcje specyficzne dla wartości logicznych, które pozwalają tworzyć je, rzutować i sprawdzać.

Wykonywanie warunkowe

W języku Clojure formuły specjalne odpowiedzialne za warunkowe wykonywanie obliczeń (np. if) traktują logiczny fałsz (wyrażony atomem false) i wartość pustą (wyrażoną atomem nil) tak samo. Wewnętrznie dokonują one rzutowania danych różnych typów do wartości logicznych (podobnie jak opisana niżej funkcja boolean).

Przykłady rzutowania typów w konstrukcjach warunkowych
1
2
3
4
(if true  'tak 'nie)  ; => tak
(if false 'tak 'nie)  ; => nie
(if nil   'tak 'nie)  ; => nie
(if 0     'tak 'nie)  ; => tak

Tworzenie wartości logicznych, boolean

Wartości logiczne można tworzyć nie tylko przez umieszczanie literalnie wyrażonych wartości true lub false, ale także z wykorzystaniem funkcji boolean.

Użycie:

  • (boolean wartość).

Funkcja przyjmuje wartość dowolnego typu i dokonuje rzutowania (ang. casting) do true lub false. Zasada jest taka, że zwracaną wartością jest true, chyba że jako argument podano nil lub false.

Przykład użycia funkcji boolean
1
2
3
4
(boolean   nil)  ; => false
(boolean false)  ; => false
(boolean  true)  ; => true 
(boolean   123)  ; => true

Badanie wartości logicznych, true? i false?

Funkcje true?false? są predykatami, które sprawdzają czy podane jako argument wartości wyrażają logiczną prawdę lub logiczny fałsz.

Użycie:

  • (true? wartość),
  • (false? wartość).

Funkcja true? zwróci true, gdy podana wartość jest równa true, a funkcja false? zwróci true, gdy podana wartość jest równa false.

Przykłady użycia funkcji true? i false?
1
2
3
4
5
6
(true?   true)  ; => true
(true?  false)  ; => false
(true?    nil)  ; => false
(true?      5)  ; => false
(false?   nil)  ; => false
(false? false)  ; => true

Testowanie wartościowości, some?

Predykat some? (dodany w Clojure 1.6) sprawdza, czy podany argument jest wartościowy (tzn. czy nie jest wartością nil).

Użycie:

  • (some? wartość),

Jeżeli podana wartość nie jest wartością nil, zwracana jest wartość true, a w przeciwnym razie false.

Przykłady użycia funkcji some?
1
2
3
4
5
(some?   nil)  ; => false
(some?  true)  ; => true
(some? false)  ; => true
(some?     1)  ; => true
(some?    ())  ; => true

Testowanie bezwartościowości, nil?

Predykat nil? działa przeciwnie do some? i pozwala sprawdzać, czy podana wartość jest wartością nil.

Użycie:

  • (nil? wartość).

Funkcja przyjmuje jeden argument i zwraca true, gdy jego wartość jest równa nil.

Przykłady użycia funkcji nil?
1
2
3
4
5
(nil?   nil)  ; => true
(nil?  true)  ; => false
(nil? false)  ; => false
(nil?     1)  ; => false
(nil?    ())  ; => false

Odwracanie wartości logicznej, not

Funkcja not pozwala odwrócić wartość logiczną.

Użycie:

  • (not wartość).

Funkcja przyjmuje jeden argument i zwraca wartość true, jeżeli wartością tego argumentu jest false lub nil. W przeciwnych przypadkach zwraca false.

Przykłady użycia funkcji not
1
2
3
4
5
(not   nil)  ; => true
(not false)  ; => true
(not  true)  ; => false 
(not     0)  ; => false
(not     1)  ; => false

Iloczyn logiczny, and

Makro and służy do sterowania wykonywaniem się programu, pozwalając na wyrażanie iloczynu logicznego.

Użycie:

  • (and & wyrażenie…).

Makro oblicza wartości kolejnych wyrażeń podanych jako jego argumenty (w porządku występowania) dopóki ich wartością jest logiczna prawda (nie wartość false i nie nil).

Zwracaną wartością jest wartość ostatnio podanego wyrażenia albo false lub nil, jeśli któreś z wyrażeń taką wartość zwróciło i przerwano przetwarzanie. Gdy nie podano żadnych argumentów, zwracana jest wartość true.

Przykłady użycia makra and
1
2
3
4
(and true  false)  ; => false
(and false  true)  ; => false
(and false false)  ; => false
(and true   true)  ; => true

Zobacz także:

  • and – opis w sekcji dot. wyrażeń warunkowych w rozdziale III.

Suma logiczna, or

Makro or służy do sterowania wykonywaniem się programu, pozwalając na wyrażanie sumy logicznej.

Użycie:

  • (or & wyrażenie…).

Makro oblicza wartości kolejnych wyrażeń podanych jako jego argumenty (w kolejności występowania) do momentu, aż wartością któregoś będzie logiczna prawda (nie wartość false i nie nil).

Zwracaną wartością jest wartość ostatnio przetwarzanego wyrażenia. Gdy nie podamy argumentów, makro or zwraca wartość nil.

Przykłady użycia makra or
1
2
3
4
(or true  false)  ; => true
(or false false)  ; => false
(or false  true)  ; => true
(or true   true)  ; => true

Zobacz także:

  • or – opis w sekcji dot. wyrażeń warunkowych w rozdziale III.

Funkcjonały logiczne

W Clojure istnieją wbudowane funkcje wyższego rzędu, które pomagają operować na wartościach logicznych zwracanych przez inne funkcje lub posługują się predykatami podanymi jako logiczne operatory. Zostaną one dokładniej omówione w późniejszych rozdziałach.

Działania na predykatach:

Pozostałe operacje:

Liczby

Wyrażać wartości liczbowe można w Clojure na wiele sposobów, korzystając na przykład z literałów liczbowych, które są atomowymi S-wyrażeniami tworzącymi formuły stałe.

Numeryczne typy danych

Liczby w Clojure obsługiwane są przez wszystkie numeryczne typy danych obecne w Javie, a dodatkowo przez dwa dodatkowe typy, które są specyficzne dla tego języka. Niektóre z typów można wyrażać, posługując się literałami liczbowymi, inne wymagają podania odpowiedniej funkcji, która zwraca egzemplarz klasy odpowiedzialnej za ich obsługę.

  • Byte:

    • klasa: java.lang.Byte,
    • zakres: od -128 do 127,
    • tworzenie: (byte wartość);
  • Short:

    • klasa: java.lang.Short,
    • zakres: od -32 768 do 32 767,
    • tworzenie: (short wartość);
  • Integer:

    • klasa: java.lang.Integer,
    • zakres: od -2 147 483 648 do 2 147 483 647,
    • tworzenie: (int wartość);
  • Ratio:

    • klasa: clojure.lang.Ratio,
    • zakres zależny od dostępnej pamięci,
    • tworzenie: (rationalize wartość.wartość-dziesiętnych) lub (/ dzielna dzielnik),
    • literał: 1/2;
  • Long:

    • klasa: java.lang.Long,
    • zakres: od -263 do 263-1,
    • tworzenie: (long wartość),
    • literały:
      • 8 – zapis w postaci dziesiętnej,
      • 0xfe – zapis w postaci szesnastkowej,
      • 012 – zapis w postaci ósemkowej,
      • 2r1101001 – zapis w postaci dwójkowej,
      • 36rABCDY – zapis w postaci trzydziestoszóstkowej;
  • BigInt:

    • klasa: clojure.lang.BigInt,
    • zakres zależny od dostępnej pamięci,
    • tworzenie: (bigint wartość),
    • literał: 123123123123123123N;
  • BigInteger:

    • klasa: java.math.BigInteger,
    • zakres zależny od dostępnej pamięci,
    • tworzenie: (biginteger wartość);
  • BigDecimal:

    • klasa: java.math.BigDecimal,
    • zakres zależny od dostępnej pamięci,
    • tworzenie: (bigdec wartość),
    • literał: 10000000000000000000M;
  • Float:

    • klasa: java.lang.Float,
    • zakres: od 2-149 do 2128-2104,
    • tworzenie: (float wartość);
  • Double:

    • klasa: java.lang.Double,
    • zakres: od 2-1084 do 21024-2971,
    • tworzenie: (double wartość),
    • literały:
      • 2.0
      • -1.2e-5.

W przypadku typów o takich samych lub podobnych właściwościach, które są zarówno wbudowanymi typami Javy, jak i typami języka Clojure (przestrzeń clojure.lang), warto korzystać z tych drugich z uwagi na optymalizacje wydajnościowe.

Operatory arytmetyczne

Operatory arytmetyczne to funkcje, które pozwalają przeprowadzać podstawowe operacje rachunkowe na typach numerycznych.

Użycie:

  • operatory wieloargumentowe:

    • (+           & składnik…) – suma,
    • (-   odjemna & odjemnik…) – różnica,
    • (*           &  czynnik…) – iloczyn,
    • (/   dzielna & dzielnik…) – iloraz,
    • (min wartość &  wartość…) – minimum,
    • (max wartość &  wartość…) – maksimum;
  • operatory dwuargumentowe:

    • (quot   dzielna dzielnik) – iloraz z dzielenia z resztą,
    • (rem    dzielna dzielnik) – reszta z dzielenia (może być ujemna),
    • (mod    dzielna dzielnik) – reszta z dzielenia (metoda Gaussa, nigdy ujemna);
  • operatory jednoargumentowe:

    • (inc             wartość) – zwiększenie o jeden,
    • (dec             wartość) – zmniejszenie o jeden.

Operatory dla dużych liczb

Niektóre operacje mogą prowadzić do wystąpienia błędu przekroczenia zakresu zmiennej całkowitej (ang. integer overflow). Wynika to z użycia w operacjach arytmetycznych liczb całkowitych, tzn. obiektów typu java.lang.Long. Z przekroczeniem zakresu mamy do czynienia, gdy dana operacja (np. sumowania czy mnożenia) doprowadziłaby do uzyskania wartości większej niż obsługiwana przez ten typ danych. Aby obsługiwać takie przypadki, w Clojure mamy do czynienia z dodatkowymi operatorami, które w razie potrzeby dokonują odpowiedniego rzutowania do wartości wyrażanych typami o szerszych zakresach. Funkcje te różnią się od swych regularnych odpowiedników symbolicznymi nazwami – mają na końcu dodany znak apostrofu.

Użycie:

  • operatory wieloargumentowe dla potencjalnie dużych liczb:

    • (+'          & składnik…) – suma,
    • (*'          &  czynnik…) – iloczyn;
  • operatory jednoargumentowe:

    • (inc'            wartość) – zwiększenie o jeden,
    • (dec'            wartość) – zmniejszenie o jeden.

Operatory bez kontroli przepełnień

Język Clojure na etapie kompilacji dokonuje sprawdzania, czy podczas wykonywania pewnych operacji nie dojdzie do przepełnień (ang. overflows). Istnieją jednak warianty operatorów przeznaczone dla typu całkowitego (Integer), które pomijają te testy.

Użycie:

  • dwuargumentowe:

    • (unchecked-add-int      składnik-1 składnik-2) – suma (możliwe przepełnienie),
    • (unchecked-subtract-int      odjemna odjemnik) – różnica (możliwe przepełnienie),
    • (unchecked-multiply-int   czynnik-1 czynnik-2) – iloczyn (możliwe przepełnienie),
    • (unchecked-divide-int        dzielna dzielnik) – iloraz (możliwe obcięcie),
    • (unchecked-remainder-int     dzielna dzielnik) – reszta z dzielenia (możliwe obcięcie);
  • jednoargumentowe:

    • (unchecked-negate-int                 wartość) – zmiana znaku (możliwe przepełnienie),
    • (unchecked-inc-int                    wartość) – zwiększenie o 1 (możliwe przepełnienie),
    • (unchecked-dec-int                    wartość) – zmniejszenie o 1 (możliwe przepełnienie).

Dynamiczna [zmienna specjalna][zmienne specjalne] o nazwie *unchecked-math* pozwala zmienić zachowanie wszystkich konwencjonalnych operacji arytmetycznych w taki sposób, że testy kontroli przepełnień nie będą przeprowadzane.

Operatory porównania

Użycie:

  • (=       wartość & wartość…) – równe,
  • (==      wartość & wartość…) – równe numerycznie,
  • (not=    wartość & wartość…) – nierówne,
  • (<       wartość & wartość…) – mniejsze,
  • (>       wartość & wartość…) – większe,
  • (<=      wartość & wartość…) – mniejsze lub równe,
  • (>=      wartość & wartość…) – większe lub równe,
  • (compare wartość  wartość  ) – porównuje wartości lub elementy kolekcji.

Zobacz także:

Rzutowanie typów numerycznych

Rzutowanie do typów numerycznych możliwe jest z zastosowaniem funkcji podanych wcześniej, które służą też do tworzenia wartości liczbowych.

Użycie:

  • (byte        wartość),
  • (short       wartość),
  • (int         wartość),
  • (long        wartość),
  • (float       wartość),
  • (double      wartość),
  • (bigdec      wartość),
  • (bigint      wartość),
  • (num         wartość),
  • (rationalize wartość),
  • (biginteger  wartość).

Predykaty typów numerycznych

Używając predykatów, można testować różne cechy wartości numerycznych.

Użycie:

  • (zero?     wartość) – czy wartość jest zerowa,
  • (pos?      wartość) – czy wartość jest dodatnia,
  • (neg?      wartość) – czy wartość jest ujemna,
  • (even?     wartość) – czy wartość jest parzysta,
  • (odd?      wartość) – czy wartość jest nieparzysta,
  • (number?   wartość) – czy wartość jest typem numerycznym,
  • (ratio?    wartość) – czy wartość jest ułamkiem (typ Ratio),
  • (rational? wartość) – czy wartość jest liczbą wymierną,
  • (integer?  wartość) – czy wartość to liczba całkowita (za wyjątkiem BigDec),
  • (decimal?  wartość) – czy wartość to liczba typu BigDec,
  • (float?    wartość) – czy wartość to liczba zmiennoprzecinkowa.

Operatory bitowe

W odniesieniu do danych typu numerycznego możemy dokonywać operacji bitowych.

Użycie:

  • funkcje wieloargumentowe (min. 2):

    • (bit-and         wartość wartość-2 & wartość…) – koniunkcja bitowa,
    • (bit-and-not     wartość wartość-2 & wartość…) – koniunkcja bitowa z negacją,
    • (bit-or          wartość wartość-2 & wartość…) – suma bitowa,
    • (bit-xor         wartość wartość-2 & wartość…) – bitowa różnica symetryczna;
  • funkcje dwuargumentowe:

    • (bit-test        wartość) – odczyt stanu bitu o podanej pozycji,
    • (bit-flip        wartość) – zmiana stanu bitu o podanej pozycji,
    • (bit-set         wartość) – zapalenie bitu o podanej pozycji,
    • (bit-clear       wartość) – zgaszenie bitu o podanej pozycji,
    • (bit-shift-left  wartość) – przesunięcie bitowe w lewo,
    • (bit-shift-right wartość) – przesunięcie bitowe w prawo,
    • (unsigned-bit-shift-right wartość) – przesunięcie bitowe w prawo z wyłączeniem bitu znaku;
  • funkcje jednoargumentowe:

    • (bit-not wartość) – negacja bitowa.

Liczby pseudolosowe

Liczby pseudolosowe to wygenerowane przez system operacyjny wartości numeryczne, które powinny być nieprzewidywalne i cechować się równomiernym rozkładem w czasie (niepowtarzalność).

Generowanie liczb pseudolosowych, rand

Do generowania liczb pseudolosowych służy funkcja rand.

Użycie:

  • (rand     górny-zakres?).

Funkcja przyjmuje jeden opcjonalny argument, wskazujący górną granicę przedziału, z którego ma być pobrany wynik (domyślnie 1, jeśli nie podano argumentu). Przedział ten jest prawostronnie otwarty (nie zawiera wartości podanej jako prawa granica), a jego pierwszym elementem jest 0.

Zwracana przez funkcję wartość jest liczbą zmiennoprzecinkową.

Całkowite liczby pseudolosowe, rand

Funkcja rand-int działa podobnie jak rand, czyli generuje liczbę pseudolosową, ale zwraca liczbę całkowitą.

Użycie:

  • (rand-int górny-zakres).

Funkcja przyjmuje jeden obowiązkowy argument, wskazujący górną granicę przedziału, z którego ma być pobrany wynik. Przedział ten jest prawostronnie otwarty (nie zawiera wartości podanej jako prawa granica), a jego pierwszym elementem jest 0.

Zwracana przez funkcję wartość jest liczbą całkowitą.

Przykłady użycia funkcji rand i rand-int
1
2
3
(rand)         ; => 0.7355816410955994
(rand 2)       ; => 1.0126588862070758
(rand-int 50)  ; => 3

Konfiguracja

Niektóre funkcje i mechanizmy służące do obsługi numerycznych typów danych możemy konfigurować. Służą do tego odpowiednie funkcje i zmienne specjalne.

Testy przepełnień

Podczas etapu kompilowania kodu źródłowego dokonywane są sprawdzenia, czy funkcje sumowania, odejmowania, mnożenia, zwiększania o jeden, zmniejszania o jeden i zaokrąglania wartości nie zwrócą błędu przekroczenia zakresu. Testy te mogą zostać wyłączone przez powiązanie zmiennej specjalnej *unchecked-math* z wartością true.

Przykład powiązania zmiennej specjalnej unchecked-match
1
(alter-var-root #'*unchecked-math* (constantly true))

Uwaga: Niektóre numeryczne typy danych i tak będą korzystały z testów przepełnień, ponieważ to ustawienie odnosi się tylko do sytuacji, gdy wszystkie operandy są typami wbudowanymi. Aby mieć pewność, że testy nie będą przeprowadzane, warto posłużyć się opcjonalnym statycznym typizowaniem przez skorzystanie z mechanizmu sugerowania typów (ang. type hinting).

Określanie dokładności, with-precision

W przypadku danych typu BigDecimal możemy sterować precyzją i trybem zaokrąglania wyników. Służy do tego makro with-precision.

Użycie:

  • (with-precision dokładność wartość),
  • (with-precision dokładność :rounding tryb wartość).

Jego pierwszy argument ustawia liczbę miejsc po przecinku, opcjonalny drugi argument sposób zaokrąglania, a ostatni argument jest wyrażeniem, które ma być przeliczone z użyciem tych ustawień.

Przykład użycia makra with-precision
1
2
(with-precision 5 :rounding CEILING (/ 1M 3))
; => 0.33334M

Możliwe tryby zaokrąglania:

  •     CEILING – do górnego pułapu,
  •       FLOOR – do dolnego pułapu,
  •     HALF_UP – w górę do połówek (tryb domyślny),
  •   HALF_DOWN – w dół do połówek,
  •   HALF_EVEN – do bliższych połówek,
  •          UP – w górę,
  •        DOWN – w dół,
  • UNNECESSARY – niewymagane.

W przypadku ostatniego trybu zgłoszony zostanie wyjątek, gdy w wyniku obliczeń pojawią się liczby po przecinku.

Zobacz także:

Znaki

Pojedyncze znaki są w Clojure reprezentowane przez obiekty klasy java.lang.Character. Są to znaki wielobajtowe i mogą być literami alfabetu Unicode.

Tworzenie znaków

Znaki można tworzyć z użyciem odpowiednich funkcji lub literałów znakowych.

Literały znakowe

Korzystając z symbolicznego zapisu z odwróconym ukośnikiem (ang. backslash), możemy literalnie wyrażać pojedyncze znaki.

Użycie:

  • \znak,
  • \znak-specjalny.
Przykłady literalnego tworzenia znaków
1
2
3
4
5
\d               ; literał znakowy
; => \d

\newline         ; literał znakowy znaku specjalnego (nowa linia)
; => \newline

Znak z kodu, char

Używając funkcji char, możemy tworzyć znak podając jego kod.

Użycie:

  • (char kod-znaku).

Funkcja przyjmuje jeden argument, który powinien być numerycznie wyrażonym kodem znaku, a zwraca znak o podanym kodzie.

Przykład użycia funkcji char
1
2
(char 100)       ; tworzy znak o kodzie 100 (litera d)
; => \d

Znak z łańcucha, get

Dzięki funkcji get jesteśmy w stanie pobrać dowolny znak podanego łańcucha znakowego.

Użycie:

  • (get łańcuch pozycja).

Pierwszym argumentem przekazywanym do funkcji powinien być łańcuch znakowy, a drugim pozycja, pod którą znajduje się znak, który chcemy pobrać (poczynając od 0).

Funkcja zwraca znak lub wartość nil, jeśli nie udało się pobrać znaku (np. z powodu niewłaściwego numeru pozycji).

Przykłady pobierania pojedynczych znaków z łańcuchów
1
2
(get "siefca" 2)
; => \e

Sekwencje znakowe

Warto zauważyć, że łańcuchy znakowe w Clojure można traktować jak sekwencje znaków i używać w stosunku do nich niektórych funkcji przeznaczonych dla sekwencji.

Poniżej znajduje się lista wybranych sekwencyjnych operacji, które prowadzą do uzyskania znaku lub zestawu znaków z łańcucha.

Użycie:

  • (seq                łańcuch) – tworzy sekwencję znaków,
  • (first              łańcuch) – pobiera pierwszy znak,
  • (last               łańcuch) – pobiera ostatni znak,
  • (rest               łańcuch) – pobiera wszystkie znaki poza pierwszym,
  • (nth         łańcuch indeks) – pobiera wskazany znak,
  • (rand-nth           łańcuch) – pobiera losowy znak,
  • (apply      funkcja łańcuch) – podstawia każdy znak jako argument funkcji,
  • (every?    predykat łańcuch) – sprawdza warunek dla każdego znaku,
  • (reverse            łańcuch) – tworzy sekwencję znaków o odwróconej kolejności,
  • (frequencies        łańcuch) – zlicza częstości występowania znaków,
  • (when-first [znak łańcuch …] wyrażenie) – oblicza wyrażenie dla pierwszego znaku.
Przykłady sekwencyjnego dostępu do łańcuchów znakowych
1
2
3
4
5
6
7
8
9
10
11
12
13
(nth           "siefca" 2 )  ; => \e
(rand-nth      "siefca"   )  ; => \f 
(first         "siefca"   )  ; => \s
(last          "siefca"   )  ; => \a
(rest          "siefca"   )  ; => (\i \e \f \c \a)
(apply  vector "siefca"   )  ; => [\s \i \e \f \c \a]
(seq           "siefca"   )  ; => (\s \i \e \f \c \a)
(every? char?  "siefca"   )  ; => true
(reverse       "siefca"   )  ; => (\a \c \f \e \i \s)
(frequencies   "aaabbc"   )  ; => {\a 3, \b 2, \c 1}
(when-first [z "abcdef"] z)  ; => \a

; uwaga: nth dla nieistniejącego indeksu zgłosi wyjątek!

Przekształcanie znaków

Cytowanie specjalnych, char-escape-string

Generowanie sekwencji unikowej dla znaków o specjalnym znaczeniu możliwe jest dzięki funkcji char-escape-string.

Użycie:

  • (char-escape-string znak).

Pierwszym argumentem powinien być znak specjalny wyrażony literalnie lub w inny sposób.

Funkcja zwraca sekwencję unikową dla podanego znaku specjalnego lub nil, jeśli nie istnieje sekwencja unikowa dla podanego znaku.

Przykłady użycia funkcji char-escape-string
1
2
3
4
5
6
7
8
;; brak sekwencji unikowej dla litery c
(char-escape-string \c)          ; => nil

;; sekwencja unikowa dla nowej linii
(char-escape-string \newline)    ; => "\\n"

;; sekwencja unikowa dla backspace'a
(char-escape-string \backspace)  ; => "\\b"

Nazwy znaków specjalnych, char-name-string

Pobieranie nazw dla znaków o specjalnym znaczeniu umożliwia funkcja char-name-string.

Użycie:

  • (char-name-string znak).

Funkcja przyjmuje jeden argument, którym powinien być wyrażony literalnie lub w inny sposób znak specjalny, a zwraca nazwę tego znaku lub nil, jeśli nie podano znaku lub podany znak nie jest znakiem specjalnym.

Przykład użycia funkcji char-name-string
1
2
(char-name-string \a)    ; => nil
(char-name-string \tab)  ; => "tab"

Testy znaków

Testowanie typu, char?

Dzięki predykatowi char? możemy sprawdzić, czy podana wartość jest znakiem.

Użycie:

  • (char? wartość).

Funkcja jako pierwszy argument przyjmuje wartość, a zwraca true, jeżeli jest ona znakiem.

Przykład użycia funkcji char?
1
2
(char? \a)  ; => true
(char?  1)  ; => false

Porównywanie znaków, =

Sprawdzanie czy znaki są takie same można przeprowadzić korzystając z operatora =.

Użycie:

  • (= znak & znak…).

Funkcja przyjmuje jeden obowiązkowy argument, którym powinien być znak i opcjonalne argumenty, którymi mogą być inne znaki.

Wartością zwracaną jest true, gdy wszystkie podane znaki są takie same, a false w przeciwnym razie.

Przykłady użycia operatora porównania na znakach
1
2
3
4
(=    \a \a)  ; => true
(=       \a)  ; => true
(= \a \a \a)  ; => true
(= \a \a \b)  ; => false

Porównywanie znaków, compare

Porównywanie czy podany znak powinien być pod względem kolejności pierwszy, ostatni czy równy drugiemu znakowi (przydatne w sortowaniu) możliwe jest dzięki funkcji compare.

Użycie:

  • (compare znak-1 znak-2).

Funkcja przyjmuje dwa argumenty, a zwraca -1 (lub wartość mniejszą), gdy pierwszy podany argument powinien być umieszczony wcześniej niż drugi, 1 (lub wartość większą) w przypadku przeciwnym i 0, jeśli obydwa znaki mogą mieć tę samą (równą) pozycję. Pod uwagę brana jest pozycja znaków w alfabecie.

Przykład użycia funkcji compare
1
2
(compare \a \b)
; => -1

Zobacz także:

Łańcuchy znakowe

Łańcuchy znakowe w Clojure to obiekty klasy java.lang.String. W języku istnieją odpowiednie funkcje, które pomagają w ich obsłudze, a jeśli jakiejś brak, zawsze można skorzystać z metod Javy.

Łańcuchy znakowe można również traktować jak sekwencje znaków i korzystać z niektórych funkcji operujących na sekwencjach. Więcej o tym sposobie dostępu można przeczytać w sekcji poświęconej sekwencyjnej obsłudze znaków.

Tworzenie łańcuchów

Istnieje kilka sposobów tworzenia łańcuchów znakowych. Można skorzystać z literału tekstowego, użyć funkcji str lub innej funkcji, która na podstawie danych wejściowych zwróci łańcuch.

Łańcuchy z literałów tekstowych

Użycie:

  • "To jest napis",
  • "".
Przykład użycia literału tekstowego
1
2
3
4
5
"To jest napis"
; => "To jest napis"

""
; => ""

Łańcuchy z szeregu wartości, str

Funkcja str przyjmuje zero lub więcej argumentów. Wartość każdego stara się rzutować do łańcucha znakowego, aby następnie dokonać złączenia wszystkich uzyskanych łańcuchów w jeden, który zostanie zwrócony.

Użycie:

  • (str & wartość…).

Funkcja przyjmuje zero lub więcej argumentów o dowolnych wartościach, a zwraca łańcuch tekstowy, który jest złączeniem podanych wartości rzutowanych do łańcuchów tekstowych.

Jeśli nie podano argumentów, funkcja str zwraca łańcuch pusty.

Przykład użycia funkcji str
1
2
3
(str 1 2 3)             ; => "123"
(str "a" "b" "c" \d 4)  ; => "abcd4"
(str)                   ; => ""

Łańcuchy z formatu, format

Funkcja format przyjmuje minimum jeden argument, którym jest łańcuch formatujący zgodny ze składnią używaną przez java.util.Formatter, która odpowiada składni wykorzystywanej w funkcji sprintf, znajdującej się w bibliotece standardowej języka C.

Użycie:

  • (format łańcuch-formatujący & wartość…).

Dla każdej sekwencji sterującej podanej w pierwszym argumencie (łańcuchu formatującym), która wymaga danych wejściowych, należy podać odpowiedni argument wyrażający wartość do podstawienia w odpowiednim miejscu łańcucha formatującego.

Funkcja zwraca przetworzony łańcuch znakowy zbudowany zgodnie z podanym wzorcem formatowania.

Przykład użycia funkcji format
1
2
(format "Witaj, %s!", "Baobabie")
; => "Witaj, Baobabie!"

Łańcuchy ze strumienia, with-out-str

Łańcuchy znakowe można tworzyć na podstawie danych pochodzących ze strumienia wyjściowego (zazwyczaj skojarzonego z deskryptorem standardowego wyjścia). Służy do tego makro with-out-str.

Użycie:

  • (with-out-str & wyrażenie…)

Makro przyjmuje zestaw wyrażeń, które zostaną zrealizowane. Jeżeli któreś z wyrażeń wygeneruje efekt uboczny w postaci zapisu do strumienia standardowego wyjścia, dane te będą przechwycone i umieszczone w zwracanym łańcuchu znakowym.

Przykład użycia makra with-out-str
1
2
(with-out-str (println "Baobab"))  ; standardowe wyjście wyrażenia do łańcucha
; => "Baobab\n"

Łańcuchy z wartości, pr-str

Funkcja pr-str działa podobnie do str i służy do przekształcania podanych wartości do ich symbolicznych reprezentacji (S-wyrażeń). Działa tak, jakby użyć pr, ale rezultat nie jest wyświetlany, lecz zwracany jako łańcuch tekstowy.

  • (pr-str & wartość…)

Funkcja przyjmuje zero lub więcej wartości i każdą z nich rzutuje do łańcucha znakowego.

Wartością zwracaną jest złączenie tekstowych reprezentacji wartości z separatorami w postaci pojedynczego znaku spacji.

Przykład użycia funkcji pr-str
1
2
(pr-str [1 2 3 4] (1))  ; wpisuje w łańcuch reprezentację obiektów
; => "[1 2 3 4] (1)"    ; zwróconą przez funkcję pr

Łańcuchy z wartości, prn-str

Funkcja prn-str działa podobnie do str i służy do przekształcania podanych wartości do ich symbolicznych reprezentacji (S-wyrażeń). Działa tak, jakby użyć prn, ale rezultat nie jest wyświetlany, lecz zwracany jako łańcuch tekstowy.

  • (prn-str & wartość…)

Funkcja przyjmuje zero lub więcej wartości i każdą z nich rzutuje do łańcucha znakowego.

Wartością zwracaną jest złączenie tekstowych reprezentacji wartości z separatorami w postaci pojedynczego znaku spacji. Łańcuch zakończony jest znakiem nowej linii.

Przykład użycia funkcji prn-str
1
2
(prn-str [1 2 3 4] (1))  ; wpisuje w łańcuch reprezentację obiektów
; => "[1 2 3 4] (1)\n"   ; zwróconą przez funkcję prn

Łańcuchy z rezultatu wyświetlania, print-str

Funkcja print-str działa tak jak print, ale zamiast wyświetlać rezultaty zwraca zawierający je łańcuch znakowy.

  • (print-str & wartość…).

Funkcja przyjmuje zero lub więcej wartości i każdą z nich rzutuje do łańcucha znakowego.

Wartością zwracaną jest złączenie tekstowych reprezentacji wartości z separatorami w postaci pojedynczego znaku spacji.

Przykład użycia funkcji print-str
1
2
(print-str "Ba" \o \b 'ab)  ; tworzy łańcuch z efektu wywołania print
; => "Ba o b ab"

Łańcuchy z rezultatu wyświetlania, println-str

Funkcja println-str działa tak jak println, ale zamiast wyświetlać rezultaty zwraca zawierający je łańcuch znakowy.

  • (print-str & wartość…).

Funkcja przyjmuje zero lub więcej wartości i każdą z nich rzutuje do łańcucha znakowego.

Wartością zwracaną jest złączenie tekstowych reprezentacji wartości z separatorami w postaci pojedynczego znaku spacji.

Przykład użycia funkcji println-str
1
2
(println-str "Ba" \o \b 'ab)  ; tworzy łańcuch z efektu wywołania println
; => "Ba o b ab\n"

Porównywanie łańcuchów

Łańcuchy znakowe można porównywać, korzystając z generycznych operatorów.

Użycie:

  • (=       łańcuch & łańcuch…) – równe,
  • (==      łańcuch & łańcuch…) – równe (niezależnie od typu),
  • (compare łańcuch   łańcuch ) – porównuje leksykograficznie dwa łańcuchy.

Zobacz także:

Przeszukiwanie łańcuchów

Metody indexOflastIndexOf

Pobieranie pozycji podanego łańcucha w tekście możliwe jest z użyciem metod Javy indexOf oraz lastIndexOf.

Użycie:

  • (.indexOf     łańcuch fragment) – wyszukuje pozycję pierwszego wystąpienia,
  • (.lastIndexOf łańcuch fragment) – wyszukuje pozycję ostatniego wystąpienia.

Metody przyjmują dwa argumenty: łańcuch tekstowy i poszukiwany fragment tekstu. Wartościami zwracanymi są pozycje (licząc od 0), pod którymi można znaleźć podane łańcuchy.

Przykłady użycia metod indexOf i lastIndexOf
1
2
(.indexOf     "Baobab tu był." "b")  ; => 3
(.lastIndexOf "Baobab tu był." "b")  ; => 10

Zliczanie znaków

Funkcja count

Funkcja count zlicza znaki w łańcuchu (włączając znaki wielobajtowe).

Użycie:

  • (count łańcuch).

Pierwszym przekazywanym argumentem powinien być łańcuch znakowy, a wartością zwracaną jest liczba całkowita znaków w tym łańcuchu.

1
2
(count "Baobab")  ; liczba znaków (nawet wielobajtowych) 
; => 6

Przekształcanie łańcuchów

Zmiana pierwszej litery w wielką, capitalize

Zmiana pierwszej litery w wielką umożliwia funkcja clojure.string/capitalize.

Użycie:

  • (clojure.string/capitalize łańcuch).

Pierwszym i jedynym argumentem powinien być łańcuch tekstowy, a wartością zwracaną jest łańcuch, którego pierwsza litera jest zmieniona w wielką.

Przykład użycia funkcji clojure.string/capitalize
1
2
(clojure.string/capitalize "baobab tu był.")
; => "Baobab tu był."

Zmiana liter w małe, lower-case

Zmiana wszystkich liter w małe możliwe jest z wykorzystaniem funkcji clojure.string/lower-case.

Użycie:

  • (clojure.string/lower-case łańcuch).

Funkcja przyjmuje jeden argument, którym powinien być łańcuch tekstowy, a zwraca nowy łańcuch, w którym przekształcono odpowiednie znaki.

Przykład użycia funkcji clojure.string/lower-case
1
2
(clojure.string/lower-case "BAOBAB")
; => "baobab"

Zmiana liter w wielkie, upper-case

Zmiana wszystkich liter w wielkie możliwa jest z użyciem funkcji clojure.string/upper-case.

Użycie:

  • (clojure.string/upper-case łańcuch).

Funkcja przyjmuje jeden argument, którym powinien być łańcuch tekstowy, a zwraca nowy łańcuch, w którym przekształcono odpowiednie znaki.

Przykład użycia funkcji clojure.string/upper-case
1
2
(clojure.string/upper-case "baobab")
; => "BAOBAB"

Dodawanie sekwencji unikowych, escape

Funkcja clojure.string/escape pozwala na dodawanie sekwencji unikowych (ang. escape sequences) przez zmianę określonych znaków w łańcuchy.

Użycie:

  • (clojure.string/escape łańcuch mapa).

Jako pierwszy argument funkcji należy podać łańcuch znakowy, a jako drugi mapę zawierającą pary, w których kluczami są znaki, a wartościami łańcuchy, na które mają zostać zamienione, gdy zostaną znalezione w tekście.

Przykład użycia funkcji clojure.string/escape
1
2
(clojure.string/escape "echo *" { \* "\\*", \; "\\;" })
; => "echo \\*"

Zmiana kolejności znaków, reverse

Do odwracania kolejności znaków w łańcuchu służy funkcja clojure.string/reverse.

Użycie:

  • (clojure.string/reverse łańcuch).

Przyjmuje ona jako pierwszy argument łańcuch znakowy, a zwraca łańcuch, który jest odwróconą wersją podanego.

Przykład użycia funkcji clojure.string/reverse
1
2
(clojure.string/reverse "nicraM")
; => "Marcin"

Odwrócona sekwencja znaków, reverse

Funkcja reverse z przestrzeni nazw clojure.core działa odmiennie niż clojure.string/reverse, ponieważ zwraca sekwencję znaków o odwróconej kolejności. Sekwencję taką można następnie przekształcić do łańcucha znakowego, np. z użyciem apply czy str.

Użycie:

  • (reverse łańcuch).

Pierwszym argumentem powinien być łańcuch znakowy, a zwracaną wartością będzie sekwencja znaków, której kolejność została odwrócona względem kolejności podanego łańcucha.

Przykład użycia funkcji reverse
1
2
(apply str (reverse "nicraM"))
; => "Marcin"

Przycinanie łańcuchów, trim

Usuwanie białych znaków (w tym znaków nowej linii) z obu stron łańcucha znakowego może być dokonane z wykorzystaniem funkcji clojure.string/trim.

Użycie:

  • (clojure.string/trim łańcuch).

Funkcja przyjmuje jako pierwszy argument łańcuch znakowy, a zwraca jego wersję z usuniętymi białymi znakami i znakami nowej linii (z jego początku i końca).

Przykład użycia funkcji clojure.string/trim
1
2
(clojure.string/trim " Baobab tu był.     ")
; => "Baobab tu był."

Przycinanie z lewej, triml

Usuwanie białych znaków (w tym znaków nowej linii) z lewej strony łańcucha znakowego możliwe jest dzięki funkcji clojure.string/triml.

Użycie:

  • (clojure.string/triml łańcuch).

Funkcja przyjmuje jako pierwszy argument łańcuch znakowy, a zwraca jego wersję z usuniętymi białymi znakami i znakami nowej linii (z jego początku).

Przykład użycia funkcji clojure.string/triml
1
2
(clojure.string/triml " Baobab tu był.     ")
; => "Baobab tu był.    "

Przycinanie z prawej, trimr

Na usuwanie białych znaków (w tym znaków nowej linii) z prawej strony łańcucha znakowego pozwala funkcja clojure.string/trimr.

Użycie:

  • (clojure.string/trimr łańcuch).

Funkcja przyjmuje jako pierwszy argument łańcuch znakowy, a zwraca jego wersję z usuniętymi białymi znakami i znakami nowej linii (z jego końca).

Przykład użycia funkcji clojure.string/trimr
1
2
(clojure.string/trimr " Baobab tu był.     ")
; => " Baobab tu był."

Przycinanie nowej linii, trim-newline

Usuwanie znaków nowej linii z prawej strony łańcucha znakowego możliwe jest z użyciem funkcji clojure.string/trim-newline.

Użycie:

  • (clojure.string/trim-newline łańcuch).

Funkcja przyjmuje jako pierwszy argument łańcuch znakowy, a zwraca jego wersję z usuniętymi znakami nowej linii (z jego końca).

Przykład użycia funkcji clojure.string/trim-newline
1
2
(clojure.string/trim-newline "Baobab tu był.\n")
; => "Baobab tu był."

Wyrażenia regularne

Wyrażenia regularne (ang. regular expressions, skr. regexps) to łańcuchy znakowe, które pozwalają opisywać tekstowe wzorce. Wzorców tych można następnie używać w celu sprawdzenia czy pasują do nich podane łańcuchy znakowe lub ich części, a także budować operatory, które na ich podstawie dokonują zastępowania pewnych fragmentów.

Literały wyrażeń regularnych

W Clojure wyrażenia regularne mogą być tworzone z użyciem odpowiedniej symbolicznej notacji:

  • #"wyrażenie",

gdzie wyrażenie jest łańcuchem znakowym określającym treść wyrażenia zgodnego z formatem argumentu przyjmowanego przez konstruktor klasy java.util.regex.Pattern.

Tworzenie wzorca, re-pattern

Tworzenie wzorca dopasowywania wyrażenia regularnego jest możliwe dzięki funkcji re-pattern. Wygenerowany wzorzec to skompilowana forma podanej, tekstowej reprezentacji wyrażenia, a posługiwanie się jego obiektem wpływa korzystnie na wykorzystanie zasobów procesora. Wzorca można wielokrotnie używać, przekazując go jako argument do funkcji operujących na wyrażeniach regularnych.

Użycie:

  • (re-pattern wzorzec).

Funkcja przyjmuje łańcuch znakowy reprezentujący wzorzec dopasowania wyrażenia regularnego, a zwraca obiekt tego wzorca.

Przykład użycia funkcji re-pattern
1
2
(re-pattern "\\d+") ; tworzenie wzorca wyrażenia regularnego
#"\\d+"             ; lukier składniowy

Warto zauważyć podwójny znak odwróconego ukośnika, który odbiera jego specjalne znaczenie.

Tworzenie regulatora, re-matcher

Tworzenie obiektu dopasowującego (regulatora) pozwala korzystać z niektórych funkcji służących do przetwarzania łańcuchów znakowych z użyciem wzorców dopasowania przedstawionych jako wyrażenia regularne. Dzięki niemu można wielokrotnie korzystać z dopasowania w jego już skompilowanej, wewnętrznej formie. Regulator jest mutowalnym obiektem Javy, który przechowuje wewnętrzne indeksy ulegające zmianie.

Do tworzenia regulatora używa się funkcji re-matcher.

Użycie:

  • (re-matcher wzorzec łańcuch).

Funkcja przyjmuje dwa argumenty. Pierwszym powinien być wzorzec w formie wyrażenia regularnego przedstawionego tekstowo, a drugim badany łańcuch.

Wartością zwracaną jest obiekt dopasowujący wyrażenia regularne.

Przykład użycia funkcji re-matcher
1
(re-matcher #"\\d+" "abcd1234efgh5678")

Wywołanie funkcji z powyższego przykładu utworzy obiekt klasy java.util.regex.Matcher. Pierwszym przekazywanym jej argumentem jest wyrażenie regularne, a drugim dopasowywany do niego łańcuch znakowy. Stworzona wartość może być następnie podana jako argument do niektórych funkcji ekstrahujących wzorce i ich fragmenty, np. re-find.

Uwaga: Należy unikać stosowania funkcji re-matcher, jeśli program ma być bezpieczny wątkowo. Wewnętrzne struktury obiektów typu Matcher mogą ulegać zmianom w nieskoordynowany sposób, generując błędne rezultaty.

Wyszukiwanie dopasowań i grup, re-find

Funkcja re-find odnajduje dopasowania podanego łańcucha znakowego do wzorca. Przyjmuje ona dwa argumenty: wyrażenie regularne i łańcuch znakowy. Można też użyć jej w formie jednoargumentowej – przyjmuje wtedy obiekt regulatora (typu Matcher), a każdorazowe wywołanie zwraca kolejny pasujący fragment.

Wyrażenia regularne mogą składać się z grup, czyli logicznych części, które zawierają wzorce fragmentaryczne, pasujące do pewnych części łańcucha znakowego. Grupy te mogą być zagnieżdżone. W przypadku wyrażenia regularnego z grupami, funkcja re-find zwróci wektor, którego poszczególne elementy będą odpowiadały kolejnym pasującym grupom. Wyjątkiem będzie element o indeksie zero, zawierający cały pasujący łańcuch znakowy. Funkcja re-find wewnętrznie czyni użytek z opisanej niżej funkcji re-groups, aby zwrócić wyniki.

Użycie:

  • (re-find regulator),
  • (re-find wzorzec łańcuch).

W wariancie jednoargumentowym przyjmowanym argumentem jest obiekt dopasowujący wyrażenia regularne. W wariancie dwuargumentowym należy podać obiekt wzorca wyrażenia regularnego i dopasowywany łańcuch znakowy.

Wartością zwracaną jest dopasowany fragment łańcucha znakowego lub wektor zawierający dopasowania.

Przykład użycia funkcji re-find
1
2
3
4
5
6
7
8
9
10
11
(re-find #"\d+" "abc123def456")                 ; zwraca pierwsze dopasowanie
; => "123"

(re-find #"(\d+)-(\d+)-(\d+)" "000-111-222")    ; zwraca pasujące grupy
; => ["000-111-222" "000" "111" "222"]

(def pasuj (re-matcher #"\d+" "abc123def456"))  ; nazywamy obiekt dopasowujący
(repeatedly 3                                   ; powtórz 3 razy
            #(re-find pasuj))                   ; wywołanie re-find na obiekcie

; => ("123" "456" nil)

Uwaga: Korzystanie z re-matcher nie jest bezpieczne wątkowo!

Odczyt pasujących grup, re-groups

Funkcja re-groups pozwala odczytać pasujące grupy wyrażenia regularnego, które było ostatnio używane.

Użycie:

  • (re-groups regulator).

Funkcja przyjmuje tylko jeden argument, którym powinien być obiekt dopasowujący (regulator), a zwraca wektor fragmentów tekstu pasujących do grup.

Przykład użycia funkcji re-groups
1
2
3
4
(def tel (re-matcher #"(\d+)-(\d+)-(\d+)" "000-111-222"))
(re-find tel)
(re-groups tel)
; => ["000-111-222" "000" "111" "222"]

Jeżeli ostatnia próba dopasowania łańcucha znakowego do wyrażenia zakończyła się zwróceniem nil, to próba wywołania funkcji re-groups zgłosi wyjątek.

Uwaga: Korzystanie z re-matcher nie jest bezpieczne wątkowo!

Dopasowywanie do wzorca, re-matches

Funkcja re-matches pozwala sprawdzić czy podany łańcuch znakowy pasuje do wzorca.

Różnica między funkcjami re-findre-matches polega na tym, że ta ostatnia nie szuka dopasowań fragmentarycznych. Podany łańcuch musi pasować do wzorca wyrażenia regularnego w całości.

Użycie:

  • (re-matches wzorzec łańcuch).

Funkcja przyjmuje jeden argument, którym powinien być łańcuch znakowy.

Zwracaną wartością będzie pojedynczy łańcuch znakowy lub wektor łańcuchów, jeśli użyto grup (wewnętrznie funkcja korzysta z re-groups). W razie braku dopasowania zwracana jest wartość nil.

Przykład użycia funkcji re-matches
1
2
(re-matches #"(\d+)-(\d+)-(\d+)" "000-111-222")
; => ["000-111-222" "000" "111" "222"]

Dostęp sekwencyjny, re-seq

Bezpieczne pod względem wątkowym i zgodne ze stylem funkcyjnym jest używanie sekwencji (a dokładniej leniwych sekwencji) do reprezentowania dopasowań łańcuchów znakowych do wzorców wyrażeń regularnych. Służy do tego funkcja re-seq, która wewnętrznie korzysta z metody java.util.regex.Matcher.find(), a następnie używa re-groups, aby wygenerować wynik.

Użycie:

  • (re-seq wzorzec łańcuch).

Funkcja przyjmuje dwa argumenty: wyrażenie regularne i łańcuch znakowy, a zwraca leniwą sekwencję kolejnych fragmentów pasujących do wzorca.

Przykład użycia funkcji re-seq
1
2
(re-seq #"[\p{L}\p{Digit}_]+" "Podzielimy to na słowa")
; => ("Podzielimy" "to" "na" "słowa")

Zamiana fragmentów tekstu, replace

Zamiana fragmentów łańcucha znakowego na inne realizowana jest przez funkcję replace, której pierwszym argumentem powinien być rzeczony łańcuch, drugim wzorzec dopasowania, natomiast trzecim tekst lub znak zastępujący pasujące do wzorca wystąpienia.

Użycie:

  • (clojure.string/replace łańcuch wzorzec zastępnik).

Funkcja przyjmuje łańcuch znakowy, obiekt wzorca dopasowania i łańcuch tekstowy będący wzorcem zastępowania pasujących fragmentów.

Możliwe są następujące specyfikacje wzorców dopasowania i tekstu używanego jako zastępnik:

  • łańcuch znakowy i łańcuch znakowy,
  • pojedynczy znak i pojedynczy znak,
  • wyrażenie regularne i łańcuch znakowy,
  • wyrażenie regularne i funkcja przekształcająca.

W przypadku łańcuchów znakowych i pojedynczych znaków tekst zastępujący nie jest traktowany specjalnie, tzn. nie można w nim korzystać z żadnych interpolowanych wzorców. Inaczej jest, gdy jako wzorzec podamy wyrażenie regularne – można wtedy podać symboliczne znaczniki, które zostaną zastąpione wartościami przechwyconymi podczas analizy tych wyrażeń.

Przykłady użycia funkcji clojure.string/replace
1
2
3
4
5
(clojure.string/replace "Kolor czerwony" "czerwony" "niebieski")
; => "Kolor niebieski"

(clojure.string/replace "Kolor czerwony" #"\b(\w+ )(\w+)" "$1niebieski")
; => "Kolor niebieski"

Zamiana pierwszego fragmentu, replace-first

Funkcja clojure.string/replace-first jest wariantem opisanej wyżej funkcji clojure.string/replace, który działa tylko dla pierwszego napotkanego wystąpienia podanego wzorca.

Użycie:

  • (clojure.string/replace-first łańcuch wzorzec zastępnik).

Przyjmowanymi przez funkcję argumentami są łańcuch znakowym, obiekt wzorca wyrażenia regularnego i wyrażony łańcuchem tekstowym wzorzec zastępujący.

Wartością zwracaną będzie łańcuch tekstowy utworzony na podstawie wzorca zastępującego z zastosowaniem interpolacji dopasowanych fragmentów wyrażenia regularnego.

Przykład użycia funkcji replace-first
1
2
3
4
(clojure.string/replace-first "zmieni miejscami słowa"
                              #"(\w+)(\s+)(\w+)" "$3$2$1")

; => "miejscami zmieni słowa"

Cytowanie zastępników, re-quote-replacement

Gdyby łańcuch znakowy używany jako zastępnik w wywołaniu clojure.string/replace był generowany dynamicznie, konieczne może okazać się dodanie sekwencji unikowych przed wzorcami, które mają znaczenie specjalne. Na przykład chcemy pewien wyraz zastąpić przykładem zawierającym zapis $1, który w przypadku korzystania z wyrażeń regularnych zostałby zastąpiony przez wartość pierwszego pasującego wyrażenia grupowego. Czynność tę można zautomatyzować z wykorzystaniem funkcji re-quote-replacement.

Użycie:

  • (re-quote-replacement łańcuch).

Pierwszym i jedynym przyjmowanym przez funkcję argumentem powinien być łańcuch znakowy. Wartością zwracaną jest łańcuch znakowy, w którym zacytowane zostały wzorce mające specjalne znaczenie w kontekście wyrażeń regularnych.

Przykład użycia funkcji clojure.string/re-quote-replacement
1
2
3
4
5
6
7
8
9
10
11
12
13
;; bez odbierania specjalnego znaczenia
(clojure.string/replace "Kolor czerwony"
                        #"(\w+) ([Cc]zerwony)"
                        "$1 określony symbolem $2")
; => "Kolor określony symbolem czerwony"

;; z odbieraniem specjalnego znaczenia
(clojure.string/replace "Kolor czerwony"
                        #"(\w+) ([Cc]zerwony)"
                        (str "$1 określony symbolem"
                             (clojure.string/re-quote-replacement
                              " $1")))
; => "Kolor określony symbolem $1"

Łączenie i dzielenie łańcuchów

Wydzielanie fragmentów, subs

Wydzielanie łańcucha o wskazanym położeniu początkowym i (opcjonalnie) końcowym możliwe jest z użyciem funkcji subs.

Użycie:

  • (subs łańcuch początek koniec?)

Jako pierwszy argument funkcja przyjmuje łańcuch znakowy, a jako kolejny numer pozycji (licząc od 0), od której należy rozpocząć wydzielanie fragmentu. Opcjonalny trzeci argument może wyrażać pozycję końcową.

Funkcja zwraca łańcuch znakowy, a w razie przekroczenia zakresów lub podania błędnych zakresów pozycji generowany jest wyjątek.

Przykład użycia funkcji subs
1
2
3
(subs "Baobab"   3)  ; => "bab"
(subs "Baobab" 2 5)  ; => "oba"
(subs "Baobab" 2 2)  ; => ""

Łączenie łańcuchów, str

Łańcuchy znakowe mogą być łączone z użyciem funkcji str.

Użycie:

  • (str & tekst…).

Argumentami funkcji str mogą być łańcuchy znakowe, a zwracaną wartością będzie efekt ich złączenia.

Przykład użycia funkcji str do łączenia łańcuchów znakowych
1
2
(str "Pierwszy " "Drugi" " Trzeci")
; => "Pierwszy Drugi Trzeci"

Korzystając z funkcji str, i traktując łańcuchy znakowe jak sekwencje znaków, możemy również dokonywać złączeń z zastosowaniem wybranego łącznika (tekstu lub pojedynczego znaku):

Przykład podejścia sekwencyjnego w łączeniu łańcuchów tekstowych łącznikiem
1
2
3
4
5
(apply str (interpose "--" ["raz" "dwa" "trzy"]))
; => "raz--dwa--trzy"

(apply str (interpose \- ["raz" "dwa" "trzy"]))
; => "raz-dwa-trzy"

Łączenie łańcuchów łącznikiem, join

Łączenie łańcuchów w jeden łańcuch z opcjonalnym użyciem podanego łącznika w postaci łańcucha znakowego można uzyskać również z wykorzystaniem funkcji clojure.string/join.

Użycie:

  • (clojure.string/join sekwencja),
  • (clojure.string/join łącznik sekwencja).

Aby złączyć łańcuchy znakowe należy umieścić je w kolekcji o sekwencyjnym interfejsie dostępu lub po prostu wyrazić sekwencyjnie.

W wariancie jednoargumentowym należy przekazać sekwencję łańcuchów znakowych. W wariancie dwuargumentowym sekwencję przekazujemy jako drugi argument, a jako pierwszy łącznik, czyli element, który będzie użyty do złączenia elementów (może to być np. spacja wyrażona łańcuchem znakowym lub pojedynczym znakiem).

Przykłady użycia funkcji clojure.string/join
1
2
3
4
5
6
7
8
9
10
11
12
13
14
(clojure.string/join " " ["raz" "dwa"])
; => "raz dwa"

(clojure.string/join "" ["Bao" "bab"])
; => "Baobab"

(clojure.string/join \space ["Bao" "bab"])
; => "Bao bab"

(clojure.string/join (seq '(B a o b a b)))
; => "Baobab"

(clojure.string/join ", " [1 2 3 4])
; => "1, 2, 3, 4"

Dzielenie łańcuchów, split

Dzielenie łańcucha znakowego na części możliwe jest z użyciem wyrażenia regularnego przekazywanego jako drugi argument funkcji clojure.string/split.

Użycie:

  • (clojure.string/split łańcuch wzorzec & limit).

Pierwszym argumentem funkcji powinien być poddawany podziałowi łańcuch, drugim wzorzec wyrażenia regularnego, a opcjonalnym trzecim limit, czyli maksymalna liczba elementów, które zostaną wydzielone.

Funkcja zwraca wektor, którego kolejne elementy są wydzielonymi fragmentami.

Przykład użycia funkcji clojure.string/split
1
2
3
4
5
6
7
8
(clojure.string/split "Baobab tu był." #" ")
; => ["Baobab" "tu" "był."]

(clojure.string/split "B123a09o2b1a55b322 1t4u 90b8y42ł3." #"\d+")
; => ["B" "a" "o" "b" "a" "b" " " "t" "u " "b" "y" "ł" "."]

(clojure.string/split "B123a09o2b1a55b322 1t4u 90b8y42ł3." #"\d+" 7)
; => ["B" "a" "o" "b" "a" "b" " 1t4u 90b8y42ł3."]

Dzielenie na nowych liniach, split-lines

Dzielenie łańcucha znakowego na części w miejscach występowania znaków nowej linii można zrealizować z wykorzystaniem funkcji clojure.string/split-lines.

Użycie:

  • (clojure.string/split-lines łańcuch).

Funkcja przyjmuje jeden argument (łańcuch znakowy), a zwraca wektor, którego elementami są kolejne fragmenty, czyli linie tekstu źródłowego.

Przykład użycia funkcji clojure.string/split-lines
1
2
(clojure.string/split-lines "Baobab\ntu\nbył.")
; => [ "Baobab" "tu" "był." ]

Dzielenie łańcuchów, re-seq

Dzielenie łańcuchów znakowych na mniejsze części można również osiągnąć w sposób sekwencyjny, wykorzystując funkcję re-seq, lub traktując łańcuch jako sekwencję znaków.

Przykład podejścia sekwencyjnego w dzieleniu łańcuchów znakowych
1
2
3
4
5
6
7
8
9
10
11
12
13
(def tekst "\n\nDzielenie na linie\nDruga")

;; sekwencja na podstawie tekstu i wyrażenia regularnego
(re-seq #"(?s)[^\n]+" tekst)
; => ("Dzielenie na linie" "Druga")

;; sekwencyjny podział na znaki i dzielenie na linie
(->> tekst                                 ; dla tekstu
     (partition-by #{\newline})            ; dzielimy na sekwencje sekwencji znaków,
     (map #(apply str %))                  ; a dla każdej sekwencji łączymy znaki,
     (drop-while #(= \newline (first %)))  ; odrzucamy wiodące nowe linie
     (take-nth 2))                         ; i pobieramy co drugi element
; => ("Dzielenie na linie" "Druga")

Ostatni przykład wymaga kilku wyjaśnień, bo zawiera funkcje i konstrukcje, które nie były do tej pory używane. Wybiegamy nim nieco w przyszłość, ponieważ nie poznaliśmy jeszcze sekwencji i funkcji specyficznych dla nich, dlatego możemy się umówić, że nie trzeba dobrze go rozumieć, ale warto do niego wrócić po zapoznaniu się z kolejnymi rozdziałami.

W linii nr 5 widzimy ciekawe makro oznaczone symbolem strzałki o podwójnym grocie (->>). Dzięki niemu można stwarzać łańcuchy przetwarzania i unikać “piętrowych” wyrażeń. Jest to lukier składniowy, dzięki któremu kod programu staje się bardziej czytelny.

Makro to “przewleka” wartość podanego wyrażenia przez wszystkie podane dalej formuły w taki sposób, że zostaje ono najpierw dołączone do pierwszej listy jako jej ostatni argument, a następnie rezultat obliczenia wartości pierwszej listy jest podstawiany jako ostatni argument drugiej podanej listy itd. Efekt działania widać dobrze na przykładzie.

Przykład zastosowania makra ->>
1
2
3
4
5
6
7
8
9
10
;; wersja bez makra ->>
(vec (map inc (take-nth 3 (vector 1 2 3 4 5 6 7 8 9 10))))
; => [2 5 8 11]

;; wersja z makrem ->>
(->> (vector 1 2 3 4 5 6 7 8 9 10)
     (map inc)
     (take-nth 3)
     (vec))
; => [2 5 8 11]

Widzimy powyżej, że przewlekanie wartości końcowej przez kolejne formuły może być dobrym sposobem reprezentowania złożonych, kaskadowych wyrażeń o większej liczbie operacji.

Wiemy już jak zorganizowane są kolejne etapy filtrowania danych w naszym poprzednim przykładzie. Spróbujmy rozpisać wykonywane operacje i zobaczyć co się dzieje:

  1. tekst:

     "\n\nDzielenie na linie\nDruga"
    
  2. Po (partition-by #{\newline}):

    ( (\newline \newline)
      (\D \z \i \e \l \e \n \i \e \space \n \a \space \l \i \n \i \e)
      (\newline)
      (\D \r \u \g \a) )
    

    Funkcja partition-by dzieli sekwencję na wiele sekwencji, używając funkcji podanej jako pierwszy argument. W naszym przypadku dokonany został podział sekwencji znaków pochodzących z tekst (dodanego przez makro) na 4 sekwencje. Operacja używana do przeprowadzenia podziału to zbiór, który może być nie tylko strukturą danych, ale również funkcją.

    Wywołany w formie funkcji zbiór działa w ten sposób, że zwracana jest wartość elementu, który podano jako argument, jeżeli ten element znajduje się w zbiorze; gdy go brak, zwracana jest wartość nil. W tym przypadku jedynym elementem zbioru jest znak nowej linii, więc korzystająca z takiej funkcji porównującej funkcja partition-by podzieli sekwencję w miejscach, gdzie elementami są znaki nowej linii (każdy element sekwencji będzie wcześniej porównany z użyciem funkcji zbioru).

  3. Po (map #(apply str %)):

    ("\n\n" "Dzielenie na linie" "\n" "Druga")
    

    Funkcja map przyjmie sekwencję sekwencji i dla każdej z tych pierwszych (czyli dla wydzielonych linii i znaków nowej linii przedstawionych jako sekwencje znaków) wywoła funkcję str, przekazując wszystkie elementy danej sekwencji jako argumenty. Zostaną one zamienione na łańcuchy znakowe.

  4. Po (drop-while #(= \newline (first %))):

    ("Dzielenie na linie" "\n" "Druga")
    

    Funkcja drop-while sprawia, że usunięty zostanie każdy element, który spełni warunek podany jako anonimowa funkcja. Ta funkcja z kolei sprawdza, czy pierwszy znak napisu jest nową linią. Funkcja ta działa tylko dla pierwszych elementów, dopóki spełniają one warunek. Używamy jej, aby wyeliminować wiodące napisy, które składają się wyłącznie ze znaków nowej linii.

  5. Po (take-nth 2):

    ("Dzielenie na linie" "Druga")
    

    Ostatnim filtrem stworzonego przez nas łańcucha przetwarzania jest funkcja take-nth, która pobiera w tym przypadku co drugi element sekwencji. Jest to konieczne, ponieważ partition-by podzieliła sekwencję znaków, ale pozostawiła w niej znaki nowej linii, na bazie których dzielenie było wykonywane (a tych nie potrzebujemy). Widzimy przy okazji dlaczego istotne było usunięcie wiodących łańcuchów znakowych, które na początku zawierają znaki nowej linii. Gdyby nie to, nie moglibyśmy trafić z filtrem, który “w ciemno” eliminuje co drugi element, spodziewając się tam właśnie tych zbędnych znaków (na tym etapie zmienionych już w napisy). Oczywiście moglibyśmy przeszukać sekwencję i odfiltrować elementy, które zaczynają się znakami nowej linii, ale byłoby to mniej wydajne od usuwania parzystych elementów.

Predykaty łańcuchowe

Testowanie typu, string?

Sprawdzania czy podany argument jest łańcuchem znakowym można dokonać z wykorzystaniem funkcji string?.

Użycie:

  • (string? wartość).

Pierwszym argumentem funkcji powinna być wartość. Jeżeli będzie ona łańcuchem znakowym, zwrócona zostanie wartość true, a w przeciwnym razie wartość false.

Przykład użycia funkcji string?
1
2
(string? "Baobab")  ; => true
(string?        7)  ; => false

Sprawdzanie czy łańcuch jest czysty, blank?

Sprawdzanie czy łańcuch znakowy jest czysty (jest wartością nil, ma zerową długość lub składa się z samych białych znaków) możliwe jest z użyciem funkcji clojure.string/blank?.

Użycie:

  • (clojure.string/blank? łańcuch).

Funkcja przyjmuje łańcuch znakowy, a zwraca true, jeżeli łańcuch jest czysty, a false w przeciwnym razie.

Przykłady użycia funkcji clojure.string/blank?
1
2
3
4
(clojure.string/blank?       "")  ; => true
(clojure.string/blank?      " ")  ; => true
(clojure.string/blank?      nil)  ; => true
(clojure.string/blank? "Baobab")  ; => false

Sprawdzanie czy łańcuch jest pusty, empty?

Sprawdzić czy łańcuch znakowy jest pusty możemy z wykorzystaniem funkcji empty?. Traktuje ona jednak łańcuchy w sposób bardziej uogólniony i przez to nie wykrywa specyficznych warunków, które mogłyby świadczyć o tym, że w sensie informacyjnym mamy do czynienia z brakiem zapisu tekstowego.

Użycie:

  • (empty? łańcuch).

Funkcja zwraca true tylko wtedy, gdy przekazany argument ma wartość nil lub jest łańcuchem znakowym o zerowej długości, a false w przeciwnym razie.

Przykład użycia funkcji empty?
1
2
(empty? "")
; => true

Kolekcje i sekwencje

Kolekcja to abstrakcyjna klasa złożonych struktur danych, które służą do przechowywania danych wieloelementowych. Z kolei sekwencja to w działaniu przypominający iteratory interfejs dostępu do wielu obecnych w Clojure kolekcji. Różnica w stosunku do iteratorów polega jednak na tym, że sekwencje nie pozwalają na mutacje danych.

Obu wspomnianym składnikom języka poświęcone są kolejne części serii “Poczytaj mi Clojure”:

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

Komentarze