Poczytaj mi Clojure, cz. 4

Struktury danych

Grafika

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 danych w przyjętym przez nas modelu.

Struktury danych

Struktura danych (ang. data structure) to sposób organizowania danych w pamięci operacyjnej dostępnej programowi komputerowemu. 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 w implementacji algorytmu kompresji. Struktury danych są abstrakcyjnymi obiektami o cechach, które mogą być wyrażane z użyciem różnych typów danych.

Typ danych (ang. data type) to z kolei klasa wartości o wspólnych właściwościach, np. możliwych zakresach, wielkościach zajmowanej przestrzeni pamięciowej bądź sposobie reprezentowania informacji. Również w przypadku typów możemy mieć do czynienia z ich doborem odpowiednim do rozwiązywanego problemu. Na przykład obliczenia matematyczne ze względów wydajnościowych lepiej przeprowadzać na typach numerycznych, reprezentowanych w pamięci liczbami całkowitymi, a nie na łańcuchach znakowych przedstawiających liczby alfabetycznie (chociaż jest to możliwe i czasami stosowane, np. w rachunkach operujących na naprawdę wielkich wartościach). Z punktu widzenia kompilatora typ konkretnej wartości jest jej metadaną – adnotacją o tym, jak się z nią obchodzić podczas tłumaczenia kodu źródłowego na maszynowy.

Warto mieć na względzie, że współczesne urządzenia komputerowe potrafią operować wyłącznie na liczbach całkowitych, więc potrzebne są sposoby wyrażania danych o innej charakterystyce (np. napisów, liczb zmiennoprzecinkowych, ułamków czy wartości logicznych) w postaci właśnie tych liczb. W tym przypadku typy danych pomagają oznaczać pewne klasy wartości, aby następnie kompilator lub interpreter mógł dobierać odpowiednie metody ich reprezentowania we wspomnianej postaci numerycznej. Na przykład łańcuch znakowy będzie reprezentowany sekwencją bajtów lub struktur wielobajtowych, a operujące na nim funkcje, gdy zajdzie taka potrzeba, skorzystają z wbudowanych w język mechanizmów przezroczystej konwersji na odpowiadające wartościom symbole alfabetu.

Typy dostarczają również informacji o tym, jak zarządzać przestrzenią pamięciową i automatycznymi konwersjami danych jednego rodzaju do danych innego. W zależności od języka programowania wiedza o typach używanych w programie będzie mniej lub bardziej eksponowana. W językach z silnym typizowaniem (ang. strong typing) mamy obowiązek oznaczania danych typami, aby kompilator mógł wyłapać niezgodności między naszymi deklaracjami a rzeczywistymi zastosowaniami; np. przekazywanie do funkcji liczby, gdy wymagana jest sekwencja znakowa będzie błędem kompilacji.

W Clojure mamy do czynienia z dynamicznym typizowaniem (ang. dynamic typing), czyli dynamicznym sprawdzaniem typów (ang. dynamic type-checking). Oznacza to, że sprawdzanie typów danych i kompatybilności pamięciowych obiektów pod tym względem odbywa się w czasie uruchamiania programu, a nie na etapie kompilacji.

Język Clojure to również tzw. typizowanie słabe (ang. weak typing), które polega na tym, że programista nie musi deklarować typów danych będących w użyciu. Jest to możliwe m.in. dzięki tzw. inferencji typów (ang. type inference), czyli ich automatycznemu odgadywaniu, a także generycznym konstrukcjom, które potrafią operować na danych dowolnego rodzaju (szczególnie w przypadku kolekcji i sekwencji).

Jednym z zastosowań wiedzy o typach danych może być polimorficzne optymalizowanie kodu, które polega na generowaniu wielu wersji tego samego podprogramu w zależności od rodzajów przyjmowanych obiektów wejściowych. Na przykład argumentem tworzonej funkcji może być albo liczba całkowita, albo łańcuch znakowy. Podczas kompilacji powstaną wtedy dwa warianty funkcyjnego obiektu w zależności od typu akceptowanego wejścia. Wygenerowana zostanie też odpowiednia funkcja dyspozycyjna, której zadaniem będzie wywoływanie pasującego do typu podprogramu obsługi. Dzięki temu kompilator będzie w stanie zastosować optymalizacje związane z konkretnym rodzajem przetwarzanych wartości w każdym wariancie funkcji (np. w przeciwieństwie do łańcuchów znakowych dane numeryczne nie muszą być rygorystycznie sprawdzane pod kątem liczby elementów czy symboli terminujących).

Charakterystyki struktur danych w Clojure

Struktury trwałe

Wróćmy do struktur danych. W Clojure są one trwałe (ang. persistent). Oznacza to, że wprowadzanie w nich zmian zawsze polega na stwarzaniu nowych obiektów przy zachowaniu oryginalnych wersji. Mamy więc do czynienia z pewnego rodzaju historią zmian.

Na poziomie wewnętrznych mechanizmów języka złożone struktury trwałe poddawane są pewnym modyfikacjom, aby wyrażać zachodzące w czasie zmiany. Polega to na tym, że w przypadku wprowadzenia zmiany w trwałej strukturze danych tworzona jest co prawda jej zupełnie nowa wersja, jednak nie dochodzi do kopiowania wszystkich elementów z poprzedniej (co byłoby mało wydajne), lecz w wynikowym obiekcie umieszczane są odwołania do poszczególnych elementów lub obszarów wersji pierwotnej struktury. Technikę tę nazywamy współdzieleniem strukturalnym (ang. structural sharing).

Po dodaniu elementu do wektora tworzony jest nowy obiekt, który współdzieli z poprzednim większość struktury

Po dodaniu elementu do wektora tworzony jest nowy obiekt, który współdzieli z poprzednim większość struktury

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.

Dane niemutowalne

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 i nie dochodzi do modyfikacji istniejących obszarów pamięciowych. Gdybyśmy mieli do czynienia z językiem mocno imperatywnym, powiedzielibyśmy, że zmiennej, której raz nadano wartość, nie można już później modyfikować.

Chociaż wewnętrznie mamy do czynienia ze współdzieleniem danych w obrębie struktur trwałych i pewnymi modyfikacjami odniesień do zmienianych fragmentów, procesy te są bezpiecznie izolowane i ukrywane przed programistą w implementacji języka.

Z perspektywy twórcy programu operacja wykonywana na strukturze trwałej daje w efekcie całkiem nowy obiekt. Przykładem może być tu duży zbiór, do którego dodajemy jeden element. W wyniku takiej operacji w programie 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ą. Oszczędza to pamięć i czas.

Typy referencyjne

Istotnym wyjątkiem od reguły niezmienności danych są w Clojure tzw. typy referencyjne (ang. reference types). Ich pamięciową zawartość możemy nadpisywać, jednak odbywa się to z użyciem odpowiednich mechanizmów międzywątkowego izolowania i zarządzania sposobem wprowadzania zmian. Co to w praktyce oznacza? Jeżeli istnieje konieczność używania stałej nazwy do reprezentowania zmiennych danych, to istnieją obiekty takich typów, które nam to umożliwią, a dodatkowo zadbają o sytuacje wyjątkowe, np. jednoczesny dostęp do aktualnie wyrażanej wartości przez wiele wątków w tym samym czasie.

Obiekty referencyjne 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 wyrażających zmieniające się stany. To, co jest aktualizowane, to właśnie referencja. W jednej chwili może wskazywać na daną wartość, a w drugiej na zupełnie inną. Część mutowalna (pamięciowy obszar poddawany modyfikacji) jest ograniczona do minimum, przez co łatwiej zarządzać zmianami.

Typy referencyjne pozwalają wskazywać na zmieniające się w czasie wartości

Typy referencyjne pozwalają wskazywać na zmieniające się w czasie wartości

W Clojure dzięki typom referencyjnym jesteśmy w stanie kontrolować zasady wymiany danych między równolegle wykonywanymi czynnościami, a także globalnie (w całym programie) identyfikować wybrane pamięciowe obiekty, np. widoczne w obrębie przestrzeni nazw funkcje bądź ważne ustawienia aplikacji.

Wybór konkretnego typu referencyjnego zależał będzie od sposobu, w jaki chcemy operować na danych w kontekście wielu wątków:

  • W przypadku stanów, które zmieniają się rzadko, skorzystamy z typu Var, który domyślnie zapewni izolowanie wartości bieżących między wątkami. Obiekty tego typu są poddawane automatycznej dereferencji, tzn. wystarczy odwołanie do ich symbolicznej nazwy, aby poznać wartość bieżącą.

  • Do obsługi tożsamości, których stany mogą być zmieniane przez wiele wątków, a zależy nam na tym, aby w danym kwancie czasu tylko jeden wątek mógł dokonać modyfikacji, posłużymy się typem Atom.

  • Jeżeli kolejność aktualizowania współdzielonego stanu nie jest dla nas istotna (np. w przypadku komunikatów obsługi zdarzeń) i nie chcemy, aby operacje aktualizacji były blokujące, wybierzemy typ Agent.

  • Bardziej złożone operacje, które wymagają skoordynowanej obsługi wielu wartości, będą wymagały typu Ref, który korzysta z programowej pamięci transakcyjnej.

Poza wyżej wspomnianymi znajdziemy w Clojure jeszcze kilka innych typów referencyjnych:

  • Future – służący do realizowania obliczeń w wątku innym, niż bieżący przy blokowaniu bieżącego wątku w oczekiwaniu na rezultat;

  • Promise – pełniący podobną funkcję do Future, lecz z możliwością wyboru, w którym wątku przeprowadzone będzie ustawienie bieżącej wartości;

  • Delay – wykorzystywany do synchronicznego odkładania w czasie wartościowania, które zostanie przeprowadzone przy pierwszej próbie odczytu;

  • Volatile – przypomina Atom, jednak z uwagi na brak gwarancji, że operacje będą atomowe, wykorzystywany do przeprowadzania modyfikacji wartości bieżącej w obrębie pojedynczego wątku.

Zobacz także:

Powiązania

Warto zaznaczyć, że w 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, pobieranie ich z otoczenia lub umieszczanie w kodzie programu w postaci literalnej.

Jeżeli rezultaty operacji przeprowadzanych na danych mają być w jakiś sposób zapamiętywane i identyfikowane (śledzone), można z użyciem powiązań stwarzać tożsamości, które będą wyrażały szeregi zmian. Są na to dwa sposoby: pierwszy polega na powiązaniu symbolicznej nazwy ze wskazaną wartością (np. z użyciem formy let), a drugi na stworzeniu obiektu referencyjnego i ustawieniu jego wartości bieżącej, którą będzie można podmieniać przez aktualizowanie odniesienia.

W praktyce bardzo często spotkamy się z sytuacją, w której oba wspomniane wyżej sposoby będą wykorzystywane równocześnie. Na przykład, aby stworzyć w programie globalnie widoczną wartość o ustalonej nazwie (odpowiednik zmiennej), użyjemy symbolicznego identyfikatora. Zostanie on powiązany w przestrzeni nazw z obiektem referencyjnym typu Var, a dopiero zawarte w nim odniesienie będzie wskazywało na konkretną wartość.

Zmienna globalna w Clojure bazuje na powiązaniu symbolu z obiektem referencyjnym typu Var i powiązaniu tego obiektu z wartością bieżącą

Zmienna globalna w Clojure bazuje na powiązaniu symbolu z obiektem referencyjnym typu Var i powiązaniu tego obiektu z wartością bieżącą

Nasuwają się tu dwa pytania. Po pierwsze: czy nie wystarczyłaby sama przestrzeń nazw i skojarzenie w niej symbolu z wartością w pamięci? Tak, byłoby to możliwe, jednak wtedy wprowadzilibyśmy do języka konwencjonalne zmienne globalne, a więc konstrukcję, która wymaga „uzbrajania” w odpowiednie mechanizmy kontroli i izolacji, gdy program ma działać na wielu wątkach. Dodatkowo, rodzaje tych mechanizmów musiałyby być dopasowane do typów konkretnych zmiennych. Bez tego, lub w przypadku błędnej implementacji współbieżnych wzorców przez programistę, mogłyby się pojawić problemy, z którymi mamy do czynienia w paradygmacie imperatywnym: sytuacje wyścigów, zakleszczenia itd.

Kolejną kwestią jest – wydawać by się mogło – nieco skomplikowane przedstawienie całej operacji nazywania globalnych wartości. Najpierw symboliczny identyfikator musi być przypisany do obiektu referencyjnego w przestrzeni nazw, a następnie ustawiona powinna być wartość bieżąca odniesienia. Na szczęście nie musimy (chociaż możemy) tak szczegółowo zarządzać tym procesem, ponieważ mamy do dyspozycji odpowiednie makra i formy specjalne, np. defdefn.

Tworzenie globalnej zmiennej i funkcji nazwanej
(def x 1)
; => #'user/x

(defn funkcja [] 1)
; => #'user/funkcja
(def x 1) ; => #'user/x (defn funkcja [] 1) ; => #'user/funkcja

W Clojure mamy również do czynienia z bezpośrednim kojarzeniem identyfikatorów z wartościami, jednak odbywa się to w ograniczonym zasięgu leksykalnym, a dokładniej w ciele wspomnianej wcześniej konstrukcji let. W takich przypadkach odwzorowanie symbolu na wartość jest chwilowo (na czas obliczenia wyrażeń wprowadzonych do tej formy) przechowywane na specjalnym stosie powiązań utrzymywanym dla danego wątku. Również tu możemy mieć do czynienia z aktualizacją wartości, która polegać będzie na nadpisaniu odwzorowania (w tzw. wektorze powiązaniowym formy let), jednak wydarzać się to będzie w sposób uszeregowany: najpierw pierwsza aktualizacja, potem następna itd. Powiązanie leksykalne nie wyraża współdzielonego stanu, tak więc nie musi być specjalnie traktowane w kontekście obsługi wielu wątków.

Aktualizowanie powiązania leksykalnego
(let [x 1            ; x powiązany z 1
      x (inc x)      ; x powiązany z 2, bo (inc x)
      x (inc x)] x)  ; x powiązany z 3, bo (inc x)
; => 3
(let [x 1 ; x powiązany z 1 x (inc x) ; x powiązany z 2, bo (inc x) x (inc x)] x) ; x powiązany z 3, bo (inc x) ; => 3

Zobacz także:

Identyfikatory

Identyfikatory to takie typy danych, które służą do nazywania wartości, elementów kolekcji bądź obiektów referencyjnych. W Clojure w roli identyfikatorów stosowane są symbole lub słowa kluczowe. Te pierwsze, jeżeli wyrażone literalnie, mają specjalne znaczenie składniowe, tzn. przeprowadzany jest względem nich proces rozpoznawania nazw (ang. name resolution), zwanego też rozwiązywaniem nazw. Słowa kluczowe nie są automatycznie przekształcane na identyfikowane nimi obiekty, a stosowane są do tworzenia typów wyliczeniowych i indeksowania zawartości struktur asocjacyjnych.

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, jak mogliśmy przeczytać wcześniej, polegają na kojarzeniu symboli ze stałymi wartościami bądź z obiektami referencyjnymi.

Przypomnijmy: Na poziomie leksykalnym symbol to element języka Clojure, który zaczyna się znakiem niebędącym liczbą i składa się ze znaków alfanumerycznych. Może również zawierać znaki *, +, !, -, _, ', ?, <, > oraz =.

Jeżeli w nazwie symbolu wyrażonego literalnie pojawi się znak ukośnika (/), to znak ten zostanie potraktowany jak separator części określającej przestrzeń nazw i części stanowiącej właściwą nazwę. Podobne znaczenie ma kropka (.), lecz służy ona do oddzielania nazw klas Javy lub części przestrzeni nazw pochodzących z pakietów.

Formy symboli

Istnieją trzy podstawowe formy bazujące na symbolach:

  1. Symbol niezacytowany umieszczony w kodzie źródłowym będzie tworzył tzw. formę symbolową wyrażającą odwołanie do jakiejś wartości. Taki symbol będzie więc wartościowany, a dalsze obliczenia będą polegały na uzyskanym wyniku. Warunkiem jest oczywiście to, żeby wcześniej symbol o podanej nazwie był z wartością powiązany. Przykłady form symbolowych to:

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

    • 'nazwa,
    • '(nazwa inna-nazwa),
    • (quote nazwa),
    • (symbol "nazwa").
  3. W pewnych konstrukcjach (np. w formie specjalnej let, formie specjalnej def, makrze defn bądź wektorze parametrycznym funkcji) niezacytowane symbole będą tworzyły tzw. formy powiązaniowe. Dzięki nim możliwe jest wytwarzanie powiązań z wartościami, czyli nazywanie umieszczonych w pamięci obiektów, np. rezultatów wykonywanych operacji, przyjmowanych argumentów bądź 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).

Warto pamiętać, że 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. Z tego powodu do indeksowania dużych struktur asocjacyjnych lepiej korzystać 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ą
(identical? &#39;a &#39;a) ; czy te dwa symbole są tym samym obiektem? ; =&gt; false ; nie są (= &#39;a &#39;a) ; czy te dwa symbole są równe? ; =&gt; 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, 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. 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
przestrzeń/nazwa &#39;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 formy 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 wyrażać je po prostu umieszczając ich nazwy w kodzie. W takiej formie (symbolowej) 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.

Formy symbolowe

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

Użycie:

  • symbol,
  • przestrzeń-nazw/symbol.

Dokładniej rzecz ujmując, formy 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. 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 formy 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 przypisany do danego wątku stos powiązań 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.

Formy stałe symboli

Symbole w formach stałych 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: nazywania elementów konfiguracji, sterowania przepływem, klasyfikowania innych 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 formy specjalnej quote lub skorzystaniu z apostrofu poprzedzającego nazwę symbolu, który jest makrem czytnika rozwijanym do wywołania tej formy. Pojedynczy, zacytowany z użyciem quote symbol w fazie parsowania 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, 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 wytwarzać 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 formy 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 literalnych
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
(symbol &#34;abc&#34;) ; tworzenie symbolu abc (symbol &#34;user&#34; &#34;abc&#34;) ; tworzenie symbolu abc z przestrzenią user (quote abc) ; cytowanie stwarza symbole, jeśli nazwa jest odpowiednia &#39;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
(list &#39;chleb &#39;mleko &#39;ser) ; lista symboli (quote (chleb mleko ser)) ; zacytowana lista symboli &#39;(chleb mleko ser) ; zacytowana lista symboli
Formy powiązaniowe symboli

Niezacytowane symbole znajdują również zastosowanie w wyrażaniu form 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:

Formy powiązaniowe znajdziemy również w definicjach zmiennych globalnych oraz funkcji.

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

;; wektory parametryczne:
(fn     [a b] (list a b))   ; argumenty funkcji anonimowej
(defn f [a b] (list a b))   ; argumenty funkcji nazwanej

;; wektory powiązaniowe:
(let             [a 5]  a)  ; powiązanie leksykalne
(binding         [a 5]  a)  ; powiązanie dynamiczne
(with-local-vars [a 5] @a)  ; zmienna lokalna
;; definicje zmiennych globalnych i funkcji: (def a 5) ; zmienna globalna powiązana z wartością (def a (fn [] (+ 2 2))) ; zmienna globalna powiązana z funkcją (defn a [] (+ 2 2)) ; zmienna globalna powiązana z funkcją ;; wektory parametryczne: (fn [a b] (list a b)) ; argumenty funkcji anonimowej (defn f [a b] (list a b)) ; argumenty funkcji nazwanej ;; wektory powiązaniowe: (let [a 5] a) ; powiązanie leksykalne (binding [a 5] a) ; powiązanie dynamiczne (with-local-vars [a 5] @a) ; zmienna lokalna

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
;; całkowicie unikatowa nazwa (gensym) ; =&gt; G__2862 ;; unikatowa nazwa z podanym przedrostkiem (gensym &#34;siefca&#34;) ; =&gt; siefca2865

Testowanie typu, symbol?

Możemy sprawdzić, czy dany obiekt na pewno jest symbolem (formą stałą symbolu) z użyciem 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 wartość identyfikowana symbolem test jest symbolem?
; => false      ; nie, nie jest
(symbol? &#39;test) ; czy test jest symbolem? ; =&gt; true ; tak, jest (symbol? test) ; czy wartość identyfikowana symbolem test jest symbolem? ; =&gt; false ; nie, nie jest

Testowanie kwalifikacji

Możemy sprawdzić, czy symbol zawiera przestrzeń nazw z użyciem predykatów:

  • (simple-symbol?    wartość) – czy jest symbolem bez przestrzeni nazw,
  • (qualified-symbol? wartość) – czy zawiera przestrzeń nazw.
Przykłady użycia predykatów simple-symbol?qualified-symbol?
1
2
3
4
5
(simple-symbol? 'test)       ; czy test jest prostym symbolem?
; => true                    ; tak, jest

(qualified-symbol? 'x/test)  ; czy x/test ma przestrzeń nazw?
; => true                    ; tak, ma
(simple-symbol? &#39;test) ; czy test jest prostym symbolem? ; =&gt; true ; tak, jest (qualified-symbol? &#39;x/test) ; czy x/test ma przestrzeń nazw? ; =&gt; true ; tak, ma
Symbole jako funkcje

Symbole mogą być używane jako funkcje. Są wtedy formami przeszukiwania kolekcji. Funkcje przeszukujące na bazie symboli przyjmują 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
(&#39;a {&#39;a 1, &#39;b 2} &#34;brak&#34;) ; =&gt; 1 (&#39;a {&#39;x 1, &#39;b 2} &#34;brak&#34;) ; =&gt; &#34;brak&#34; (&#39;a {&#39;x 1, &#39;b 2}) ; =&gt; nil (&#39;a &#39;{a 1, b 2} &#34;brak&#34;) ; =&gt; 1 (&#39;a &#39;{x 1, b 2} &#34;brak&#34;) ; =&gt; &#34;brak&#34; (&#39;a &#39;{x 1, b 2}) ; =&gt; nil ((quote a) &#39;{a 1} &#34;brak&#34;) ; =&gt; 1 ((quote a) &#39;{x 1} &#34;brak&#34;) ; =&gt; &#34;brak&#34; ((quote a) &#39;{x 1}) ; =&gt; nil (&#39;a #{&#39;a &#39;b} &#34;brak&#34;) ; =&gt; a (&#39;a #{&#39;x &#39;b} &#34;brak&#34;) ; =&gt; &#34;brak&#34; (&#39;a #{&#39;x &#39;b}) ; =&gt; nil (&#39;a &#39;#{a b} &#34;brak&#34;) ; =&gt; a (&#39;a &#39;#{x b} &#34;brak&#34;) ; =&gt; &#34;brak&#34; (&#39;a &#39;#{x b}) ; =&gt; 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 mogą być tu zmienne globalne, których właściwościami możemy sterować z użyciem metadanych skojarzonych z symbolami używanymi do nadania im nazw. Obiekty typu Var mogą być wyposażone w następujące metadane:

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 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 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}
;; tworzenie zmiennej globalnej (referencji do wartości 5) (def x 5) ;; brak metadanych dla wartości 5 (wskazywanej symbolem) (meta x) ; =&gt; nil ;; brak metadanych dla symbolu x (meta &#39;x) ; =&gt; nil ;; są metadane dla zmiennej globalnej (meta #&#39;x) ; =&gt; { :ns #&lt;Namespace user&gt;, ; =&gt; :name x, :file &#34;NO_SOURCE_PATH&#34;, ; =&gt; :column 1, ; =&gt; :line 1 } ;; wyrażenie metadanowe, są metadane (meta &#39;^:testowa y) ; =&gt; {:testowa true}

Dodawanie metadanych, with-meta

Aby ustawić własne metadane symbolu, można 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
(with-meta &#39;nazwa {:klucz &#34;wartość&#34;}) ; =&gt; 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
22
;; tworzenie zmiennej globalnej (referencji do wartości 5)
;; z ustawioną w symbolu metadaną wyrażającą ustawioną flagę
(def ^:testowa x 5)
; => #'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 }
;; tworzenie zmiennej globalnej (referencji do wartości 5) ;; z ustawioną w symbolu metadaną wyrażającą ustawioną flagę (def ^:testowa x 5) ; =&gt; #&#39;user/x (meta #&#39;x) ; =&gt; { :ns #&lt;Namespace user&gt;, ; =&gt; :name x, :file &#34;NO_SOURCE_PATH&#34;, ; =&gt; :column 1, ; =&gt; :line 1, ; =&gt; :testowa true} (def ^{ :testowa &#34;napis&#34;, :druga 123 } x 5) ; =&gt; #&#39;user/x (meta #&#39;x) ; =&gt; { :ns #&lt;Namespace user&gt;, ; =&gt; :name x, :file &#34;NO_SOURCE_PATH&#34;, ; =&gt; :column 1, ; =&gt; :line 1, ; =&gt; :testowa &#34;napis&#34;, ; =&gt; :druga 123 }

Uwaga: W przypadku form 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ą formą 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 automatyczny.

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

Warto pamiętać, że 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ą
(identical? :a :a) ; czy te dwa klucze są tym samym obiektem? ; =&gt; true ; tak są (= :a :a) ; czy te dwa klucze są równe? ; =&gt; 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żeli na przykład sprawdzane jest samo istnienie obiektu klucza (utworzonego przez przynajmniej jednokrotne jego użycie) i zależy od tego logika działania programu, wtedy tworząc bibliotekę korzystającą z kluczy, a której użyje autor takiej aplikacji, potencjalnie moglibyśmy wpłynąć na jej poprawną pracę.

W zapisie literalnym słowa kluczowe mogą zawierać znak ukośnika, który posłuży do oddzielenia części reprezentującej przestrzeń nazw od właściwej nazwy słowa kluczowego.

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
(keyword &#34;klucz&#34;) ; =&gt; :klucz (keyword &#34;przestrzeń&#34; &#34;a&#34;) ; =&gt; :przestrzeń/a
Klucze literalne

Tworzyć słowa kluczowe możemy z wykorzystaniem literału kluczowego w postaci zapisu z wiodącym dwukropkiem.

Użycie:

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

Przed właściwą nazwą klucza możemy umieścić nazwę przestrzeni nazw oddzieloną znakiem ukośnika. Gdy literał rozpoczęto znakiem pojedynczego dwukropka, to podawana przestrzeń nazw nie musi być zdefiniowana; możemy więc podać dowolny ciąg znaków.

Dwa dwukropki przy nazwie będą oznaczały, że życzymy sobie, aby tworzone słowo kluczowe było poddane procesowi rozpoznawania przestrzeni nazw. Oznacza to, że stworzony zostanie klucz z dookreśloną przestrzenią nazw, przy czym musi ona istnieć w momencie wczytywania i wartościowania zapisu. Jeżeli dana przestrzeń nazw jest aliasem utworzonym w bieżącej, to podczas tworzenia klucza zostanie podstawiona nazwa docelowej przestrzeni.

Szczególną sytuacją jest literał słowa kluczowego, który poprzedzono dwoma dwukropkami bez podawania przestrzeni nazw. Przestrzeń zostanie ustawiona zgodnie z wartością wskazywaną przez specjalną zmienną dynamiczną *ns*, która oznacza bieżącą przestrzeń nazw.

Przykłady wyrażeń kluczowych
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
:klucz             ; => :klucz
:przestrzeń/klucz  ; => :przestrzeń/klucz

(ns user)          ; bieżąca przestrzeń nazw: user
::klucz            ; => :user/klucz
::user/klucz       ; => :user/klucz

(ns siup)          ; bieżąca przestrzeń nazw: siup
::klucz            ; => :siup/klucz

;; importowanie powiązań z przestrzeni nazw clojure.string
;; do przestrzeni bieżącej z użyciem aliasu s
(require '[clojure.string :as s])

::s/klucz          ; => :clojure.string/klucz
:klucz ; =&gt; :klucz :przestrzeń/klucz ; =&gt; :przestrzeń/klucz (ns user) ; bieżąca przestrzeń nazw: user ::klucz ; =&gt; :user/klucz ::user/klucz ; =&gt; :user/klucz (ns siup) ; bieżąca przestrzeń nazw: siup ::klucz ; =&gt; :siup/klucz ;; importowanie powiązań z przestrzeni nazw clojure.string ;; do przestrzeni bieżącej z użyciem aliasu s (require &#39;[clojure.string :as s]) ::s/klucz ; =&gt; :clojure.string/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)
(keyword? &#34;klucz&#34;) ; =&gt; false (nie jest) (keyword? :klucz) ; =&gt; true (jest)

Testowanie kwalifikacji

Możemy sprawdzić, czy słowo kluczowe zawiera przestrzeń nazw wykorzystując predykaty:

  • (simple-keyword?    wartość) – czy jest kluczem bez przestrzeni nazw,
  • (qualified-keyword? wartość) – czy zawiera przestrzeń nazw.

Przykłady użycia predykatów simple-keyword?qualified-keyword?
1
2
3
4
5
(simple-keyword? :test)        ; czy test jest prostym słowem kluczowym?
; => true                      ; tak, jest

(qualified-keyword? ::x/test)  ; czy x/test ma przestrzeń nazw?
; => true                      ; tak, ma
(simple-keyword? :test) ; czy test jest prostym słowem kluczowym? ; =&gt; true ; tak, jest (qualified-keyword? ::x/test) ; czy x/test ma przestrzeń nazw? ; =&gt; true ; tak, ma

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
(find-keyword &#34;słowo&#34;) ; istnieje? ; =&gt; nil ; nie :słowo ; pierwsze użycie internalizuje klucz (find-keyword &#34;słowo&#34;) ; istnieje? ; =&gt; :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
(find-keyword &#34;przestrzeń&#34; &#34;słowo&#34;) ; istnieje? ; =&gt; nil ; nie :przestrzeń/słowo ; pierwsze użycie internalizuje klucz (find-keyword &#34;przestrzeń&#34; &#34;słowo&#34;) ; istnieje? ; =&gt; :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
(:a {:a 1, :b 2} &#34;brak&#34;) ; =&gt; 1 (:a {:x 1, :b 2} &#34;brak&#34;) ; =&gt; &#34;brak&#34; (:a {:x 1, :b 2}) ; =&gt; nil (:a #{:a :b} &#34;brak&#34;) ; =&gt; :a (:a #{:x :b} &#34;brak&#34;) ; =&gt; &#34;brak&#34; (:a #{:x :b}) ; =&gt; nil

Funkcje obsługi identyfikatorów

Od wersji 1.9 język Clojure wyposażono w kilka funkcji, które pozwalają sprawdzać właściwości identyfikatorów (symboli i kluczy).

Testowanie identyfikatorów

Możemy sprawdzić, czy dany obiekt jest identyfikatorem (symbolem lub kluczem), a także jakie ma cechy, korzystając z następujących predykatów:

  • (ident?           wartość) – czy jest identyfikatorem,
  • (simple-ident?    wartość) – czy jest identyfikatorem bez przestrzeni nazw,
  • (qualified-ident? wartość) – czy jest identyfikatorem z przestrzenią nazw.

Przykłady użycia predykatów identyfikatorowych
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
(ident? 'test)              ; czy test jest identyfikatorem?
; => true                   ; tak, jest

(simple-ident? 'test)       ; czy test jest prostym identyfikatorem?
; => true                   ; tak, jest

(qualified-ident? 'x/test)  ; czy x/test ma przestrzeń nazw?
; => true                   ; tak, ma

(ident? :test)              ; czy :test jest identyfikatorem?
; => true                   ; tak, jest

(simple-ident? :test)       ; czy :test jest prostym identyfikatorem?
; => true                   ; tak, jest

(qualified-ident? :x/test)  ; czy :x/test ma przestrzeń nazw?
; => true                   ; tak, ma
(ident? &#39;test) ; czy test jest identyfikatorem? ; =&gt; true ; tak, jest (simple-ident? &#39;test) ; czy test jest prostym identyfikatorem? ; =&gt; true ; tak, jest (qualified-ident? &#39;x/test) ; czy x/test ma przestrzeń nazw? ; =&gt; true ; tak, ma (ident? :test) ; czy :test jest identyfikatorem? ; =&gt; true ; tak, jest (simple-ident? :test) ; czy :test jest prostym identyfikatorem? ; =&gt; true ; tak, jest (qualified-ident? :x/test) ; czy :x/test ma przestrzeń nazw? ; =&gt; true ; tak, ma

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 formy 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
(if true &#39;tak &#39;nie) ; =&gt; tak (if false &#39;tak &#39;nie) ; =&gt; nie (if nil &#39;tak &#39;nie) ; =&gt; nie (if 0 &#39;tak &#39;nie) ; =&gt; 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
(boolean nil) ; =&gt; false (boolean false) ; =&gt; false (boolean true) ; =&gt; true (boolean 123) ; =&gt; true

Testowanie typu, boolean?

Sprawdzania czy podany argument jest wartością logiczną można dokonać z wykorzystaniem funkcji boolean?.

Użycie:

  • (boolean? wartość).

Jeżeli pierwszym argumentem funkcji będzie wartość logiczna (true lub false), zwrócona zostanie wartość true, a w przeciwnym razie wartość false.

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

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
(true? true) ; =&gt; true (true? false) ; =&gt; false (true? nil) ; =&gt; false (true? 5) ; =&gt; false (false? nil) ; =&gt; false (false? false) ; =&gt; 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
(some? nil) ; =&gt; false (some? true) ; =&gt; true (some? false) ; =&gt; true (some? 1) ; =&gt; true (some? ()) ; =&gt; 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
(nil? nil) ; =&gt; true (nil? true) ; =&gt; false (nil? false) ; =&gt; false (nil? 1) ; =&gt; false (nil? ()) ; =&gt; 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
(not nil) ; =&gt; true (not false) ; =&gt; true (not true) ; =&gt; false (not 0) ; =&gt; false (not 1) ; =&gt; false

Zawsze prawda, any?

Funkcja any? zwraca logiczną prawdę dla każdej wartości argumentu.

Użycie:

  • (any? wartość).

Funkcja przyjmuje jeden argument i zwraca wartość true.

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

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
(and true false) ; =&gt; false (and false true) ; =&gt; false (and false false) ; =&gt; false (and true true) ; =&gt; true

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
(or true false) ; =&gt; true (or false false) ; =&gt; false (or false true) ; =&gt; true (or true true) ; =&gt; true

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 tworzą atomowe S-wyrażenia będące formami stałymi.

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 (met. Gaussa – nieujemna);
  • 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:

    • (+'   & 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) lub niedomiarów (ang. underflows). 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,
    • (unchecked-subtract-int  odjemna      odjemnik) – różnica,
    • (unchecked-multiply-int  czynnik-1   czynnik-2) – iloczyn,
    • (unchecked-divide-int    dzielna      dzielnik) – iloraz,
    • (unchecked-remainder-int dzielna      dzielnik) – reszta z dzielenia;
  • jednoargumentowe:

    • (unchecked-negate-int wartość) – zmiana znaku,
    • (unchecked-inc-int    wartość) – zwiększenie o 1,
    • (unchecked-dec-int    wartość) – zmniejszenie o 1.

Dynamiczna zmienna specjalna 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,
  • (pos-int?) wartość) – czy wartość całkowita jest dodatnia,
  • (neg-int?) wartość) – czy wartość całkowita jest ujemna,
  • (nat-int?) wartość) – czy wartość całkowita jest liczbą naturalną (wł. 0),
  • (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,
  • (double?   wartość) – czy wartość to liczba podwójnej precyzji,

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 z negacją,
    • (bit-or      wartość wartość-2 & wartość…) – suma bitowa,
    • (bit-xor     wartość wartość-2 & wartość…) – 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 w prawo bez 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
(rand) ; =&gt; 0.7355816410955994 (rand 2) ; =&gt; 1.0126588862070758 (rand-int 50) ; =&gt; 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))
(alter-var-root #&#39;*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
(with-precision 5 :rounding CEILING (/ 1M 3)) ; =&gt; 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
\d ; literał znakowy ; =&gt; \d \newline ; literał znakowy znaku specjalnego (nowa linia) ; =&gt; \newline

Znak z kodu, char

Używając funkcji char, możemy tworzyć znak podając jego kod zgodny ze standardem UTF-16BE.

Użycie:

  • (char kod-znaku).

Funkcja przyjmuje jeden argument, który powinien być numerycznie wyrażonym kodem znaku, a zwraca znak o podanym kodzie. Zwracany znak może być wewnętrznie reprezentowany wartością wielobajtową.

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

(char 261)       ; litera ą
; => \ą
(char 100) ; tworzymy znak o kodzie 100 (litera d) ; =&gt; \d (char 261) ; litera ą ; =&gt; \ą

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, na której znajduje się znak, który chcemy pobrać (poczynając od 0).

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

Przykład pobierania pojedynczych znaków z łańcuchów
1
2
(get "siefca" 2)
; => \e
(get &#34;siefca&#34; 2) ; =&gt; \e

Sekwencje znakowe

Warto zauważyć, że łańcuchy znakowe w Clojure można traktować jak sekwencje znakowe i używać w stosunku do nich 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) – odwraca kolejność sekwencji znakowej,
  • (frequencies        łańcuch) – zlicza częstości występowania znaków,
  • (when-first [znak łańcuch …] wyrażenie) – wartościuje 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!
(nth &#34;siefca&#34; 2 ) ; =&gt; \e (rand-nth &#34;siefca&#34; ) ; =&gt; \f (first &#34;siefca&#34; ) ; =&gt; \s (last &#34;siefca&#34; ) ; =&gt; \a (rest &#34;siefca&#34; ) ; =&gt; (\i \e \f \c \a) (apply vector &#34;siefca&#34; ) ; =&gt; [\s \i \e \f \c \a] (seq &#34;siefca&#34; ) ; =&gt; (\s \i \e \f \c \a) (every? char? &#34;siefca&#34; ) ; =&gt; true (reverse &#34;siefca&#34; ) ; =&gt; (\a \c \f \e \i \s) (frequencies &#34;aaabbc&#34; ) ; =&gt; {\a 3, \b 2, \c 1} (when-first [z &#34;abcdef&#34;] z) ; =&gt; \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żeli nie istnieje sekwencja unikowa.

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"
;; brak sekwencji unikowej dla litery c (char-escape-string \c) ; =&gt; nil ;; sekwencja unikowa dla nowej linii (char-escape-string \newline) ; =&gt; &#34;\\n&#34; ;; sekwencja unikowa dla backspace&#39;a (char-escape-string \backspace) ; =&gt; &#34;\\b&#34;

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"
(char-name-string \a) ; =&gt; nil (char-name-string \tab) ; =&gt; &#34;tab&#34;

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
(char? \a) ; =&gt; true (char? 1) ; =&gt; 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
(= \a \a) ; =&gt; true (= \a) ; =&gt; true (= \a \a \a) ; =&gt; true (= \a \a \b) ; =&gt; 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
(compare \a \b) ; =&gt; -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 poza tym można korzystać z operujących na łańcuchach metod Javy.

Łańcuchy znakowe można również traktować jak sekwencje znakowe i korzystać z 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, albo też 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"

""
; => ""
&#34;To jest napis&#34; ; =&gt; &#34;To jest napis&#34; &#34;&#34; ; =&gt; &#34;&#34;

Łańcuchy z szeregu wartości, str

Funkcja str przyjmuje zero lub więcej argumentów. Wartość każdego stara się przekształcić 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 skonwertowanych 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)                   ; => ""
(str 1 2 3) ; =&gt; &#34;123&#34; (str &#34;a&#34; &#34;b&#34; &#34;c&#34; \d 4) ; =&gt; &#34;abcd4&#34; (str) ; =&gt; &#34;&#34;

Łańcuchy z formatu, format

Funkcja format przyjmuje minimum jeden argument, którym powinien być łańcuch formatujący zgodny ze składnią używaną przez java.util.Formatter (odpowiadającą 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.

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!"
(format &#34;Witaj, %s!&#34;, &#34;Baobabie&#34;) ; =&gt; &#34;Witaj, Baobabie!&#34;

Ł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 (obliczone). Jeżeli któreś z nich 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
3
;; standardowe wyjście wyrażenia do łańcucha
(with-out-str (println "Baobab"))
; => "Baobab\n"
;; standardowe wyjście wyrażenia do łańcucha (with-out-str (println &#34;Baobab&#34;)) ; =&gt; &#34;Baobab\n&#34;

Ł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
(pr-str [1 2 3 4] (1)) ; wpisuje w łańcuch reprezentację obiektów ; =&gt; &#34;[1 2 3 4] (1)&#34; ; 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
(prn-str [1 2 3 4] (1)) ; wpisuje w łańcuch reprezentację obiektów ; =&gt; &#34;[1 2 3 4] (1)\n&#34; ; zwróconą przez funkcję prn

Łańcuchy z rezultatu print, 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"
(print-str &#34;Ba&#34; \o \b &#39;ab) ; tworzy łańcuch z efektu wywołania print ; =&gt; &#34;Ba o b ab&#34;

Łańcuchy z rezultatu println, println-str

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

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

Funkcja przyjmuje zero lub więcej wartości i każdą z nich konwertuje 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"
(println-str &#34;Ba&#34; \o \b &#39;ab) ; tworzy łańcuch z efektu wywołania println ; =&gt; &#34;Ba o b ab\n&#34;

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

Funkcje index-oflast-index-of

Pobieranie pozycji podanego łańcucha w tekście możliwe jest z użyciem funkcji clojure.string/index-ofclojure.string/last-index-of.

Użycie:

  • (clojure.string/index-of      łańcuch fragment start?)
    – wyszukuje pozycję pierwszego wystąpienia,

  • (clojure.string/last-index-of łańcuch fragment start?)
    – wyszukuje pozycję ostatniego wystąpienia.

Funkcje przyjmują dwa obowiązkowe 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.

Gdy podano opcjonalny, trzeci argument start, powinien on określać pozycję, od której rozpoczęte będzie przeszukiwanie.

Przykłady użycia funkcji index-of i last-index-of
1
2
(clojure.string/index-of      "Baobab tu był." "b")  ; => 3
(clojure.string/last-index-of "Baobab tu był." "b")  ; => 10
(clojure.string/index-of &#34;Baobab tu był.&#34; &#34;b&#34;) ; =&gt; 3 (clojure.string/last-index-of &#34;Baobab tu był.&#34; &#34;b&#34;) ; =&gt; 10

Zliczanie znaków

Funkcja count

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

Wykorzystywany jest sekwencyjny interfejs dostępu do łańcuchów znakowych, więc czas zliczania zależny jest od liczby elementów.

Użycie:

  • (count łańcuch).

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

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

Wyrażenia regularne

Wyrażenia regularne (ang. regular expressions, skr. regexps) to obiekty, które pozwalają opisywać wzorce dopasowywania do tekstu. Wewnętrznie reprezentowane są odpowiednio zoptymalizowanymi strukturami danych, ale podczas ich tworzenia korzysta się z czytelnego zapisu tekstowego, zrozumiałego dla człowieka.

Wyrażeń regularnych możemy używać w celu sprawdzania, czy podane łańcuchy znakowe lub ich części pasują do określonych wzorców. Jesteśmy też w stanie budować operatory, które na podstawie wyrażeń regularnych będą dokonywały zastępowania pasujących fragmentów tekstu innymi lub wyodrębniały te fragmenty.

Literały wyrażeń regularnych

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

  • #"wzorzec",

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

Tworzenie z wzorca, re-pattern

Tworzenie obiektu wyrażenia regularnego z tekstowej reprezentacji wzorca dopasowywania jest możliwe dzięki funkcji re-pattern. Wygenerowana wartość to skompilowana forma podanej, tekstowej reprezentacji wyrażenia, a posługiwanie się takim obiektem wpływa korzystnie na wykorzystanie zasobów procesora. Obiektu wyrażenia regularnego 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, a zwraca obiekt wyrażenia regularnego.

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

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

Tworzenie dopasowywacza, re-matcher

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

Do tworzenia obiektu dopasowującego używa się funkcji re-matcher.

Użycie:

  • (re-matcher wyrażenie łańcuch).

Funkcja przyjmuje dwa argumenty. Pierwszym powinien być obiekt wyrażenia regularnego, 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")
(re-matcher #&#34;\\d+&#34; &#34;abcd1234efgh5678&#34;)

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 dopasowania, np. re-find.

Uwaga: 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 wyrażenia regularnego lub dopasowywacza. Przyjmuje ona dwa argumenty: wyrażenie regularne i łańcuch znakowy. Można też użyć jej w formie jednoargumentowej – przyjmuje wtedy obiekt dopasowujący (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żane. 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 fragment łańcucha, który pasuje do wszystkich wzorców.

Funkcja re-find wewnętrznie czyni użytek z opisanej niżej funkcji re-groups, aby zwrócić wyniki.

Użycie:

  • (re-find dopasowywacz),
  • (re-find wyrażenie łańcuch).

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

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

;; zwraca pierwsze dopasowanie

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

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

;; nazywamy globalny obiekt dopasowujący
(def pasuj (re-matcher #"\d+" "abc123def456"))
; => #'user/pasuj

;; powtarzamy trzykrotnie dopasowywanie
(repeatedly 3
            #(re-find pasuj))
; => ("123" "456" nil)
(re-find #&#34;\d+&#34; &#34;abc123def456&#34;) ; =&gt; &#34;123&#34; ;; zwraca pasujące grupy (re-find #&#34;(\d+)-(\d+)-(\d+)&#34; &#34;000-111-222&#34;) ; =&gt; [&#34;000-111-222&#34; &#34;000&#34; &#34;111&#34; &#34;222&#34;] ;; nazywamy globalny obiekt dopasowujący (def pasuj (re-matcher #&#34;\d+&#34; &#34;abc123def456&#34;)) ; =&gt; #&#39;user/pasuj ;; powtarzamy trzykrotnie dopasowywanie (repeatedly 3 #(re-find pasuj)) ; =&gt; (&#34;123&#34; &#34;456&#34; 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 dopasowywacz).

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

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.

Przykład użycia funkcji re-groups
1
2
3
4
5
6
;; zmienna globalna tel wskazuje obiekt dopasowujący
(def tel (re-matcher #"(\d+)-(\d+)-(\d+)" "000-111-222"))

;; odczyt pasujących grup w postaci wektora
(re-groups tel)
; => ["000-111-222" "000" "111" "222"]
;; zmienna globalna tel wskazuje obiekt dopasowujący (def tel (re-matcher #&#34;(\d+)-(\d+)-(\d+)&#34; &#34;000-111-222&#34;)) ;; odczyt pasujących grup w postaci wektora (re-groups tel) ; =&gt; [&#34;000-111-222&#34; &#34;000&#34; &#34;111&#34; &#34;222&#34;]

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

Dopasowywanie, re-matches

Funkcja re-matches pozwala sprawdzić, czy podany łańcuch znakowy pasuje do wzorca reprezentowanego wyrażeniem regularnym.

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 wyrażenie łańcuch).

Funkcja przyjmuje dwa argumenty. Pierwszym powinno być wyrażenie regularne, a drugim ł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"]
(re-matches #&#34;(\d+)-(\d+)-(\d+)&#34; &#34;000-111-222&#34;) ; =&gt; [&#34;000-111-222&#34; &#34;000&#34; &#34;111&#34; &#34;222&#34;]

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 wyrażenie ł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")
(re-seq #&#34;[\p{L}\p{Digit}_]+&#34; &#34;Podzielimy to na słowa&#34;) ; =&gt; (&#34;Podzielimy&#34; &#34;to&#34; &#34;na&#34; &#34;słowa&#34;)

Cytowanie, 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. W przypadku statycznego napisu dodalibyśmy po prostu znak odwróconego ukośnika (\$1), aby odebrać specjalne znaczenie zapisowi. Gdyby jednak zastępnik był efektem działania programu, musielibyśmy go zmienić, zastępując specjalne wzorce innymi. W czynności tej może wyręczyć nas funkcja re-quote-replacement.

Użycie:

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

Pierwszym i jedynym przyjmowanym przez funkcję argumentem powinien być łańcuch znakowy reprezentujący zastępnik, którego chcemy użyć w innych funkcjach. 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"
;; bez odbierania specjalnego znaczenia (clojure.string/replace &#34;Kolor czerwony&#34; #&#34;(\w+) ([Cc]zerwony)&#34; &#34;$1 określony symbolem $2&#34;) ; =&gt; &#34;Kolor określony symbolem czerwony&#34; ;; z odbieraniem specjalnego znaczenia (clojure.string/replace &#34;Kolor czerwony&#34; #&#34;(\w+) ([Cc]zerwony)&#34; (str &#34;$1 określony symbolem&#34; (clojure.string/re-quote-replacement &#34; $1&#34;))) ; =&gt; &#34;Kolor określony symbolem $1&#34;

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ł."
(clojure.string/capitalize &#34;baobab tu był.&#34;) ; =&gt; &#34;Baobab tu był.&#34;

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"
(clojure.string/lower-case &#34;BAOBAB&#34;) ; =&gt; &#34;baobab&#34;

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"
(clojure.string/upper-case &#34;baobab&#34;) ; =&gt; &#34;BAOBAB&#34;

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 \\*"
(clojure.string/escape &#34;echo *&#34; { \* &#34;\\*&#34;, \; &#34;\\;&#34; }) ; =&gt; &#34;echo \\*&#34;

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"
(clojure.string/reverse &#34;nicraM&#34;) ; =&gt; &#34;Marcin&#34;

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ą jest 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"
(apply str (reverse &#34;nicraM&#34;)) ; =&gt; &#34;Marcin&#34;

Przycinanie łańcuchów, trim

Usuwanie białych znaków (w tym znaków nowej linii) z obu krańców ł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ł."
(clojure.string/trim &#34; Baobab tu był. &#34;) ; =&gt; &#34;Baobab tu był.&#34;

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ł.    "
(clojure.string/triml &#34; Baobab tu był. &#34;) ; =&gt; &#34;Baobab tu był. &#34;

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ł."
(clojure.string/trimr &#34; Baobab tu był. &#34;) ; =&gt; &#34; Baobab tu był.&#34;

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ł."
(clojure.string/trim-newline &#34;Baobab tu był.\n&#34;) ; =&gt; &#34;Baobab tu był.&#34;

Zamiana fragmentów tekstu, replace

Zamiana fragmentów łańcucha znakowego na inne realizowana jest przez funkcję replace, której pierwszym argumentem powinien być łańcuch znakowy, drugim wzorzec dopasowania (w postaci wyrażenia regularnego, tekstu bądź znaku), natomiast trzecim zastępnik umieszczany w miejscach pasujących do wzorca (tekst, znak bądź funkcja przekształcająca). Wartością zwracaną jest łańcuch tekstowy, w którym dopasowane do wzorca fragmenty (lub całość) zostały zastąpione odpowiednimi składnikami zastępnika (lub całą wartością zastępnika).

Użycie:

  • (clojure.string/replace wzorzec zastępnik);

gdzie wzorzec to:

  • łańcuch znakowy,
  • pojedynczy znak,
  • wyrażenie regularne;

zastępnik to:

  • łańcuch znakowy,
  • pojedynczy znak,
  • funkcja przekształcająca.

Możliwe są następujące kombinacje wzorców i zastępników:

  • ł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ć odpowiednie znaczniki, które zostaną zastąpione wartościami przechwyconymi podczas analizy tych wyrażeń.

Jeżeli wzorzec wyrazimy wyrażeniem regularnym, a zastępnik zdefiniujemy obiektem funkcyjnym, wtedy przekazywana jako ostatni argument funkcja musi przyjmować jeden argument i zwracać łańcuch znakowy. Jako argument przekazany jej będzie wektor dopasowań, przy czym jego pierwszy element (o indeksie 0) będzie zawierał zgrupowane wszystkie dopasowania do wzorca. Zwrócona przez funkcję wartość stanie się nowym łańcuchem znakowym, która w całości zastąpi wejściowy.

Przykłady użycia funkcji clojure.string/replace
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
;; wzorzec jako tekst, zastępnik jako tekst
(clojure.string/replace "Kolor czerwony" "czerwony" "niebieski")
; => "Kolor niebieski"

;; wzorzec jako tekst, zastępnik jako tekst
(clojure.string/replace "Litera A" \A \B)
; => "Litera B"

;; wzorzec jako wyrażenie regularne, zastępnik jako tekst
(clojure.string/replace "Kolor czerwony" #"\b(\w+ )(\w+)" "$1niebieski")
; => "Kolor niebieski"

;; wzorzec jako wyrażenie regularne, zastępnik jako funkcja
(clojure.string/replace "Kolor czerwony"
                        #"\b(\w+ )(\w+)"
                        #(str (%1 1) "niebieski"))
; => "Kolor niebieski"
;; wzorzec jako tekst, zastępnik jako tekst (clojure.string/replace &#34;Kolor czerwony&#34; &#34;czerwony&#34; &#34;niebieski&#34;) ; =&gt; &#34;Kolor niebieski&#34; ;; wzorzec jako tekst, zastępnik jako tekst (clojure.string/replace &#34;Litera A&#34; \A \B) ; =&gt; &#34;Litera B&#34; ;; wzorzec jako wyrażenie regularne, zastępnik jako tekst (clojure.string/replace &#34;Kolor czerwony&#34; #&#34;\b(\w+ )(\w+)&#34; &#34;$1niebieski&#34;) ; =&gt; &#34;Kolor niebieski&#34; ;; wzorzec jako wyrażenie regularne, zastępnik jako funkcja (clojure.string/replace &#34;Kolor czerwony&#34; #&#34;\b(\w+ )(\w+)&#34; #(str (%1 1) &#34;niebieski&#34;)) ; =&gt; &#34;Kolor niebieski&#34;

Zwróćmy uwagę, że w ostatnim przykładzie używamy literału funkcji anonimowej, w którym używamy funkcji str, aby złączyć dwa łańcuchy znakowe: pierwszy będący wynikiem użycia formy przeszukiwania wektora przekazanego jako pierwszy i jedyny argument, a drugi będący stałym napisem. S-wyrażenie (%1 1) jest więc wywołaniem umieszczonego na pierwszej pozycji wektora jako funkcji z argumentem 1, który oznacza drugi element do pobrania. W naszym przypadku jest nim napis Kolor umieszczony w wektorze właśnie na tej pozycji. Dla przypomnienia: pozycja wcześniejsza, o indeksie 0, zawiera cały dopasowany łańcuch znakowy bez podziału na grupy.

Zamiana pierwszego, 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.

Pierwszym argumentem powinien być łańcuch znakowy, drugim wzorzec dopasowania (w postaci wyrażenia regularnego, tekstu bądź znaku), natomiast trzecim zastępnik umieszczany w miejscach pasujących do wzorca (tekst, znak bądź funkcja przekształcająca). Wartością zwracaną jest łańcuch tekstowy, w którym dopasowane do wzorca fragmenty (lub całość) zostały zastąpione odpowiednimi składnikami zastępnika (lub całą wartością zastępnika).

Użycie:

  • (clojure.string/replace-first wzorzec zastępnik);

gdzie wzorzec to:

  • łańcuch znakowy,
  • pojedynczy znak,
  • wyrażenie regularne;

zastępnik to:

  • łańcuch znakowy,
  • pojedynczy znak,
  • funkcja przekształcająca.

Możliwe są następujące kombinacje wzorców i zastępników:

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

Przykład użycia funkcji replace-first
1
2
(clojure.string/replace-first "zamieni miejscami słowa raz dwa trzy"
                              #"(\w+)(\s+)(\w+)" "$3$2$1")
(clojure.string/replace-first &#34;zamieni miejscami słowa raz dwa trzy&#34; #&#34;(\w+)(\s+)(\w+)&#34; &#34;$3$2$1&#34;)

Zauważmy, że trzy ostatnie słowa z podanego napisu nie zostały zastąpione.

Łą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)  ; => ""
(subs &#34;Baobab&#34; 3) ; =&gt; &#34;bab&#34; (subs &#34;Baobab&#34; 2 5) ; =&gt; &#34;oba&#34; (subs &#34;Baobab&#34; 2 2) ; =&gt; &#34;&#34;

Łą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"
(str &#34;Pierwszy &#34; &#34;Drugi&#34; &#34; Trzeci&#34;) ; =&gt; &#34;Pierwszy Drugi Trzeci&#34;

Korzystając z funkcji str i traktując łańcuchy znakowe jak sekwencje znakowe, 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"
(apply str (interpose &#34;--&#34; [&#34;raz&#34; &#34;dwa&#34; &#34;trzy&#34;])) ; =&gt; &#34;raz--dwa--trzy&#34; (apply str (interpose \- [&#34;raz&#34; &#34;dwa&#34; &#34;trzy&#34;])) ; =&gt; &#34;raz-dwa-trzy&#34;

Łą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"
(clojure.string/join &#34; &#34; [&#34;raz&#34; &#34;dwa&#34;]) ; =&gt; &#34;raz dwa&#34; (clojure.string/join &#34;&#34; [&#34;Bao&#34; &#34;bab&#34;]) ; =&gt; &#34;Baobab&#34; (clojure.string/join \space [&#34;Bao&#34; &#34;bab&#34;]) ; =&gt; &#34;Bao bab&#34; (clojure.string/join (seq &#39;(B a o b a b))) ; =&gt; &#34;Baobab&#34; (clojure.string/join &#34;, &#34; [1 2 3 4]) ; =&gt; &#34;1, 2, 3, 4&#34;

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 wyrażenie regularne, 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."]
(clojure.string/split &#34;Baobab tu był.&#34; #&#34; &#34;) ; =&gt; [&#34;Baobab&#34; &#34;tu&#34; &#34;był.&#34;] (clojure.string/split &#34;B123a09o2b1a55b322 1t4u 90b8y42ł3.&#34; #&#34;\d+&#34;) ; =&gt; [&#34;B&#34; &#34;a&#34; &#34;o&#34; &#34;b&#34; &#34;a&#34; &#34;b&#34; &#34; &#34; &#34;t&#34; &#34;u &#34; &#34;b&#34; &#34;y&#34; &#34;ł&#34; &#34;.&#34;] (clojure.string/split &#34;B123a09o2b1a55b322 1t4u 90b8y42ł3.&#34; #&#34;\d+&#34; 7) ; =&gt; [&#34;B&#34; &#34;a&#34; &#34;o&#34; &#34;b&#34; &#34;a&#34; &#34;b&#34; &#34; 1t4u 90b8y42ł3.&#34;]

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ł." ]
(clojure.string/split-lines &#34;Baobab\ntu\nbył.&#34;) ; =&gt; [ &#34;Baobab&#34; &#34;tu&#34; &#34;był.&#34; ]

Dzielenie sekwencyjne, re-seq

Podziału łańcuchów znakowych na mniejsze części można też dokonać w sposób sekwencyjny z wykorzystaniem funkcji re-seq lub przez potraktowanie łańcucha jako sekwencji znakowej.

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})            ; sekwencje sekwencji znaków
     (map #(apply str %))                  ; - dla każdej łączymy znaki
     (drop-while #(= \newline (first %)))  ; - odrzucamy pocz. nowe linie
     (take-nth 2))                         ; - pobieramy co drugi
; => ("Dzielenie na linie" "Druga")
(def tekst &#34;\n\nDzielenie na linie\nDruga&#34;) ;; sekwencja na podstawie tekstu i wyrażenia regularnego (re-seq #&#34;(?s)[^\n]+&#34; tekst) ; =&gt; (&#34;Dzielenie na linie&#34; &#34;Druga&#34;) ;; sekwencyjny podział na znaki i dzielenie na linie (-&gt;&gt; tekst ; dla tekstu (partition-by #{\newline}) ; sekwencje sekwencji znaków (map #(apply str %)) ; - dla każdej łączymy znaki (drop-while #(= \newline (first %))) ; - odrzucamy pocz. nowe linie (take-nth 2)) ; - pobieramy co drugi ; =&gt; (&#34;Dzielenie na linie&#34; &#34;Druga&#34;)

Ostatni przykład wymaga kilku wyjaśnień, bo zawiera 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 formy 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 poniższym 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]
;; wersja bez makra -&gt;&gt; (vec (map inc (take-nth 3 (vector 1 2 3 4 5 6 7 8 9 10)))) ; =&gt; [2 5 8 11] ;; wersja z makrem -&gt;&gt; (-&gt;&gt; (vector 1 2 3 4 5 6 7 8 9 10) (map inc) (take-nth 3) (vec)) ; =&gt; [2 5 8 11]

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

Wiemy już w jaki sposób 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"
    &#34;\n\nDzielenie na linie\nDruga&#34;

  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) )
    ( (\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")
    (&#34;\n\n&#34; &#34;Dzielenie na linie&#34; &#34;\n&#34; &#34;Druga&#34;)

    Funkcja map przyjmie sekwencję sekwencji i dla każdej z tych pierwszych (czyli dla wydzielonych linii i znaków nowej linii przedstawionych jako sekwencje znakowe) 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")
    (&#34;Dzielenie na linie&#34; &#34;\n&#34; &#34;Druga&#34;)

    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")
    (&#34;Dzielenie na linie&#34; &#34;Druga&#34;)

    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
(string? &#34;Baobab&#34;) ; =&gt; true (string? 7) ; =&gt; false

Sprawdzanie czy jałowy, blank?

Sprawdzanie czy łańcuch znakowy jest jałowy (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
(clojure.string/blank? &#34;&#34;) ; =&gt; true (clojure.string/blank? &#34; &#34;) ; =&gt; true (clojure.string/blank? nil) ; =&gt; true (clojure.string/blank? &#34;Baobab&#34;) ; =&gt; false

Sprawdzanie czy 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
(empty? &#34;&#34;) ; =&gt; 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”:

Jesteś w sekcji

comments powered by Disqus