stats

Poczytaj mi Clojure, cz. 6

Powiązania i przestrzenie nazw

Grafika

Powiązania pozwalają nazwom z kodu źródłowego oznaczać wartości i obiekty referencyjne, a przestrzenie nazw organizują Vary najwyższego poziomu, klasy Javy i aliasy. W tym odcinku dowiemy się, jak mechanizmy te kształtują rozpoznawanie nazw, zasięg i widoczność w Clojure.

Powiązania i przestrzenie nazw

Istotnym elementem programowania komputerów jest zarządzanie identyfikatorami, czyli zrozumiałymi dla człowieka etykietami, które pozwalają odwoływać się do obiektów umieszczonych w pamięci.

Powiązania

Powiązanie (ang. binding) kojarzy identyfikator z wartością albo z obiektem, przez który można tę wartość uzyskać. Proces ustanawiania takiej relacji nazywamy wiązaniem nazw (ang. name binding). Na przykład lokalna nazwa a może oznaczać niezmienną wartość 123. Symbol użyty jako nazwa w przestrzeni nazw może natomiast zostać rozpoznany jako obiekt Var, którego bieżące powiązanie zawiera tę wartość.

Przykłady powiązań nazw w Clojure
1(def a 123)     ; powiązanie globalne
2(let [a 123] a) ; powiązanie leksykalne
(def a 123) ; powiązanie globalne (let [a 123] a) ; powiązanie leksykalne

Dzięki powiązaniom nazw możemy w kodzie źródłowym programów komputerowych odwoływać się do umieszczonych w pamięci danych z użyciem czytelnych identyfikatorów zamiast np. pamięciowych adresów. Poza tym otwierają one drogę do abstrahowania struktur danych, tzn. uniezależniania dostępu do nich od sprzętowej architektury.

Powiązania oddzielają znaczenie programu od sposobu rozmieszczenia danych w pamięci. Powiązanie leksykalne jest niezmienne, natomiast stabilna tożsamość referencyjna może zgodnie ze swoim kontraktem udostępniać z czasem różne wartości. Temat ten jest szerzej omówiony w rozdziale XIII.

Przykłady wiązania nazw z wartościami mogą kojarzyć się z operowaniem na zmiennych, jednak wytwarzanie asocjacji wartości z identyfikatorem niekoniecznie oznacza, że zawartość obszaru pamięci, w którym znajduje się wartość będzie można zmieniać. Wiązanie nazw jest więc czymś bardziej elementarnym niż tworzenie zmiennych i może być jego etapem.

Wiązanie wczesnewiązanie późne opisują moment rozpoznania nazwy lub operacji. Zasięg leksykalny i dynamiczny opisuje natomiast miejsce, z którego wybierane jest powiązanie. Są to niezależne osie i nie należy ujmować ich wspólnym, przeciążonym rozróżnieniem „statyczne–dynamiczne”. W Clojure analiza kompilatora rozpoznaje nazwy leksykalne i Vary, ale dereferencja Vara oraz dyspozycja protokołu czy multimetody mogą nadal wybierać wartość lub implementację podczas działania programu.

Inżynieria oprogramowania wyróżnia dwie ważne właściwości, które decydują o tym jak i gdzie możemy korzystać z powiązań i wskazywanych nimi wartości:

  • zasięg (ang. scope),
  • widoczność (ang. visibility).

Zobacz także:

Zasięg

Zasięgiem powiązania (identyfikatora z pamięciowym obiektem) nazwiemy część programu, w której możemy skorzystać z takiego powiązania, aby odwołać się do obiektu używając przypisanej mu nazwy.

Częścią programu będzie najczęściej leksykalny fragment kodu źródłowego (np. blok, moduł, funkcja, plik z kodem źródłowym, itp.), ale może być też ona rozpatrywana dynamicznie i zależeć od stanu wykonywania się programu w danym punkcie czasu.

Zasięg wyznaczany strukturą kodu źródłowego nazywamy leksykalnym. Dynamiczne powiązanie Vara w Clojure wybierane jest natomiast ze stosu powiązań bieżącego wątku, zgodnie z kontekstem wywołań. Funkcja może więc odwołać się do dynamicznego Vara zdefiniowanego gdzie indziej i zobaczyć najbardziej wewnętrzne aktywne powiązanie ustanowione przez wywołującego. Domknięcia przechwytują z kolei powiązania leksykalne; nie oznacza to jednak zamrożenia wszystkich przechwyconych obiektów w chwili definiowania funkcji.

W Clojure obsługiwane są następujące rodzaje zasięgów powiązań:

Przykłady zasięgów powiązań nazw w Clojure
 1;; zasięg leksykalny
 2;; ograniczony ciałem formy let
 3
 4(let [a 123]  ; powiązanie symbolu z wartością
 5  a)          ; użycie wartości identyfikowanej symbolem
 6
 7;; definicja na poziomie przestrzeni nazw
 8
 9(def a 123)            ; powiązanie zmiennej globalnej z wartością
10a                      ; użycie wartości bieżącej identyfikowanej symbolem
11
12;; zasięg dynamiczny
13
14(def ^:dynamic *a* 456)  ; powiązanie główne dynamicznego Vara
15*a*                      ; użycie wartości bieżącej
16(binding [*a* 789]       ; ustanowienie tymczasowego powiązania wątku
17  *a*)                   ; użycie wartości wybranej dynamicznie
;; zasięg leksykalny ;; ograniczony ciałem formy let (let [a 123] ; powiązanie symbolu z wartością a) ; użycie wartości identyfikowanej symbolem ;; definicja na poziomie przestrzeni nazw (def a 123) ; powiązanie zmiennej globalnej z wartością a ; użycie wartości bieżącej identyfikowanej symbolem ;; zasięg dynamiczny (def ^:dynamic *a* 456) ; powiązanie główne dynamicznego Vara *a* ; użycie wartości bieżącej (binding [*a* 789] ; ustanowienie tymczasowego powiązania wątku *a*) ; użycie wartości wybranej dynamicznie

Widoczność

Widocznością nazwiemy część programu, w której możemy uzyskać dostęp i korzystać z powiązanej z identyfikatorem wartości przechowywanej w pamięciowej strukturze. Powiemy więc, że (zależnie od języka) widoczna jest: zmienna, stała, wartość lub inna konstrukcja odpowiedzialna za przechowywanie danych.

Przykładem różnicy między widocznością a zasięgiem jest sytuacja, w której pamięciowy obiekt (np. liczba całkowita 2) identyfikowany symboliczną nazwą (np. x) w pewnym obszarze leksykalnym (np. wewnątrz definicji funkcji) przestaje być widoczny, ponieważ nazwa x została użyta jako parametr funkcji, którego zadaniem jest odwoływanie się do wartości przekazywanego argumentu wywołania. Wartość 2 nie znika, ale jej identyfikacja jest tymczasowo przesłaniana w obszarze definicji funkcji. Gdy definiujący funkcję fragment kodu źródłowego się zakończy x nadal będzie odnosić się do wartości 2. Powiemy więc o zasięgu x (lub powiązania x z wartością), ale o widoczności wartości 2 powiązanej z x.

Widoczność identyfikowanego obiektu nie będzie nigdy większa, niż zasięg powiązania, ale zasięg powiązania może być większy, niż widoczność – tak jak np. w powyższym przykładzie.

W Clojure widocznością możemy sterować, korzystając z przestrzeni nazw i określając globalne identyfikatory jako prywatne bądź publiczne, natomiast zasięg zależy od rodzaju użytych konstrukcji i od kontekstu.

Przestrzenie nazw

W Clojure funkcjonuje mechanizm przestrzeni nazw (ang. namespace). Dzięki niemu można organizować poszczególne elementy programów i separować symboliczne identyfikatory, które pochodzą z różnych źródeł, aby unikać konfliktów nazewnictwa.

Przestrzeń nazw w Clojure jest obiektem o unikatowej nazwie, który zarządza odwzorowaniami symboli na Vary i klasy Javy oraz aliasami innych przestrzeni nazw. Stanowi kontekst rozpoznawania nazw, a nie zwykłą mapę ani kontener przechowujący same wartości.

Definicje najwyższego poziomu korzystają z przestrzeni nazw, ponieważ ich symbole są w nich internalizowane jako odwzorowania na Vary. Powiązania leksykalne są zaś reprezentowane w otoczeniu leksykalnym kompilatora i nie stają się odwzorowaniami przestrzeni nazw.

Przestrzeń bieżąca

Skąd kompilator czerpie wiedzę na temat tego, o jaką przestrzeń chodzi? Po pierwsze może korzystać z aktualnie ustawionej przestrzeni bieżącej. Gdy spojrzymy na plik z kodem źródłowym dowolnego programu, zobaczymy na jego wstępie zapis podobny do poniższego:

(ns smangler.api
  (:require [clojure.string :as    string]
            [clojure.set    :refer   :all]))
(ns smangler.api (:require [clojure.string :as string] [clojure.set :refer :all]))

Mówi on kompilatorowi, że podczas dalszego wczytywania kodu źródłowego dynamiczny Var *ns* ma mieć wartość obiektu typu clojure.lang.Namespace. Jeżeli przestrzeń o podanej nazwie (w tym przykładzie smangler.api) jeszcze nie istnieje, zostanie utworzona. Analizowane następnie definicje najwyższego poziomu będą w tym kontekście internalizowane jako Vary tej przestrzeni, o ile jawnie nie wskazują innego dopuszczalnego kontekstu.

Poniższy zapis sprawi, że w przestrzeni bieżącej (np. smangler.api) pojawi się powiązanie symbolu a z obiektem typu Var odwołującym się do wartości 1:

(def a 1)
(def a 1)

Podczas analizy niekwalifikowany symbol, który nie jest formą specjalną ani nazwą leksykalną, rozpoznawany jest przez odwzorowania bieżącej przestrzeni nazw. Symbole kwalifikowane, nazwy klas i elementy środowiska Javy podlegają odrębnym regułom rozpoznawania.

Makro ns obsługuje sporo dodatkowych opcji, jednak :require jest najczęściej wykorzystywaną. Sprawia ona, że w przestrzeni powstają tzw. aliasy, które pozwalają odwoływać się do innych przestrzeni (opcja :as). Można go również używać do umieszczania w bieżącej przestrzeni powiązań pochodzących z innej, aby nie trzeba było poprzedzać symboli jej nazwą (opcja :refer).

Widzimy więc, że powołując do istnienia funkcje czy zmienne globalne w Clojure, programista musi wybrać, w jakiej przestrzeni będą rezydowały ich symboliczne identyfikatory. Jest potem w stanie odwoływać się do dwóch różnych konstruktów o takich samych nazwach, lecz zarejestrowanych w różnych przestrzeniach, np. funkcji sumuj z przestrzeni lata-świetlne i funkcji sumuj z przestrzeni liczby-wymierne (z użyciem zapisu lata-świetlne/sumujliczby-wymierne/sumuj). Może też zdecydować, że niektóre powiązania pochodzące z innych przestrzeni mają być odniesione w przestrzeni bieżącej, jeżeli ich nazwy są wystarczająco unikatowe, a nawet dokonać przemianowania.

Symbole kwalifikowane przestrzenią nazw

Do globalnych identyfikatorów umiejscowionych w przestrzeniach nazw możemy odwoływać się również z użyciem specjalnej formy symbolowej, w której poza właściwą nazwą identyfikującą znajdziemy też nazwę przestrzeni, np.:

(clojure.core/println 1 2 3)
(clojure.core/println 1 2 3)

Powyższy zapis oznacza, że chcemy wywołać funkcję identyfikowaną nazwą println z przestrzeni nazw clojure.core. Aby odnaleźć podprogram funkcji, kompilator użyje określonej przez nas przestrzeni zamiast korzystać z tej wskazywanej wartością bieżącą zmiennej *ns*.

Budowa przestrzeni

W bardziej usystematyzowany sposób scharakteryzujemy przestrzeń nazw jako obiekt (typu clojure.lang.Namespace):

  • służący do utrzymywania:

    • odwzorowań identyfikatorów na globalne Vary,
    • odwzorowań identyfikatorów na klasy Javy,
    • aliasów odwołujących się do innych przestrzeni nazw;
  • zawierający dwie asocjacyjne struktury:

    • mapę aliasów,
    • mapę odwzorowań;
  • rejestrowany w globalnym repozytorium w momencie wytworzenia, aby mechanizmy języka mogły z niego skorzystać podczas automatycznej zamiany symbolicznych identyfikatorów na odpowiadające im pamięciowe obiekty skrywające wartości.

Funkcją obecnej w przestrzeni nazw mapy aliasów jest tworzenie odwołań do innych przestrzeni nazw z użyciem przypisanych im alternatywnych identyfikatorów w postaci symboli.

Mapa aliasów w Clojure

Mapa aliasów przestrzeni nazw przyporządkowuje symbole do obiektów innych przestrzeni

Zamiast poprzedzać każdą nazwę symbolu długą nazwą przestrzeni (np. io.randomseed.blog.kalkulator/dodaj) możemy w przestrzeni bieżącej wytworzyć jej alias (np. kalkulator) i podczas odwoływania się do identyfikowanych wartości bądź funkcji używać skróconej wersji (np. kalkulator/dodaj).

Mapa odwzorowań zawiera pary elementów, w których kluczami są również symbole, a skojarzonymi z nimi wartościami mogą być:

  • obiekty typu Var,
  • obiekty Class reprezentujące klasy Javy.

Warto zauważyć, że obiekt typu Var umieszczony w przestrzeni nazw nazywamy zmienną globalną. Zmiennych globalnych w Clojure używa się do identyfikowania informacji konfiguracyjnych bądź podprogramów (np. funkcji czy makr).

Mapa odwzorowań w Clojure

Mapa odwzorowań przestrzeni nazw przyporządkowuje symbole do obiektów typu Var lub klas Javy

Wśród wartości znajdujących się w mapie odwzorowań możemy znaleźć nie tylko Vary stworzone i przypisane do tej samej przestrzeni, ale też obiekty znajdujące się w innych przestrzeniach.

Odwzorowania na obiekty typu Var pochodzące z innych przestrzeni tworzy się z użyciem funkcji refer lub use, odwzorowania na klasy Javy z użyciem import, natomiast nowe zmienne globalne odwzorowywane są automatycznie w chwili ich stwarzania (np. z użyciem def czy intern).

Zmienne publiczne i prywatne

Internalizowany Var może mieć metadaną prywatności (ang. private). Operacje odnoszące publiczne Vary pomijają wtedy taki obiekt, a kompilator odrzuca zwykły dostęp z innej przestrzeni nazw. Prywatność jest cechą metadanych Vara, a nie odrębnym rodzajem wpisu w mapie przestrzeni. W praktyce:

  • Będzie można się do nich odwoływać z wnętrza funkcji, które zdefiniowano w tej samej przestrzeni.

  • Będzie można się do nich odwoływać wtedy, gdy bieżącą, ustawioną przestrzenią nazw jest ta, do której przynależą. Jest to dodatkowy sposób kontrolowania widoczności.

Inicjowanie przestrzeni nazw

Gdy przygotowywane jest środowisko uruchomieniowe języka Clojure, inicjalizowana jest przestrzeń nazw clojure.core. Symbol *ns* identyfikuje globalną zmienną dynamiczną zdefiniowaną w clojure.core, a jej bieżąca wartość wskazuje obiekt bieżącej przestrzeni nazw. W sesji REPL wartość ta zwykle wskazuje przestrzeń user.

Dla każdej publicznej zmiennej globalnej z clojure.core tworzone są odniesienia w przestrzeni bieżącej, dzięki czemu funkcje i makra rdzenia języka są dostępne bez konieczności podawania pełnych nazw. Formy specjalne są obsługiwane bezpośrednio przez kompilator i nie są przechowywane jako odwzorowania przestrzeni nazw.

Wśród automatycznie dodawanych odniesień znajdziemy między innymi takie, które identyfikują poniższe funkcje:

  • in-ns – służy do ustawiania bieżącej przestrzeni nazw,

  • import – makro przypisujące nazwy wybranych klas Javy do identyfikatorów w bieżącej przestrzeni,

  • refer – przypisuje w bieżącej przestrzeni symbole do obiektów Var z innej przestrzeni.

Dzięki temu programista może dalej rozszerzać zawartość przestrzeni nazw o nowe przyporządkowania.

Przedstawione dalej funkcje pozwalają na mniej lub bardziej ogólny dostęp do przestrzeni nazw. W praktyce jednak rzadko używa się ich bezpośrednio, ale korzysta z makra ns.

Tworzenie przestrzeni nazw

Aby skorzystać z danej przestrzeni nazw, trzeba najpierw ją utworzyć. Niektóre przestrzenie powstają automatycznie, np. clojure.core czy user (gdy używamy REPL). Gdybyśmy jednak chcieli samodzielnie utworzyć przestrzeń nazw, na przykład w celu hermetyzacji tworzonej przez nas biblioteki bądź lepszego zarządzania widocznością w aplikacji, możemy skorzystać z odpowiedniej funkcji.

Tworzenie przestrzeni, create-ns

Funkcja create-ns przyjmuje nazwę przestrzeni nazw w postaci literalnego symbolu i tworzy podaną przestrzeń, jeżeli ta jeszcze nie istnieje. W przypadku, gdy przestrzeń o podanej nazwie już została utworzona, nie jest podejmowana żadna czynność.

Użycie:

  • (create-ns symboliczna-nazwa).

Funkcja przyjmuje nazwę przestrzeni wyrażoną wartością symbolu, a zwraca obiekt przestrzeni nazw (typu clojure.lang.Namespace). W kodzie źródłowym symbol ten zwykle cytujemy, aby został przekazany jako dane, a nie poddany rozpoznaniu.

Przykład tworzenia przestrzeni nazw z użyciem funkcji create-ns
1(create-ns 'nowa)
2; => #<Namespace nowa>
(create-ns &#39;nowa) ; =&gt; #&lt;Namespace nowa&gt;

Zobacz także:

Używanie przestrzeni nazw

Korzystanie z przestrzeni nazw polega na rozpoznawaniu form symbolowych jako odwzorowanych Varów lub klas Javy. Semantyka rozpoznawania przez kompilator jest tam, gdzie ma to zastosowanie, równoważna publicznym operacjom resolvens-resolve, ale kompilator nie musi podczas kompilacji dosłownie wywoływać ich Varów jak zwykłych funkcji.

Przypomnijmy sobie, że symbole mogą mieć dookreśloną przestrzeń nazw lub nie zawierać informacji o przestrzeni. Właściwość ta pozwala używać ich w dwojaki sposób i przekłada się na różne tryby odwoływania się do przestrzeni nazw.

Gdy podamy symbol i nie określimy w nim przestrzeni nazw (np. replace), to zostanie przeszukana bieżąca przestrzeń nazw. Jeżeli natomiast podamy w symbolu przestrzeń (np. clojure.string/replace), to będzie przeszukana określona przestrzeń nazw.

Rozpoznanie daje odwzorowany Var albo klasę Javy. Wartościowanie symbolu rozpoznanego jako Var pobiera jego bieżące powiązanie; bezpośrednie wywołanie resolve zwraca zaś sam obiekt Var.

Ustawianie przestrzeni bieżącej, in-ns

Bieżąca przestrzeń nazw jest przechowywana przez dynamiczny Var *ns*. W pliku źródłowym lub kontekście REPL możemy więc przełączyć przestrzeń, zmieniając jego bieżące powiązanie wątku. Częstą praktyką jest ustawianie jej na początku pliku, którego definicje funkcji, makr i globalnych Varów będą podczas wczytywania domyślnie rejestrowane w wybranej przestrzeni.

Aby nie zastanawiać się nad sposobem przechowywania informacji o bieżącej przestrzeni, a więc nad metodami operowania na niej, możemy skorzystać z gotowej funkcji przeznaczonej do przełączania przestrzeni bieżącej. Nazywa się ona in-ns.

Użycie:

  • (in-ns symboliczna-nazwa).

Funkcja przyjmuje jeden argument, który powinien być zacytowanym symbolem. Jeżeli określona nim przestrzeń nazw jeszcze nie istnieje, będzie wywołana funkcja create-ns, aby ją utworzyć. Funkcja ustawia bieżące powiązanie *ns* i zwraca obiekt przestrzeni.

Przykład przełączania bieżącej przestrzeni nazw z użyciem funkcji in-ns
1(in-ns 'nowa)
2; => #<Namespace nowa>
(in-ns &#39;nowa) ; =&gt; #&lt;Namespace nowa&gt;

Po wykonaniu powyższego kodu w REPL możemy zdziwić się, że kompilator nie „widzi” funkcji języka, które wcześniej były osiągalne przez podanie ich symbolicznych identyfikatorów. Dzieje się tak dlatego, że do nowo utworzonej przestrzeni nie odnieśliśmy powiązań symboli obecnych w przestrzeni user, z której domyślnie korzysta REPL.

Żeby korzystać z nazw powiązanych ze zmiennymi globalnymi z innej przestrzeni, należy skorzystać z (omówionej dalej) funkcji refer, która wytworzy odpowiednie odniesienia. Istnieje też wygodne makro ns, które zostanie omówione później.

Ustalanie nazwy przestrzeni, namespace

Funkcja namespace zwraca składnik przestrzeni nazw symbolu albo słowa kluczowego.

Użycie:

  • (namespace symboliczna-nazwa).

Funkcja ta nie dokonuje rozpoznawania w przestrzeni nazw, lecz odczytuje składnik zapisany w podanej nazwanej wartości.

Zwracaną wartością jest łańcuch tekstowy lub nil, jeżeli mamy do czynienia z niekwalifikowaną wartością.

Przykład użycia funkcji namespace
1(namespace 'przestrzeń/jakaś-nazwa)
2; => "przestrzeń"
3
4(namespace :przykład/klucz)
5; => "przykład"
(namespace &#39;przestrzeń/jakaś-nazwa) ; =&gt; &#34;przestrzeń&#34; (namespace :przykład/klucz) ; =&gt; &#34;przykład&#34;

Rozpoznawanie po nazwach, ns-resolve

Funkcja ns-resolve rozpoznaje symbol względem przestrzeni nazw podanej jako pierwszy argument (symbol lub obiekt przestrzeni). Zwraca odwzorowany Var albo obiekt Class, a nie bieżącą wartość Vara; gdy odwzorowania nie znaleziono, zwraca nil.

Przeszukiwana przestrzeń nazw musi istnieć, a jeżeli nie istnieje, zgłoszony zostanie wyjątek.

W wariancie trójargumentowym funkcja jako drugi argument przyjmuje nazwę tzw. otoczenia (ang. environment). Może to być dowolna kolekcja, na której można wywołać funkcję contains? (np. zbiór lub mapa). Jeżeli podany symbol zostanie w środowisku znaleziony, nie będzie przeszukiwana przestrzeń nazw, a funkcja zwróci wartość nil.

Użycie:

  • (ns-resolve przestrzeń-nazw            symboliczna-nazwa),
  • (ns-resolve przestrzeń-nazw środowisko symboliczna-nazwa).
Przykłady użycia funkcji ns-resolve
1(ns-resolve 'user 'replace)                  ; => #'clojure.core/replace
2(ns-resolve *ns*  'replace)                  ; => #'clojure.core/replace
3(ns-resolve 'clojure.string 'replace)        ; => #'clojure.string/replace
4(ns-resolve 'clojure.string 'cośtam)         ; => nil
5(ns-resolve *ns* #{'replace 'coś} 'replace)  ; => nil
(ns-resolve &#39;user &#39;replace) ; =&gt; #&#39;clojure.core/replace (ns-resolve *ns* &#39;replace) ; =&gt; #&#39;clojure.core/replace (ns-resolve &#39;clojure.string &#39;replace) ; =&gt; #&#39;clojure.string/replace (ns-resolve &#39;clojure.string &#39;cośtam) ; =&gt; nil (ns-resolve *ns* #{&#39;replace &#39;coś} &#39;replace) ; =&gt; nil

Rozpoznawanie w bieżącej, resolve

Funkcja resolve jest mniej wymagającą wersją ns-resolve. Nie trzeba jej podawać nazwy przestrzeni nazw, ponieważ domyślnie korzysta ona z bieżącej (określonej przez *ns*). Symbol kwalifikowany przestrzenią kieruje rozpoznawanie do nazwanej lub wskazanej aliasem przestrzeni. Wartością zwracaną może być Var, klasa Javy albo nil.

Użycie:

  • (resolve            symboliczna-nazwa),
  • (resolve środowisko symboliczna-nazwa).
Przykłady użycia funkcji resolve
 1(resolve 'replace)
 2; => #'clojure.core/replace
 3
 4;; podajemy środowisko zawierające symbol
 5
 6(resolve #{'replace} 'replace)
 7; => nil
 8
 9;; podajemy symbol z dookreśloną przestrzenią nazw
10
11(resolve 'clojure.string/replace)
12; => #'clojure.string/replace
(resolve &#39;replace) ; =&gt; #&#39;clojure.core/replace ;; podajemy środowisko zawierające symbol (resolve #{&#39;replace} &#39;replace) ; =&gt; nil ;; podajemy symbol z dookreśloną przestrzenią nazw (resolve &#39;clojure.string/replace) ; =&gt; #&#39;clojure.string/replace

Zarządzanie przestrzeniami

Dzięki funkcjom all-nsfind-ns możemy wyszukiwać przestrzenie nazw i pobierać ich listy.

Pobieranie listy przestrzeni, all-ns

Funkcja all-ns nie przyjmuje żadnych argumentów, a zwraca sekwencję wszystkich aktualnie zarejestrowanych obiektów przestrzeni nazw. Konkretna klasa sekwencji jest szczegółem implementacyjnym.

Użycie:

  • (all-ns).
Przykład użycia funkcji all-ns
1(all-ns)
2; => (#<Namespace reply.main> #<Namespace clojure.tools.nrepl.misc> … )
(all-ns) ; =&gt; (#&lt;Namespace reply.main&gt; #&lt;Namespace clojure.tools.nrepl.misc&gt; … )

Wyszukiwanie przestrzeni, find-ns

Funkcja find-ns zwraca obiekt przestrzeni, której nazwa określona symbolem w formie stałej została podana jako jej pierwszy argument. Jeżeli przestrzeń o podanej nazwie nie istnieje, zwracana jest wartość nil.

Użycie:

  • (find-ns nazwa-przestrzeni).
Przykład użycia funkcji find-ns
1(find-ns 'user)
2; => #<Namespace user>
(find-ns &#39;user) ; =&gt; #&lt;Namespace user&gt;

Usuwanie przestrzeni, remove-ns

Przestrzenie nazw można nie tylko dodawać, ale też usuwać. Operacja ta jest możliwa z użyciem funkcji remove-ns. Przyjmuje ona jeden argument, który jest nazwą przestrzeni do usunięcia wyrażoną symbolem w formie stałej.

Funkcja zwraca wartość nil, gdy przestrzeń nie istnieje lub obiekt przestrzeni, gdy dokonano usunięcia. Nie można jej użyć do pozbycia się przestrzeni nazw clojure.core.

Użycie:

  • (remove-ns nazwa-przestrzeni).
Przykłady użycia funkcji remove-ns
1(create-ns 'tymczasowa.przestrzeń)
2(some? (remove-ns 'tymczasowa.przestrzeń))
3; => true
(create-ns &#39;tymczasowa.przestrzeń) (some? (remove-ns &#39;tymczasowa.przestrzeń)) ; =&gt; true

Zarządzanie odwzorowaniami

Powiązania symboli ze zmiennymi globalnymi lub obiektami Javy można dodawać do przestrzeni nazw, używając jednej z kilku przeznaczonych do tego celu funkcji.

Dodawanie odniesień do zmiennych, refer

Funkcja refer umożliwia dodawanie w bieżącej przestrzeni nazw odniesień do obiektów typu Var umieszczonych w innych przestrzeniach. Jako pierwszy argument przyjmuje ona nazwę źródłowej przestrzeni nazw w postaci formy stałej symbolu, a jako kolejne, opcjonalne argumenty, filtry, które zostaną użyte, aby uszczegółowić przeprowadzane operacje.

Funkcja zwraca nil, a w przypadku nieistniejącej przestrzeni nazw generowany jest wyjątek.

Użycie:

  • (refer nazwa-przestrzeni & filtry).

W efekcie wywołania refer z podanej przestrzeni nazw zostaną pobrane wszystkie symbole wskazujące na obiekty typu Var, a następnie w bieżącej przestrzeni utworzone będą odniesienia o takich samych symbolicznych nazwach, chyba że użyto odpowiednich filtrów.

W przypadku, gdy tworzone odwołanie ma taką samą symboliczną nazwę, jak już istniejące przyporządkowanie, zostanie ono nadpisane, a na standardowe wyjście diagnostyczne wysłane będzie stosowne ostrzeżenie.

W bieżącej przestrzeni nazw symbolom będą bezpośrednio przyporządkowane obiekty Var rezydujące w źródłowej przestrzeni. Gdy w oryginalnej przestrzeni powiązanie zostanie usunięte (np. z użyciem ns-unmap), bieżąca przestrzeń nadal będzie zawierała przyporządkowanie symbolu do obiektu typu Var.

Możliwe do zastosowania filtry to:

  • :exclude sekwencja-symboli – symbole do pominięcia,
  • :only    sekwencja-symboli – symbole do wyłącznego przetworzenia,
  • :rename       mapa-symboli – symbole do przemianowania.

Filtry te powinny składać się z nazwy w formie słowa kluczowego, po której następuje sekwencja symboli, określająca wartości filtra, którymi będą symboliczne nazwy do uwzględnienia (:only) lub pominięcia (:exclude).

Sekwencjami mogą być dowolne kolekcje (np. wektory) wyposażone w sekwencyjny interfejs dostępu. Jeżeli zażądano przemianowania (:rename), to zamiast sekwencji należy podać mapę zawierającą pary przekształceń. W ten sposób do danej zmiennej globalnej będzie można odwoływać się w bieżącej przestrzeni pod inną symboliczną nazwą.

Przykłady użycia funkcji refer
 1;; stwórz odniesienia do wszystkich Varów z clojure.string
 2
 3(refer 'clojure.string)
 4; => nil
 5
 6;; stwórz odniesienie tylko do identyfikatora 'replace'
 7
 8(refer 'clojure.string :only '[replace])
 9; => nil
10
11;; stwórz odniesienia do wszystkich Varów z clojure.string
12;; za wyjątkiem 'replace' i 'reverse'
13
14(refer 'clojure.string :exclude '[replace reverse])
15; => nil
16
17;; stwórz odniesienia do wszystkich Varów z clojure.string
18;; za wyjątkiem 'replace', a 'reverse' zamień na 'nazad'
19
20(refer 'clojure.string
21       :exclude '[replace]
22       :rename  '{reverse nazad})
23; => nil
;; stwórz odniesienia do wszystkich Varów z clojure.string (refer &#39;clojure.string) ; =&gt; nil ;; stwórz odniesienie tylko do identyfikatora &#39;replace&#39; (refer &#39;clojure.string :only &#39;[replace]) ; =&gt; nil ;; stwórz odniesienia do wszystkich Varów z clojure.string ;; za wyjątkiem &#39;replace&#39; i &#39;reverse&#39; (refer &#39;clojure.string :exclude &#39;[replace reverse]) ; =&gt; nil ;; stwórz odniesienia do wszystkich Varów z clojure.string ;; za wyjątkiem &#39;replace&#39;, a &#39;reverse&#39; zamień na &#39;nazad&#39; (refer &#39;clojure.string :exclude &#39;[replace] :rename &#39;{reverse nazad}) ; =&gt; nil

Zobacz także:

Dodawanie odniesień Clojure, refer-clojure

Wariantem funkcji refer jest makro refer-clojure, które można traktować jak lukier składniowy, ponieważ wywołuje refer z przekazywaną wartością pierwszego argumentu ustawioną na 'clojure.core.

refer-clojure często skorzystamy, gdy będziemy chcieli zrezygnować z automatycznego tworzenia odniesień do wszystkich funkcji i makr zdefiniowanych w głównej bibliotece języka Clojure, a wybrać tylko te, z których w danym pliku źródłowym korzystamy. Zazwyczaj będzie ono wtedy wywołane pośrednio, przez jedną z klauzul makra ns.

Kiedy możemy tego potrzebować? Na przykład wtedy, gdy nasza biblioteka definiuje we własnej przestrzeni funkcje bądź makra o takich samych nazwach, jak w clojure.core. Unikamy wtedy ostrzeżeń o przesłanianiu istniejących identyfikatorów.

Makro zwraca nil.

Użycie:

  • (refer-clojure & filtry).

W efekcie ewaluacji kodu produkowanego przez makro w bieżącej przestrzeni nazw zostaną dodane odniesienia do obiektów typu Var pochodzących z clojure.core. Kolejne, opcjonalne argumenty, mogą wyrażać filtry, które zostaną użyte, aby uszczegółowić przeprowadzane operacje.

W przypadku, gdy tworzone odwołanie ma taką samą symboliczną nazwę, jak już istniejące w bieżącej przestrzeni przyporządkowanie, zostanie ono nadpisane, a na standardowe wyjście diagnostyczne wysłane będzie stosowne ostrzeżenie.

W bieżącej przestrzeni nazw symbolom będą bezpośrednio przyporządkowane obiekty Var rezydujące w clojure.core. Gdy w oryginalnej przestrzeni powiązanie zostanie usunięte (np. z użyciem ns-unmap), bieżąca przestrzeń nadal będzie zawierała przyporządkowanie symbolu do obiektu typu Var.

Możliwe do zastosowania filtry to:

  • :exclude sekwencja-symboli – symbole do pominięcia,
  • :only    sekwencja-symboli – symbole do wyłącznego przetworzenia,
  • :rename       mapa-symboli – symbole do przemianowania.

Filtry te powinny składać się z nazwy w formie słowa kluczowego, po której następuje sekwencja symboli, określająca wartości filtra, którymi będą symboliczne nazwy do uwzględnienia (:only) lub pominięcia (:exclude).

Sekwencjami mogą być dowolne kolekcje (np. wektory) wyposażone w sekwencyjny interfejs dostępu. Jeżeli zażądano przemianowania (:rename), to zamiast sekwencji należy podać mapę zawierającą pary przekształceń. W ten sposób do danej zmiennej globalnej będzie można odwoływać się w bieżącej przestrzeni pod inną symboliczną nazwą.

Przykład użycia makra refer-clojure
 1;; usuń powiązania dla apply i format
 2;; z bieżącej przestrzeni nazw
 3
 4(ns-unmap *ns* 'apply)
 5(ns-unmap *ns* 'format)
 6
 7;; stwórz odniesienia do wszystkich Varów z clojure.core
 8;; za wyjątkiem apply i format
 9
10(refer-clojure :exclude '[apply format])
11; => nil
12
13;; brak ostrzeżenia przy definiowaniu apply
14;; w bieżącej przestrzeni nazw
15
16(defn apply [] "aplikacja")
;; usuń powiązania dla apply i format ;; z bieżącej przestrzeni nazw (ns-unmap *ns* &#39;apply) (ns-unmap *ns* &#39;format) ;; stwórz odniesienia do wszystkich Varów z clojure.core ;; za wyjątkiem apply i format (refer-clojure :exclude &#39;[apply format]) ; =&gt; nil ;; brak ostrzeżenia przy definiowaniu apply ;; w bieżącej przestrzeni nazw (defn apply [] &#34;aplikacja&#34;)

Dodawanie odniesień do klas Javy, import

Makro import pozwala dodawać do przestrzeni nazw przyporządkowania symboli do odniesień odwołujących się do klas Javy, które znajdują się w podanym pakiecie.

Przyjmowanymi argumentami mogą być pojedyncze symbole – znaczy to wtedy, że zaimportowane mają być konkretne klasy umieszczone w pakietach. Argumentem może być również lista symboli – w takim przypadku pierwszy z nich określa pakiet (np. java.util), a kolejne są nazwami klas z tego pakietu (np. Date).

W efekcie działania makra do bieżącej przestrzeni zostaną dodane symboliczne nazwy takie same jak nazwy klas Javy, wraz z przyporządkowanymi im odniesieniami do tych klas.

Makro zwraca wartość obiektu ostatnio zaimportowanej klasy, a w przypadku nieistniejącego pakietu lub nazwy klasy zgłaszany jest wyjątek.

Użycie:

  • (import [& symbol…] & lista-symboli…).

Gdzie lista-symboli to:

  • (symbol-pakietu symbole-nazw-klas).
Przykłady użycia makra import
 1;; importowanie pojedynczych klas
 2
 3(import java.util.Date)
 4
 5;; importowanie wybranych klas z pakietu
 6
 7(import '(java.util Date Dictionary))
 8
 9;; użycie obiektu
10
11(Date.)
12; => #inst "2015-04-02T11:35:43.980-00:00"
;; importowanie pojedynczych klas (import java.util.Date) ;; importowanie wybranych klas z pakietu (import &#39;(java.util Date Dictionary)) ;; użycie obiektu (Date.) ; =&gt; #inst &#34;2015-04-02T11:35:43.980-00:00&#34;

Makro import może być również używane do importowania nowych typów danych utworzonych w Clojure (np. z użyciem deftype czy defrecord).

Zobacz także:

Internalizowanie obiektów Var, intern

Funkcja intern służy do internalizowania obiektów typu Var i tworzenia w ten sposób zmiennych globalnych. Przyjmuje ona dwa argumenty: nazwę przestrzeni nazw wyrażoną symbolem w formie stałej lub obiekt przestrzeni i nazwę zmiennej w postaci stałej symbolu.

Użycie intern sprawia, że w podanej przestrzeni nazw tworzone jest przyporządkowanie podanego symbolu do obiektu typu Var. Jeżeli obiekt typu Var już istnieje pod podaną nazwą, nie zostanie zastąpiony nowym.

W wersji trójargumentowej funkcja inicjuje obiekt podaną wartością, to znaczy ustawia wewnątrz zmiennej globalnej referencję, która odnosi się do wskazanego obiektu pamięciowego (tzw. powiązanie główne zmiennej globalnej). Jeżeli zmienna globalna już istnieje, zostanie zaktualizowana jej referencja bez wytwarzania nowego obiektu typu Var.

Funkcja zwraca obiekt typu Var identyfikowany symbolem.

Uwaga: Funkcja intern zastępuje obiektami typu Var istniejące już w przestrzeni nazw odniesienia do klas Javy o takich samych nazwach, nawet jeżeli nie ustawiono wartości początkowej.

Użycie:

  • (intern przestrzeń-nazw symboliczna-nazwa & wartość-początkowa).
Przykłady użycia funkcji intern
1(intern 'user 'zmienna)         ; => #'user/zmienna
2(intern 'user 'zmienna 5)       ; => #'user/zmienna
3user/zmienna                    ; => 5
4zmienna                         ; => 5
5(intern (find-ns 'user) 'a 10)  ; => #'user/a
(intern &#39;user &#39;zmienna) ; =&gt; #&#39;user/zmienna (intern &#39;user &#39;zmienna 5) ; =&gt; #&#39;user/zmienna user/zmienna ; =&gt; 5 zmienna ; =&gt; 5 (intern (find-ns &#39;user) &#39;a 10) ; =&gt; #&#39;user/a

Możemy sprawić, aby internalizowana zmienna globalna została potraktowana jako prywatna, tzn. była widoczna tylko w przestrzeni nazw, do której ją dodano. W praktyce będzie to oznaczało, że odwoływać się do takiej zmiennej z użyciem jej identyfikatora będą mogły konstrukcje z obszarów programu, w których bieżąca przestrzeń nazw jest ustawiona na taką samą, jak przestrzeń zdefiniowanej zmiennej. Aby oznaczyć globalną zmienną jako prywatną, należy skorzystać z tzw. metadanych symboli w odniesieniu do przekazywanego argumentu nazwy. Konkretnie chodzi tu o metadaną określaną kluczem :private.

Przykład użycia funkcji intern do tworzenia prywatnych powiązań
1(intern 'user '^:private zmienna)                      ; => #'user/zmienna
2(intern 'user '^{:private true} zmienna-2)             ; => #'user/zmienna
3(intern 'user (with-meta 'zmienna-3 {:private true}))  ; => #'user/zmienna
(intern &#39;user &#39;^:private zmienna) ; =&gt; #&#39;user/zmienna (intern &#39;user &#39;^{:private true} zmienna-2) ; =&gt; #&#39;user/zmienna (intern &#39;user (with-meta &#39;zmienna-3 {:private true})) ; =&gt; #&#39;user/zmienna

Pełna lista metadanych, które są istotne podczas internalizowania obiektu typu Var, jest podana w opisie formy specjalnej def.

Zmienne typu Var mogą być aktualizowane przez ponowne wywołanie funkcji intern. Aktualizowanie polega na powiązaniu ich z nowymi wartościami. Jeżeli zmienna już istnieje, to jej obiekt w przestrzeni nazw nie zostanie zastąpiony innym, będzie po prostu zmienione jego wewnętrzne wskazanie na konkretną wartość.

Przykład zmiany powiązania głównego
1(intern 'user 'xx 5)      ; utworzenie i powiązanie z wartością 5
2
3(pprint #'xx)             ; wyświetlenie obiektu referencyjnego
4; >> #<Var@7a0ad359: 5>
5
6(intern 'user 'xx 7)      ; powiązanie z wartością 7
7(pprint #'xx)             ; wyświetlenie obiektu referencyjnego
8; >> #<Var@7a0ad359: 7>
(intern &#39;user &#39;xx 5) ; utworzenie i powiązanie z wartością 5 (pprint #&#39;xx) ; wyświetlenie obiektu referencyjnego ; &gt;&gt; #&lt;Var@7a0ad359: 5&gt; (intern &#39;user &#39;xx 7) ; powiązanie z wartością 7 (pprint #&#39;xx) ; wyświetlenie obiektu referencyjnego ; &gt;&gt; #&lt;Var@7a0ad359: 7&gt;

Uwaga: Funkcja intern może być użyta do zmiany powiązania głównego, które jest współdzielone między wątkami, nawet gdy znajdujemy się w konstrukcji izolującej zmienną w wątku (np. w zasięgu dynamicznym).

Dodawanie obiektów Var, def

Forma specjalna def działa podobnie do intern, ale operuje na bieżącej przestrzeni nazw i wymaga podania niezacytowanego symbolu jako pierwszego argumentu. Symbol w tym kontekście nie będzie tworzył formy symbolowej ani formy stałej, lecz formę powiązaniową.

Forma def przyjmuje jeden obligatoryjny argument (wspomniany symbol, którego nazwa ma być skojarzona z tworzonym obiektem typu Var) i dwa argumenty opcjonalne: tekstowy łańcuch dokumentujący i wartość początkową zmiennej globalnej (która będzie użyta do aktualizacji, jeżeli zmienna już istnieje).

Jeżeli podano symbol z dookreśloną przestrzenią nazw, musi to być przestrzeń bieżąca – w przeciwnym razie zostanie zgłoszony wyjątek. Z wyjątkiem będziemy też mieli do czynienia, gdy użyjemy def do aktualizacji odwzorowania, które identyfikuje klasę Javy lub jest odniesieniem do obiektu z innej przestrzeni nazw.

Funkcja zwraca obiekt typu Var identyfikowany symbolem, który jest tożsamy z obiektem umieszczonym w przestrzeni nazw.

Użycie:

  • (def symbol łańcuch-dokumentujący? wartość-początkowa?).
Przykłady użycia funkcji def
 1(def zmienna)                   ; => #'user/zmienna
 2(def zmienna 5)                 ; => #'user/zmienna
 3(def zmienna "dokumentacja" 5)  ; => #'user/zmienna
 4user/zmienna                    ; => 5
 5zmienna                         ; => 5
 6
 7;; dostęp do dokumentacji – funkcja doc
 8
 9(doc zmienna)
10; >> user/zmienna
11; >>  dokumentacja zmiennej
12; => nil
(def zmienna) ; =&gt; #&#39;user/zmienna (def zmienna 5) ; =&gt; #&#39;user/zmienna (def zmienna &#34;dokumentacja&#34; 5) ; =&gt; #&#39;user/zmienna user/zmienna ; =&gt; 5 zmienna ; =&gt; 5 ;; dostęp do dokumentacji – funkcja doc (doc zmienna) ; &gt;&gt; user/zmienna ; &gt;&gt; dokumentacja zmiennej ; =&gt; nil

W przypadku def również można dodać do symbolu metadaną, która wskaże, że w przestrzeni nazw przyporządkowanie ma być prywatne, czyli widoczne wyłącznie przez konstrukcje z tej samej przestrzeni.

Skojarzone z symbolem metadane zostaną skopiowane do obiektu typu Var podczas jego tworzenia.

Przykłady użycia funkcji def do tworzenia globalnej zmiennej prywatnej
1(def ^:private zmienna)             ; => #'user/zmienna
2(def ^{ :private true } zmienna 5)  ; => #'user/zmienna
(def ^:private zmienna) ; =&gt; #&#39;user/zmienna (def ^{ :private true } zmienna 5) ; =&gt; #&#39;user/zmienna

Poniżej znajduje się spis wszystkich metadanych, które mają znaczenie podczas korzystania z def:

Klucz Typ Znaczenie
:private java.lang.Boolean Flaga logiczna, która wskazuje, że zmienna ma być prywatna
:dynamic java.lang.Boolean Flaga logiczna, która wskazuje, że zmienna ma być dynamiczna
:doc łańcuch java.lang.String Łańcuch tekstowy dokumentujący tożsamość zmiennej
:tag obiekt Class lub Symbol Symbol stanowiący nazwę klasy lub obiektu typu Class, który wskazuje na typ obiektu Javy znajdującego się w zmiennej (chyba, że jest to funkcja – wtedy będzie to jej zwracana wartość)
:test funkcja (implementujący IFn) Bezargumentowa funkcja używana do testów (obiekt zmiennej będzie w niej osiągalny jako literał fn umieszczony w metadanych)

Podczas tworzenia obiektu typu Var zostaną w nim automatycznie umieszczone następujące metadane:

Klucz Typ Znaczenie
:file java.lang.String Nazwa pliku źródłowego
:line java.lang.Integer Numer linii pliku źródłowego
:name clojure.lang.Symbol Nazwa zmiennej
:ns clojure.lang.Namespace Przestrzeń nazw
:macro java.lang.Boolean Flaga oznaczająca, że obiekt odnosi się do makra
:arglists PersistentVector$ChunkedSeq Sekwencja wektorowa z argumentami, jeżeli obiekt odnosi się do funkcji lub makra

Zmienne globalne mogą być aktualizowane m.in. przez ponowne wywołanie funkcji def. Polega to na powiązaniu referencji wewnątrz obiektu Var z nową wartością. Jeżeli zmienna identyfikowana podanym symbolem już istnieje, jej obiekt w przestrzeni nazw nie zostanie zastąpiony innym, ale zmienione będzie jego odniesienie do konkretnej wartości (powiązanie główne).

Przykład zmiany powiązania głównego obiektu typu Var
1(def xx 5)               ; utworzenie i powiązanie z wartością 5
2(pprint #'xx)            ; wyświetlenie obiektu
3; >> #<Var@4eee52c: 5>
4
5(def xx 7)               ; powiązanie z wartością 7
6(pprint #'xx)            ; wyświetlenie obiektu
7; >> #<Var@4eee52c: 7>
(def xx 5) ; utworzenie i powiązanie z wartością 5 (pprint #&#39;xx) ; wyświetlenie obiektu ; &gt;&gt; #&lt;Var@4eee52c: 5&gt; (def xx 7) ; powiązanie z wartością 7 (pprint #&#39;xx) ; wyświetlenie obiektu ; &gt;&gt; #&lt;Var@4eee52c: 7&gt;

Uwaga: Forma specjalna def może być użyta do zmiany powiązania głównego, które jest współdzielone między wątkami, nawet jeżeli znajdujemy się w konstrukcji izolującej zmienną w wątku (np. w zasięgu dynamicznym).

Dodawanie jednokrotne Varów, defonce

Makro defonce internalizuje obiekt typu Var w bieżącej przestrzeni nazw. Jeśli użyto symbolu kwalifikowanego, musi on wskazywać tę samą przestrzeń. Makro działa podobnie do def, ale gdy Var ma już powiązanie główne, nie wartościuje wyrażenia inicjującego ani nie zastępuje tego powiązania.

Makro nie przyjmuje osobnego łańcucha dokumentacyjnego, bowiem ma tylko dwa argumenty: niezacytowany symbol, który może nieść metadane, oraz wyrażenie inicjujące, którego wartość przy pierwszej inicjalizacji stanie się powiązaniem głównym Vara.

Makro zwraca obiekt typu Var, jeżeli ustawiono powiązanie główne lub wartość nil, jeżeli powiązanie już istniało.

Makro defonce przydaje się do zachowania zainicjowanego powiązania głównego podczas ponownego wczytywania pliku, np. w pracy interaktywnej. Nie czyni jednak wartości stałą na poziomie języka: inne operacje nadal mogą zmienić powiązanie główne Vara.

Uwaga: Nawet jeżeli powiązanie z wartością nie doszło do skutku, to ustawione będą metadane pochodzące z przekazanego symbolu i zastąpią poprzednie.

Użycie:

  • (defonce symbol wyrażenie).
Przykłady użycia funkcji defonce
1(defonce zmienna 5)               ; => #'user/zmienna
2(defonce ^:flaszka zmienna 1000)  ; => nil
3user/zmienna                      ; => 5
4zmienna                           ; => 5
5
6(meta #'zmienna)
7; => { :ns #<Namespace user>, :name zmienna,
8; =>   :flaszka true, :file "NO_SOURCE_PATH",
9; =>   :column 1, :line 1 }
(defonce zmienna 5) ; =&gt; #&#39;user/zmienna (defonce ^:flaszka zmienna 1000) ; =&gt; nil user/zmienna ; =&gt; 5 zmienna ; =&gt; 5 (meta #&#39;zmienna) ; =&gt; { :ns #&lt;Namespace user&gt;, :name zmienna, ; =&gt; :flaszka true, :file &#34;NO_SOURCE_PATH&#34;, ; =&gt; :column 1, :line 1 }

Usuwanie odwzorowań, ns-unmap

Dzięki funkcji ns-unmap możemy usuwać z przestrzeni nazw powiązania symboli ze zmiennymi globalnymi lub klasami Javy. Przyjmuje ona dwa argumenty. Pierwszy jest określeniem przestrzeni (z użyciem symbolu w formie stałej lub obiektu przestrzeni), a drugi symbolicznie wyrażoną nazwą konkretnego przyporządkowania, które ma być usunięte.

Funkcja zwraca wartość nil, a gdy podana przestrzeń nie istnieje, zgłaszany jest wyjątek.

Użycie:

  • (ns-unmap przestrzeń-nazw symboliczna-nazwa).
Przykład użycia funkcji ns-unmap
 1;; tworzymy zmienną globalną x
 2
 3(def x 5)
 4; => #'user/x
 5
 6;; sięgamy po jej wartość
 7
 8x
 9; => 5
10
11;; dodatkowo tworzymy odwołanie do obiektu Var tej zmiennej
12
13(def y (var x))
14
15;; usuwamy odwzorowanie
16
17(ns-unmap 'user 'x)
18; => nil
19
20;; sprawdzamy, czy identyfikator x jest wciąż widoczny
21
22(resolve 'x)
23; => nil
24
25;; sprawdzamy, czy sam obiekt Var istnieje,
26;; choć nie jest już powiązany z symbolem x
27
28(deref y)
29; => 5
;; tworzymy zmienną globalną x (def x 5) ; =&gt; #&#39;user/x ;; sięgamy po jej wartość x ; =&gt; 5 ;; dodatkowo tworzymy odwołanie do obiektu Var tej zmiennej (def y (var x)) ;; usuwamy odwzorowanie (ns-unmap &#39;user &#39;x) ; =&gt; nil ;; sprawdzamy, czy identyfikator x jest wciąż widoczny (resolve &#39;x) ; =&gt; nil ;; sprawdzamy, czy sam obiekt Var istnieje, ;; choć nie jest już powiązany z symbolem x (deref y) ; =&gt; 5

W powyższym przykładzie pokazaliśmy przy okazji, że usunięcie odwzorowania nie niszczy Vara: osobne odniesienie y nadal do niego prowadzi. Var zachowuje też metadane :name:ns, lecz zwykłe rozpoznawanie w przestrzeni nazw nie odnajdzie go już przez usunięte odwzorowanie.

Dodawanie aliasów, alias

Mechanizm aliasów pozwala odwoływać się do różnych przestrzeni nazw z użyciem alternatywnych identyfikatorów umieszczonych w bieżącej przestrzeni.

Funkcja alias pozwala dodawać do bieżącej przestrzeni alternatywne nazwy innych przestrzeni nazw. Przyjmuje ona dwa argumenty: pierwszy powinien być symbolem w formie stałej, a drugi obiektem przestrzeni nazw lub jej nazwą wyrażoną literalnym symbolem. Pierwszy argument to nazwa aliasu, a drugi przestrzeń nazw, do której odniesienie ma być wytworzone.

Funkcja zwraca wartość nil, a w przypadku podania nieistniejącej przestrzeni nazw zgłaszany jest wyjątek.

Użycie:

  • (alias symboliczna-nazwa przestrzeń-nazw).
Przykład użycia funkcji alias
1(alias 'st 'clojure.string)
2(st/reverse "abcdef")
3; => "fedcba"
(alias &#39;st &#39;clojure.string) (st/reverse &#34;abcdef&#34;) ; =&gt; &#34;fedcba&#34;

Zobacz także:

Usuwanie aliasów, unalias

Funkcja ns-unalias usuwa aliasy dodane z użyciem alias. Przyjmuje dwa argumenty. Pierwszy powinien określać przestrzeń nazw z użyciem formy stałej symbolu lub obiektu przestrzeni, a drugi powinien być nazwą aliasu wyrażoną symbolicznie.

Funkcja zawsze zwraca wartość nil, niezależnie od tego, czy dany alias istniał, lub czy nie było możliwe jego usunięcie, ponieważ nie był w istocie aliasem. Gdy podana przestrzeń nazw nie istnieje, zgłaszany jest wyjątek.

Użycie:

  • (ns-unalias przestrzeń-nazw symboliczna-nazwa).
Przykład użycia funkcji ns-unalias
1(ns-unalias 'user 'st)
2; => nil
(ns-unalias &#39;user &#39;st) ; =&gt; nil

Odczytywanie zawartości

Odczyt nazwy, ns-name

Funkcja ns-name jako argument przyjmuje obiekt przestrzeni nazw (lub symbol reprezentujący jej nazwę), a zwraca symbol określający nazwę przestrzeni. Jeżeli przestrzeń nie istnieje, zgłaszany jest wyjątek.

Użycie:

  • (ns-name przestrzeń-nazw).
Przykłady użycia funkcji ns-name
1(ns-name 'user)            ; => user
2(ns-name (find-ns 'user))  ; => user
(ns-name &#39;user) ; =&gt; user (ns-name (find-ns &#39;user)) ; =&gt; user

Odczyt aliasów, ns-aliases

Funkcja ns-aliases przyjmuje jeden argument, którym powinien być symbol w formie stałej określający nazwę przestrzeni nazw lub obiekt tej przestrzeni, a zwraca mapę zawierającą zdefiniowane aliasy, czyli przyporządkowania obiektów innych przestrzeni nazw do symbolicznych identyfikatorów. Jeżeli przestrzeń nie istnieje, zgłoszony zostanie wyjątek.

Użycie:

  • (ns-aliases przestrzeń-nazw).
Przykład użycia funkcji ns-aliases
1(ns-name (get (ns-aliases 'clojure.core) 'jio))
2; => clojure.java.io
(ns-name (get (ns-aliases &#39;clojure.core) &#39;jio)) ; =&gt; clojure.java.io

Odczyt odwzorowań Varów, ns-interns

Funkcja ns-interns przyjmuje jeden argument, którym powinien być symbol w formie stałej określający nazwę przestrzeni nazw lub obiekt tej przestrzeni, a zwraca mapę zawierającą przyporządkowania symbolicznych identyfikatorów do zmiennych globalnych (obiektów typu Var). Jeżeli przestrzeń nie istnieje, zgłoszony zostanie wyjątek.

Użycie:

  • (ns-interns przestrzeń-nazw).
Przykład użycia funkcji ns-interns
1(identical? (get (ns-interns 'clojure.core) 'map)
2            #'clojure.core/map)
3; => true
(identical? (get (ns-interns &#39;clojure.core) &#39;map) #&#39;clojure.core/map) ; =&gt; true

Odczyt odniesień do Varów, ns-refers

Funkcja ns-refers przyjmuje jeden argument, którym powinien być symbol w formie stałej określający nazwę przestrzeni nazw lub obiekt tej przestrzeni, a zwraca mapę zawierającą przyporządkowania symbolicznych identyfikatorów do obiektów typu Var, które zostały odniesione do podanej przestrzeni (np. z użyciem refer).

Użycie:

  • (ns-refers przestrzeń-nazw).
Przykład użycia funkcji ns-refers
1(identical? (get (ns-refers 'user) 'map)
2            #'clojure.core/map)
3; => true
(identical? (get (ns-refers &#39;user) &#39;map) #&#39;clojure.core/map) ; =&gt; true

Warto wiedzieć, że oryginalne powiązanie symbolu z Varem w innej przestrzeni nazw może zostać usunięte (np. z użyciem funkcji ns-unmap). W takim przypadku powiązanie odzwierciedlone w przestrzeni bieżącej nie zniknie, ponieważ symbol będzie przyporządkowany bezpośrednio do obiektu typu Var, a nie do elementu w wewnętrznej mapie innej przestrzeni. Minusem takiej sytuacji może być jednak pewna niezgodność metadanych docelowego obiektu ze stanem faktycznym: zapisana w Var metadana o kluczu :ns będzie wskazywała na oryginalną przestrzeń nazw, w której nie znajdziemy już powiązania.

Odczyt odniesień do klas, ns-imports

Funkcja ns-imports przyjmuje jeden argument, którym powinien być symbol w formie stałej określający nazwę przestrzeni nazw lub obiekt tej przestrzeni, a zwraca mapę zawierającą przyporządkowania symbolicznych identyfikatorów do odniesień wskazujących klasy Javy. Jeżeli przestrzeń nie istnieje, zgłoszony zostanie wyjątek.

Użycie:

  • (ns-imports przestrzeń-nazw).
Przykład użycia funkcji ns-imports
1(identical? (get (ns-imports 'clojure.core) 'String)
2            java.lang.String)
3; => true
(identical? (get (ns-imports &#39;clojure.core) &#39;String) java.lang.String) ; =&gt; true

Odczyt wszystkich odwzorowań, ns-map

Funkcja ns-map przyjmuje jeden argument, którym powinien być symbol w formie stałej określający nazwę przestrzeni nazw lub obiekt tej przestrzeni, a zwraca mapę zawierającą wszystkie przyporządkowania symbolicznych identyfikatorów do obiektów (zmiennych referencyjnych typu Var i klas Javy). Jeżeli przestrzeń nie istnieje, wygenerowany zostanie wyjątek.

Użycie:

  • (ns-map przestrzeń-nazw).
Przykład użycia funkcji ns-map
1(every? #(or (var? %) (class? %))
2        (vals (ns-map 'clojure.core)))
3; => true
(every? #(or (var? %) (class? %)) (vals (ns-map &#39;clojure.core))) ; =&gt; true

Odczyt publicznych, ns-publics

Funkcja ns-publics przyjmuje jeden argument, którym powinien być symbol w formie stałej określający nazwę przestrzeni nazw lub obiekt tej przestrzeni, a zwraca mapę zawierającą jej publiczne internalizowane Vary. Nie obejmuje Varów odniesionych ani zaimportowanych klas. Jeżeli przestrzeń nie istnieje, wygenerowany zostanie wyjątek.

Użycie:

  • (ns-publics przestrzeń-nazw).
Przykład użycia funkcji ns-publics
1(identical? (get (ns-publics 'clojure.core) 'map)
2            #'clojure.core/map)
3; => true
(identical? (get (ns-publics &#39;clojure.core) &#39;map) #&#39;clojure.core/map) ; =&gt; true

Obsługa bibliotek

Biblioteka (ang. library), a dokładniej biblioteka oprogramowania (ang. software library), zwana też po polsku biblioteką programistyczną, to zbiór umieszczonych w plikach zasobów, które mogą być wykorzystywane przez oprogramowanie, aby wzbogacać dostępne funkcje. W bibliotece mogą znajdować się dane, podprogramy (np. makra czy funkcje), a nawet definicje nowych typów danych. Dzięki bibliotekom możliwe jest ponowne korzystanie z już zaimplementowanych metod rozwiązywania problemów.

W zależności od języka programowania biblioteki programistyczne mogą składać się wyłącznie z kodu źródłowego lub występować w wersjach skompilowanych z dodatkiem w postaci plików źródłowych zawierających deklaracje, dzięki którym kompilator potrafi połączyć wywołania podprogramów z odpowiednimi implementacjami w języku maszynowym bądź kodzie bajtowym.

Na JVM biblioteka jest często rozpowszechniana jako archiwum JAR. Pojedynczy JAR może zawierać pliki źródłowe Clojure, pliki klas JVM wytworzone z Clojure lub Javy oraz zwykłe zasoby. To, co zostanie wczytane, zależy od żądanej przestrzeni nazw i wpisów ścieżki klas; archiwum JAR nie jest ograniczone do jednego rodzaju zawartości.

Wczytywanie bibliotek

Ładowanie bibliotek i umieszczanie potrzebnych odwzorowań w bieżącej przestrzeni nazw korzysta z operacji przedstawionych wcześniej, takich jak referimport. Zwykły interfejs do tej pracy stanowią funkcja require i deklaratywne klauzule makra ns.

W Clojure pliki danej biblioteki powinny znajdować się w katalogu umieszczonym w ścieżce przeszukiwania klas (ang. classpath), zaś zgodnie z konwencją jej nazwa będzie wyrażana jako symbol w formie stałej (podczas przekazywania jej do różnych makr czy funkcji).

Wczytywanie samodzielne, load

Za ładowanie bibliotek odpowiada funkcja load. Przyjmuje ona zero lub więcej argumentów, które powinny być ścieżkami systemu plikowego wyrażonymi łańcuchami znakowymi.

Użycie:

  • (load & ścieżka…).

Dla każdej podanej względnej ścieżki (nie rozpoczynającej się separatorem nazw ścieżkowych) plik biblioteki będzie poszukiwany w katalogu głównym (ang. root directory) bieżącej przestrzeni nazw. Katalog główny jest uzyskiwany przez:

  • pobranie nazwy bieżącej przestrzeni nazw;
  • dodatnie z przodu znaku ukośnika (/);
  • zamianie wszystkich dywizów (-) na znaki podkreślenia (_);
  • zamianie wszystkich kropek (.) na znaki ukośnika (/);
  • wydzielenie fragmentu od początku do ostatniego wystąpienia ukośnika;
  • dodanie na końcu znaku ukośnika;
  • dodanie na końcu ścieżki podanej jako argument.

Dla każdej ścieżki bezwzględnej (rozpoczynającej się separatorem nazw ścieżkowych) dokonane zostanie przeszukanie wszystkich lokalizacji, które są złożeniami kolejnych ścieżek umieszczonych w ścieżce przeszukiwania klas (ang. classpath).

Przykłady katalogów głównych w zależności od nazwy przestrzeni i podanej ścieżki:

  • bieżąca przestrzeń nazw user:

    • (load "test"): /test,
    • (load "raz/dwa"): /raz/dwa,
  • bieżąca przestrzeń nazw clojure.core:

    • (load "test"): /clojure/test,
    • (load "raz/dwa"): /clojure/raz/dwa,
    • (load "string"): /clojure/string.

Niezależnie od tego, czy podano ścieżkę względną czy bezwzględną, ostatni element podanej ścieżki będzie potraktowany jak nazwa pliku do wczytania i zostanie do niego dołączony łańcuch tekstowy z rozszerzeniem .clj.

Możemy przekonać się, w jaki sposób tworzone są nazwy, ustawiając zmienną dynamiczną *clojure.core/loading-verbosely* na wartość różną od false i różną od nil w zasięgu dynamicznym makra binding.

Przykład użycia funkcji load
 1;; ładowanie pliku projektu
 2;; src/projekt/core.clj
 3
 4(load "projekt/core")
 5; => nil
 6
 7;; ładowanie pliku głównego
 8;; biblioteki clojure.string
 9
10(load "/clojure/string")
11; => nil
;; ładowanie pliku projektu ;; src/projekt/core.clj (load &#34;projekt/core&#34;) ; =&gt; nil ;; ładowanie pliku głównego ;; biblioteki clojure.string (load &#34;/clojure/string&#34;) ; =&gt; nil

Funkcja require

Funkcja require ładuje biblioteki Clojure. Ponieważ ich specyfikacje są danymi, w bezpośrednich wywołaniach zwykle je cytujemy. Każdy argument powinien być jednym z kilku rodzajów:

  • specyfikacji biblioteki (ang. library spec),
  • listy przedrostkowej (ang. prefix list),
  • flagi modyfikatora (ang. modifier flag).

Specyfikacja biblioteki to albo symbolicznie wyrażona nazwa biblioteki, albo wektor zawierający nazwę i dodatkowe parametry. Nazwy tych parametrów powinny być wyrażone słowami kluczowymi i zgrupowane w sekwencyjnej kolekcji. Dzięki parametrom specyfikacji biblioteki możemy zdecydować co stanie się zaraz po załadowaniu jej do pamięci.

Możliwe parametry to:

  • :as symboliczna-nazwa – korzysta z funkcji alias i wytwarza odniesienie do ładowanej biblioteki pod podaną nazwą w bieżącej przestrzeni nazw;

  • :refer wektor-symboli-lub-:all – korzysta z funkcji refer, aby wytworzyć w bieżącej przestrzeni odwzorowania wybranych publicznych Varów albo wszystkich publicznych Varów, gdy wartością jest :all;

  • :as-alias symboliczna-nazwa – tworzy jedynie alias przestrzeni nazw, bez wczytywania biblioteki docelowej; przydaje się to, gdy nazwy kwalifikowane są używane jako dane.

Lista przedrostkowa umożliwia załadowanie bibliotek, których nazwy zaczynają się tak samo. Oszczędza to klawiaturę i nasze palce. Zamiast podanego, wspólnego przedrostka tworzy się listę specyfikacji bibliotek. Istotnym warunkiem jest to, że nazwy z tej listy nie mogą już zawierać kropek, tzn. muszą być ostatnimi elementami ścieżki (i nazwy).

Flagi modyfikatorów pozwalają wpływać na zachowanie funkcji. Są to słowa kluczowe:

  • :reload – wymusza ponowne wczytanie bibliotek do pamięci, nawet jeżeli już zostały wczytane;

  • :reload-all – działa jak :reload, ale wpływa na wszystkie biblioteki zależne, ładowane przez wczytywaną (jeżeli jest w nich czyniony użytek z use lub require);

  • :verbose – sprawia, że wypisane zostaną informacje diagnostyczne dotyczące ładowania i tworzenia odniesień.

Funkcja lokalizuje i wczytuje główny zasób biblioteki, którego ścieżka wynika z nazwy przestrzeni. Wczytanie przestrzeni Clojure nie tworzy automatycznie odpowiadającego jej pakietu Javy. Ścieżka zasobu powstaje według następującego schematu:

  • kropki są zamieniane na separatory nazw ścieżkowych (np. a.b na a/b);
  • ostatnia część nazwy uznawana jest za nazwę pliku (np. b.clj);
  • pozostała część nazwy uznawana jest za nazwę katalogu głównego (np. a);
  • względna ścieżka wraz z nazwą pliku jest dołączana do kolejnych ścieżek przeszukiwania klas, aż zostanie odnaleziony główny plik biblioteki.

W głównym pliku powinna być zdefiniowana przestrzeń nazw całej biblioteki.

Jeżeli biblioteka już została wcześniej wczytana do pamięci, to nie jest wykonywane jej ponowne ładowanie.

Użycie:

  • (require & specyfikacja… & lista-przedrostkowa… & flaga…).
Przykłady użycia funkcji require
 1;; wczyta bibliotekę i utworzy przestrzeń nazw clojure.string
 2
 3(require 'clojure.string)
 4; => nil
 5
 6;; możemy podać kilka bibliotek
 7
 8(require 'clojure.string 'clojure.test 'clojure.set)
 9
10;; możemy podać kilka bibliotek o wspólnym przedrostku
11
12(require '[clojure string test set])
13
14;; wczyta bibliotekę i utworzy przestrzeń nazw clojure.string
15;; nawet, jeżeli już była wczytana
16
17(require 'clojure.string :reload :verbose)
18; => (clojure.core/load "/clojure/string")
19; => nil
20
21;; wczyta bibliotekę, jeżeli jeszcze nie była wczytana
22;; i utworzy alias w bieżącej przestrzeni nazw, aby
23;; przestrzeń clojure.string była widoczna jako st
24
25(require '[clojure.string :as st] :verbose)
26; => (clojure.core/in-ns 'user)
27; => (clojure.core/alias 'st 'clojure.string)
28; => nil
;; wczyta bibliotekę i utworzy przestrzeń nazw clojure.string (require &#39;clojure.string) ; =&gt; nil ;; możemy podać kilka bibliotek (require &#39;clojure.string &#39;clojure.test &#39;clojure.set) ;; możemy podać kilka bibliotek o wspólnym przedrostku (require &#39;[clojure string test set]) ;; wczyta bibliotekę i utworzy przestrzeń nazw clojure.string ;; nawet, jeżeli już była wczytana (require &#39;clojure.string :reload :verbose) ; =&gt; (clojure.core/load &#34;/clojure/string&#34;) ; =&gt; nil ;; wczyta bibliotekę, jeżeli jeszcze nie była wczytana ;; i utworzy alias w bieżącej przestrzeni nazw, aby ;; przestrzeń clojure.string była widoczna jako st (require &#39;[clojure.string :as st] :verbose) ; =&gt; (clojure.core/in-ns &#39;user) ; =&gt; (clojure.core/alias &#39;st &#39;clojure.string) ; =&gt; nil

Funkcja use

Funkcja use łączy ładowanie z odnoszeniem publicznych Varów do bieżącej przestrzeni nazw. Przyjmuje ten sam starszy styl cytowanych specyfikacji bibliotek co require, lecz szerokie importy czynią zależności i kolizje nazw mniej widocznymi.

Funkcja use może przyjmować dodatkowe parametry w specyfikacji bibliotek:

  • :exclude sekwencja-symboli – symbole do pominięcia,
  • :only    sekwencja-symboli – symbole do wyłącznego przetworzenia,
  • :rename       mapa-symboli – symbole do przemianowania.

Użycie:

  • (use & specyfikacja… & lista-przedrostkowa… & flaga…).

Zamiast use należy preferować makro ns z klauzulą :require, aliasami i jawnym wektorem :refer.

Makro ns

Makro ns grupuje deklarację przestrzeni nazw i operacje na zależnościach w jednym miejscu, zwykle na początku pliku źródłowego.

Makro pozwala ustawić bieżącą przestrzeń nazw (tworząc ją w razie potrzeby), wczytać potrzebne biblioteki, ustanowić odniesienia i aliasy, zaimportować klasy Javy oraz zlecić generowanie klasy JVM podczas kompilacji AOT.

Makro przyjmuje nazwę przestrzeni i opcjonalny zestaw klauzul opisujących wymagane biblioteki, importy i powiązane operacje. Klauzule są deklaratywną składnią interpretowaną przez ns; ich elementy nie są najpierw wartościowane jak zwykłe argumenty wywołania, dlatego nazw bibliotek nie trzeba w nich cytować.

Opcjonalnie można po nazwie przestrzeni podać łańcuch dokumentujący (np. opisujący plik źródłowy), a także mapę atrybutów.

Użycie:

  • (ns nazwa-przestrzeni łańcuch-dokumentujący? mapa-atrybutów? & klauzula…).

Klauzule referencyjne:

  • (:require       …) – wywołuje require,
  • (:use           …) – wywołuje use,
  • (:import        …) – wywołuje import,
  • (:load          …) – wywołuje load,
  • (:gen-class     …) – wywołuje gen-class,
  • (:refer-clojure …) – wywołuje refer-clojure.

W przypadku gen-class domyślnie przekazywanymi do wywołania argumentami są:

  • :name nazwa-przestrzeni,
  • :impl-ns nazwa-przestrzeni,
  • :main true.

Klauzula :gen-class emituje żądaną klasę nazwaną podczas kompilacji AOT; nie ma wpływu na samo wczytanie źródła. Kompilacja AOT bez :gen-class nadal wytwarza klasy implementacyjne potrzebne przestrzeni nazw, a ich dokładny zestaw jest szczegółem implementacyjnym.

Przykład użycia makra ns
1(ns randomseed.pl.przykłady
2  (:refer-clojure                    :exclude          [printf])
3  (:require [clojure.set                  :as    set          ]
4            [clojure.string               :as    string       ]
5            [clojure.repl                 :refer [doc dir]    ]
6            [randomseed.pl.zasoby.plikowe :as    pliki        ])
7  (:use     [randomseed.pl.podręczne      :only  [funkcja inna]])
8  (:import  [java.util                           Date Random  ]))
(ns randomseed.pl.przykłady (:refer-clojure :exclude [printf]) (:require [clojure.set :as set ] [clojure.string :as string ] [clojure.repl :refer [doc dir] ] [randomseed.pl.zasoby.plikowe :as pliki ]) (:use [randomseed.pl.podręczne :only [funkcja inna]]) (:import [java.util Date Random ]))

W powyższym przykładzie widzimy, że tworzona jest przestrzeń nazw randomseed.pl.przykłady, a zaraz potem ładowane są odniesienia do zmiennych globalnych ze standardowej biblioteki języka, ale z wyłączeniem obiektu oznaczonego symbolem printf.

Następnie w przestrzeni randomseed.pl.przykłady wytwarzane są aliasy dla przestrzeni nazw clojure.set, clojure.string, randomseed.pl.zasoby.plikowe w celu ich łatwiejszego specyfikowania. W tej samej sekcji wytwarzane są również odwołania do zmiennych globalnych z przestrzeni clojure.repl (m.in. dla docdir), aby można było je wywoływać bez określania przestrzeni.

Klauzula :use działa podobnie jak :require z parametrem :refer, tzn. w podanej przestrzeni (tu randomseed.pl.podręczne) lokalizowane są obiekty (tu o nazwach funkcjainna) i w obsługiwanej przez makro przestrzeni (tu randomseed.pl.przykłady) wytwarzane są do nich odniesienia. Zaleca się korzystanie z :require (z parametrem :refer) zamiast z :use.

Ostatnia klauzula (:import) wytwarza odniesienia do klas Javy (DateRandom) z pakietu java.util.

Obsługa powiązań

Powiązania i referencje odpowiadają na różne pytania. Kontekst rozpoznawania nazw mówi, co oznacza symbol; obiekt referencyjny mówi, który stan jest bieżący; dynamiczne powiązanie Vara mówi zaś, która tymczasowa wartość obowiązuje w bieżącym kontekście wykonania. Rozdzielenie tych warstw ułatwia rozumowanie o przesłanianiu i mutacji. W praktyce obsługa powiązań polega na:

Odczytywanie wartości powiązań to zadanie dla mechanizmów języka (wystarczy w kodzie źródłowym użyć niezacytowanego symbolu), a w przypadku typów referencyjnych zadanie dla programisty, w którym bardzo pomagają gotowe funkcje i makra czytnika. Poniżej skupimy się więc na wytwarzaniu powiązań w zależności od ich rodzajów i używanych w tym celu konstrukcji.

Leksykalne formy powiązaniowe kojarzą symbole z wartościami w obszarze leksykalnym. Powiązania te są niezmienne; zagnieżdżone powiązanie może przesłonić zewnętrzne. Odwzorowania przestrzeni nazw są zarządzane osobno, a dynamiczne binding nie tworzy nowego odwzorowania symbolu — wybiera tymczasową wartość bieżącą istniejącego dynamicznego Vara.

W przypadku typów referencyjnych możemy dokonywać aktualizacji wartości bieżących, do których instancje tych typów się odnoszą, korzystając z odpowiednich funkcji. W ten sposób możemy wytwarzać stałe tożsamości, które będą odnosiły się do zmiennych stanów.

Rodzaje powiązań

Dla potrzeb tego rozdziału warto rozróżnić trzy warstwy:

  • powiązania i odwzorowania nazw, kojarzące symbole z wartościami leksykalnymi, Varami lub klasami Javy;
  • stan referencyjny, w którym stabilny obiekt referencyjny udostępnia wartość bieżącą;
  • dynamiczne powiązania Varów, tymczasowo przesłaniające powiązanie główne dynamicznego Vara w dynamicznym zakresie wykonania.

Dynamiczny Var pozostaje zwykłym obiektem Var odwzorowanym w przestrzeni nazw. Dodatkowa metadana :dynamic zezwala na stosowe powiązania wątku; nie wprowadza odrębnego mechanizmu odwzorowań przestrzeni nazw.

Powiązania symboli

Powiązania symboli służą do identyfikowania wartości lub obiektów referencyjnych w pewnych kontekstach. Możemy wyróżnić powiązania symboli:

  • przestrzeniach nazw (ang. namespaces):

    • ze zmiennymi globalnymi (typ Var, forma def),
    • klasami Javy (typ java.lang.Class);
  • powiązaniach leksykalnych (ang. lexical bindings):

    • lokalnymi wartościami (formy let, loop i podobne);
    • lokalnymi obiektami Var (forma with-local-vars);
    • argumentami funkcji w ich definicjach
      (tzw. powiązania parametryczne – formy fn, defn);

Dekompozycja jest składnią tworzenia wielu powiązań leksykalnych lub parametrycznych z jednej wartości wejściowej, a nie trzecim mechanizmem przechowywania.

Poprawne semantycznie powiązania symboli w pewnych kontekstach będziemy też nazywali formami powiązaniowymi symboli.

Powiązania strukturalne

Powiązania strukturalne (ang. structural bindings) to powiązania leksykalne lub parametryczne, w których dochodzi do dekompozycji struktury asocjacyjnej (np. mapy) lub sekwencyjnej (np. wektora), aby za jednym razem powiązać wiele symboli z wartościami.

Dekompozycję, która będzie omówiona dalej, możemy wyobrazić sobie jako sposób wytwarzania powiązań z użyciem dwóch podobnych pod względem aranżacji struktur. Z lewej strony umieścimy strukturę zawierającą niezacytowane symbole, a z prawej izomorficzną do niej strukturę z wartościami inicjującymi. Symbole umieszczone w lewej strukturze zostaną powiązane z wartościami z prawej struktury zależnie od pozycji (w przypadku kolekcji sekwencyjnych, np. wektorów) bądź kluczy (w przypadku map).

Powiązania obiektów referencyjnych

Obiekty referencyjne dostarczają stabilnych tożsamości, których obserwowalny stan podlega jawnemu kontraktowi aktualizacji albo realizacji. Do mutowalnych typów referencyjnych należą:

Future’y, Promise’yDelay’e również zapewniają stabilne tożsamości oraz dereferencjonowalne wyniki, ale podlegają kontraktom jednokrotnego obliczenia lub dostarczenia, zamiast pozwalać na dowolne, powtarzane aktualizacje stanu.

Powiązania dynamiczne

Powiązania dynamiczne (ang. dynamic bindings) służą do tymczasowego przesłaniania wartości globalnych Varów, którym ustawiono flagę :dynamic w metadanych. Makro binding ustanawia stosowe powiązanie wątku dla swojego ciała i wywołań wykonanych w tym dynamicznym zakresie. Przestrzeń nazw nadal zawiera to samo odwzorowanie Vara.

Zasięgi powiązań

Zasięg powiązania to obszar programu, w którym dane powiązanie może być użyte.

Poza zasięgiem powiązania możemy też mówić o widoczności identyfikowanej wartości, czyli o obszarze, w którym można się do niej odwoływać. Widoczność wartości zależy od zasięgu powiązania, ale można też dodatkowo nią sterować, korzystając z przestrzeni nazw.

Widoczność może być mniejsza niż zasięg, jeżeli w danym kontekście ten sam symbol jest używany do oznaczenia więcej niż jednego powiązania. Mówimy wtedy o przesłanianiu.

W Clojure spotykamy zasięg leksykalny, dostępność na poziomie przestrzeni nazw oraz dynamiczny zakres powiązań wątku.

Dostępność na poziomie przestrzeni nazw

Odwzorowania globalnych Varówklas Javy pozostają dostępne tak długo, jak długo zarejestrowana jest ich przestrzeń nazw oraz same odwzorowania. Dostęp nadal zależy od kwalifikacji, aliasów, odniesień, prywatności i tego, czy przestrzeń została wczytana; „globalny” nie znaczy, że każdy niekwalifikowany symbol może taki obiekt zobaczyć.

Odwzorowania przestrzeni nazw pozwalają nadawać Varom stabilne nazwy. Powiązanie główne Vara może zmieniać się w czasie, podczas gdy tożsamość Vara i jego odwzorowanie pozostają na miejscu, o ile kod jawnie go nie usunie albo nie zastąpi.

Przykładem powszechnego wykorzystywania nieograniczonego zasięgu są nazwy funkcji. Symboliczne identyfikatory są w przestrzeniach nazw powiązane z obiektami referencyjnymi typu Var, które z kolei zawierają odniesienia do obiektów typu funkcyjnego. Właśnie dzięki przestrzeniom nazw możliwe jest sterowanie widocznością w tym zasięgu.

Przykłady definiowania i używania zmiennych globalnych
 1(ns nasza)        ; przełączenie przestrzeni nazw
 2(def x 5)         ; zmienna globalna x (powiązanie główne z wartością 5)
 3(defn funk [] x)  ; funkcja, która zwraca wartość zmiennej globalnej x
 4(funk)            ; wywołanie funkcji
 5; => 5            ; rezultat wywołania
 6
 7(ns inna)         ; przełączenie przestrzeni nazw
 8(funk)            ; próba wywołania funkcji
 9; >> Unable to resolve symbol: funk in this context
10
11(nasza/funk)      ; wywołanie funkcji symbolem z dookreśloną przestrzenią
12; => 5
(ns nasza) ; przełączenie przestrzeni nazw (def x 5) ; zmienna globalna x (powiązanie główne z wartością 5) (defn funk [] x) ; funkcja, która zwraca wartość zmiennej globalnej x (funk) ; wywołanie funkcji ; =&gt; 5 ; rezultat wywołania (ns inna) ; przełączenie przestrzeni nazw (funk) ; próba wywołania funkcji ; &gt;&gt; Unable to resolve symbol: funk in this context (nasza/funk) ; wywołanie funkcji symbolem z dookreśloną przestrzenią ; =&gt; 5

Zwróćmy uwagę, że po przełączeniu bieżącej przestrzeni nazw na przestrzeń inna utraciliśmy widoczność wartości powiązanej ze zmienną globalną funk nazywającą funkcję. Powiązanie nie zniknęło, dlatego korzystając z formy symbolowej z dookreśloną przestrzenią możemy z niego skorzystać.

Zasięg leksykalny

zasięgiem leksykalnym (ang. lexical scope) mamy do czynienia w przypadku powiązań leksykalnych (ang. lexical bindings). Możliwość korzystania z powiązań objętych tym zasięgiem zależy od umiejscowienia identyfikujących je symbolikodzie źródłowym.

Zasięg leksykalny jest wykorzystywany w wielu językach programowania. Można mówić wtedy np. o lokalnym zasięgu leksykalnym (w obrębie ciała funkcji czy pewnego bloku kodu).

W Clojure zasięg leksykalny:

  • tworzymy wprost z użyciem formy specjalnej let lub podobnych;
  • tworzony jest automatycznie dla parametrów funkcji i makr.
Forma let i wektor powiązań

Korzystając z formy specjalnej let, możemy tworzyć powiązania leksykalne, których zasięg będzie ograniczony do S-wyrażeń podanych jako jej argumenty.

Forma let jest bardzo często używana w Clojure i w innych dialektach języka Lisp. Można powiedzieć, że obok form tworzących funkcje czy listy jest jedną z fundamentalnych konstrukcji języka. Dzięki niej możemy pisać czytelny, deklaratywny kod i nadawać wartościom symboliczne etykiety w wybranych obszarach programu.

Użycie:

  • (let wektor-powiązań & wyrażenie…),

gdzie wektor-powiązań to:

  • [forma-powiązaniowa wyrażenie-inicjujące …].

Pierwszym argumentem, jaki należy przekazać konstrukcji let, jest wektor powiązań (ang. binding vector). Jest to wektorowe S-wyrażenie, które powinno składać się z tzw. par powiązaniowych (ang. binding pairs). Pierwsze elementy tych par powinny być formami powiązaniowymi, a drugie tzw. wyrażeniami inicjującymi (ang. initialization expressions), które zostaną przeliczone do wartości stałych.

Formy powiązaniowe w wektorze powiązań formy let możemy wyrażać z użyciem:

Symbole powinny wyrażać formy powiązaniowe, a więc występować w postaci niezacytowanej, natomiast mapy bądź wektory mają zastosowanie w przypadku tzw. dekompozycji (zwanej też destrukturyzacją), która omówiona będzie później i pozwala na tworzenie abstrakcyjnych powiązań strukturalnych. Znajdują one zastosowanie wtedy, gdy zachodzi potrzeba powiązania symboli z wartościami konkretnych elementów pochodzących z wieloelementowych struktur.

Przykład użycia formy specjalnej let
1(let [a 1         ; powiązanie symbolu a z wartością 1
2      b (inc a)   ; powiązanie symbolu b z wartością a+1
3      c 3]        ; powiązanie symbolu c z wartością 3
4  (+ a b c))      ; powiązania widoczne tylko w wyrażeniu let
5; => 6
(let [a 1 ; powiązanie symbolu a z wartością 1 b (inc a) ; powiązanie symbolu b z wartością a+1 c 3] ; powiązanie symbolu c z wartością 3 (+ a b c)) ; powiązania widoczne tylko w wyrażeniu let ; =&gt; 6

Przypisane do form powiązaniowych wartości mogą być reprezentowane dowolnymi S-wyrażeniami, które staną się poprawnymi formami. Nazywamy je w tym kontekście wyrażeniami inicjującymi. W wyrażeniach inicjujących możemy odwoływać się do symboli, które zostały powiązane z wartościami na wcześniejszych pozycjach wektora powiązaniowego.

Kolejne, opcjonalne argumenty let to S-wyrażenia do przeliczenia, w których można korzystać z powiązanych wcześniej symboli. Gdy dany symbol zostanie podany, jego forma symbolowa zostanie przeliczona do wartości.

Warto zaznaczyć, że let wprowadza lokalne powiązania leksykalne, a nie obiekty typu Var. Kompilator reprezentuje i optymalizuje te lokalne wartości zależnie od środowiska docelowego; ich umieszczenie na stosie JVM, w lokalnych slotach czy na stercie nie jest częścią kontraktu języka Clojure.

Możemy przesłaniać wartości powiązań leksykalnych tworząc nowe, które bazują na tych samych symbolicznych nazwach:

Przykład przesłaniania powiązań leksykalnych
1(let [a 1         ; powiązanie symbolu a z wartością 1
2      b (inc a)   ; powiązanie symbolu b z wartością powiązaną z a + 1
3      a b]        ; powiązanie symbolu a z wartością powiązaną z b
4  a)
5; => 2
(let [a 1 ; powiązanie symbolu a z wartością 1 b (inc a) ; powiązanie symbolu b z wartością powiązaną z a + 1 a b] ; powiązanie symbolu a z wartością powiązaną z b a) ; =&gt; 2

Forma let ewaluowana jest do wartości ostatnio obliczonego S-wyrażenia lub wartości nil, jeżeli żadnego wyrażenia nie podano.

Leksykalny zasięg powiązań utworzonych w wektorze powiązań formy let jest ograniczony do wyrażeń inicjujących jej wektora i S-wyrażeń podanych jako jej argumenty. Zasięg każdego powiązania w wektorze rozpoczyna się od miejsca jego utworzenia – w wyrażeniach inicjujących wektora możemy korzystać z powiązań powstałych na wcześniejszych pozycjach.

Powiązanie warunkowe, if-let

Makro if-let działa podobnie jak forma specjalna let, czyniąc wewnętrznie użytek z formy if. Umożliwia tworzenie jednego powiązania leksykalnego widocznego w S-wyrażeniach, które będą wartościowane w zależności od tego, czy pochodząca z wyrażenia inicjującego wartość będzie reprezentowała logiczną prawdę czy fałsz.

Pierwszym argumentem makra if-let jest wektor powiązań, drugim powinno być S-wyrażenie, które zostanie przeliczone, jeżeli wartość wyrażenia inicjującego z wektora będzie prawdziwa (nie będzie równa false ani nil). Po nim może pojawić się opcjonalny trzeci argument, który zostanie przeliczony, jeżeli wartość drugiego okaże się fałszywa (równa false lub nil). Warto pamiętać, że w wyrażeniu tym nie można korzystać z powiązania, ponieważ nie zostanie ono utworzone.

Wartością zwracaną jest wartość ostatnio przeliczanego wyrażenia lub nil, jeżeli żadne wyrażenie nie było wartościowane (ponieważ np. nie był spełniony warunek prawdy, a nie podano dodatkowego wyrażenia do przeliczenia).

Użycie:

  • (if-let wektor-powiązań wyrażenie-prawda wyrażenie-fałsz?)
Przykłady użycia makra if-let
1(if-let [a 1]     a)         ; => 1
2(if-let [a 0]     a)         ; => 0
3(if-let [a false] a)         ; => nil
4(if-let [a nil]   a)         ; => nil
5(if-let [a nil]   a "brak")  ; => "brak"
(if-let [a 1] a) ; =&gt; 1 (if-let [a 0] a) ; =&gt; 0 (if-let [a false] a) ; =&gt; nil (if-let [a nil] a) ; =&gt; nil (if-let [a nil] a &#34;brak&#34;) ; =&gt; &#34;brak&#34;

Leksykalny zasięg powiązania utworzonego w wektorze powiązań formy if-let jest ograniczony do S-wyrażeń podanych jako jej argumenty.

Powiązanie funkcji, letfn

Makro letfn jest wersją formy specjalnej let, które pozwala definiować funkcje i dokonywać ich leksykalnych powiązań z symbolami w taki sposób, że stają się one widoczne we wszystkich wyrażeniach inicjujących danego wektora powiązań (nawet w umieszczonych wcześniej).

W prostych przypadkach możemy użyć let do powiązania symbolu z anonimową funkcją, a potem tę funkcję wywołać:

1(let [x (fn [a] (+ 2 a))]
2  (x 2))
3
4; => 4
(let [x (fn [a] (+ 2 a))] (x 2)) ; =&gt; 4

Funkcje możemy też wywoływać w wektorze powiązań, podczas stwarzania powiązań, a więc zwracane wartości traktować jak wyrażenia inicjujące lub ich składniki:

1(let [x (fn [a] (+ 2 a))       ; funkcja x
2      y (fn [a] (+ 3 (x a)))]  ; funkcja y korzysta z funkcji x
3  (y 2))                       ; wywołanie funkcji y
4
5; => 7
(let [x (fn [a] (+ 2 a)) ; funkcja x y (fn [a] (+ 3 (x a)))] ; funkcja y korzysta z funkcji x (y 2)) ; wywołanie funkcji y ; =&gt; 7

Spójrzmy jednak co się stanie, gdy w wektorze powiązań odwołamy się do funkcji wcześniej, niż doszło do powiązania jej z symbolem:

1(let [y (fn [a] (+ 3 (x a)))  ; funkcja y korzysta z funkcji x
2      x (fn [a] (+ 2 a))]     ; funkcja x
3  (y 2))                      ; wywołanie funkcji y
4
5; >> java.lang.RuntimeException:
6; >> Unable to resolve symbol: x in this context
(let [y (fn [a] (+ 3 (x a))) ; funkcja y korzysta z funkcji x x (fn [a] (+ 2 a))] ; funkcja x (y 2)) ; wywołanie funkcji y ; &gt;&gt; java.lang.RuntimeException: ; &gt;&gt; Unable to resolve symbol: x in this context

Widzimy, że nie jest to możliwe, bo wyrażenia wektora powiązań przetwarzane są w określonej kolejności. Jednak są pewne dziedziny zastosowań, gdzie musimy odwoływać się do obiektu funkcji, która dopiero zostanie zdefiniowana (np. w tzw. rekurencji wzajemnej). W takich przypadkach z pomocą przychodzi letfn.

Użycie:

  • (letfn wektor-specyfikacji-funkcji & wyrażenie…);

gdzie wektor-specyfikacji-funkcji to:

  • [(nazwa wektor-parametryczny wyrażenie…)],
  • [(nazwa (wektor-parametryczny wyrażenie…)+)].

Drugi wariant wektora specyfikacji funkcji służy do tworzenia tzw. funkcji wieloczłonowych, które są omówione w rozdziale poświęconym funkcjom.

Przykład użycia makra letfn
1(letfn [(y [a] (+ 3 (x a)))
2        (x [a] (+ 2 a))]
3  (y 2))
4
5; => 7
(letfn [(y [a] (+ 3 (x a))) (x [a] (+ 2 a))] (y 2)) ; =&gt; 7

Leksykalny zasięg powiązań utworzonych w wektorze powiązań formy letfn jest ograniczony do wyrażeń inicjujących jej wektora i S-wyrażeń podanych jako jej argumenty. Zasięg każdego powiązania w wektorze jest obejmuje cały wektor – w wyrażeniach inicjujących wektora możemy korzystać z każdego umieszczonego w nim powiązania bez względu na kolejność powstawania.

Zobacz także:

Powiązanie warunkowe, when-let

Makro when-let jest wersją formy specjalnej let, które wewnętrznie korzysta z makra when. Umożliwia ono tworzenie jednego powiązania leksykalnego widocznego w wyrażeniach, które będą wartościowane pod warunkiem, że pochodząca z wyrażenia inicjującego wartość będzie reprezentowała logiczną prawdę (nie będzie równa false ani nil).

Pierwszy argument when-let powinien być wektorem powiązań zawierającym dokładnie jedną parę powiązaniową, a każdy następny zostanie potraktowany jak wyrażenie, które ma być obliczone i w którym można korzystać z formy symbolowej odwołującej się do powiązanej w wektorze wartości.

Makro zwraca wartość ostatnio wartościowanego wyrażenia lub nil, gdy nie doszło do wartościowania, ponieważ warunek prawdy nie został spełniony.

Użycie:

  • (when-let wektor-powiązań & wyrażenie…).
Przykłady użycia makra when-let
1(when-let [a 0]     (str "mam " a))  ; => "mam 0"
2(when-let [a 1]     (str "mam " a))  ; => "mam 1"
3(when-let [a nil]   (str "mam " a))  ; => nil
4(when-let [a false] (str "mam " a))  ; => nil
(when-let [a 0] (str &#34;mam &#34; a)) ; =&gt; &#34;mam 0&#34; (when-let [a 1] (str &#34;mam &#34; a)) ; =&gt; &#34;mam 1&#34; (when-let [a nil] (str &#34;mam &#34; a)) ; =&gt; nil (when-let [a false] (str &#34;mam &#34; a)) ; =&gt; nil

Leksykalny zasięg powiązania utworzonego w wektorze powiązań formy when-let jest ograniczony do S-wyrażeń podanych jako jej argumenty.

Powiązanie 1-go niepustego, when-first

Makro when-first jest wersją makra when-let. Umożliwia tworzenie powiązania leksykalnego widocznego w wyrażeniach, które będą wartościowane pod warunkiem, że pochodząca z wyrażenia inicjującego wartość będzie strukturą, którą da się przekształcić do niepustej sekwencji.

Pierwszy argument when-first powinien być wektorem powiązań, zawierającym dokładnie jedną parę powiązaniową, a każdy następny zostanie potraktowany jak wyrażenie, które ma być obliczone i w którym można korzystać z formy symbolowej odwołującej się do powiązanej w wektorze wartości. Powiązany zostanie pierwszy element reprezentowany przez wyrażenie inicjujące.

Makro zwraca wartość ostatnio wartościowanego wyrażenia lub nil, gdy nie doszło do wartościowania, ponieważ warunek prawdy nie został spełniony.

Użycie:

  • (when-first wektor-powiązań & wyrażenie…).
Przykłady użycia makra when-first
1(when-first [a [0 1 2]]     (str "mam " a))  ; => "mam 0"
2(when-first [a [false 2 3]] (str "mam " a))  ; => "mam false"
3(when-first [a [nil 2 3]]   (str "mam " a))  ; => "mam "
4(when-first [a "123"]       (str "mam " a))  ; => "mam 1"
5(when-first [a nil]         (str "mam " a))  ; => nil
6(when-first [a []]          (str "mam " a))  ; => nil
7(when-first [a ""]          (str "mam " a))  ; => nil
(when-first [a [0 1 2]] (str &#34;mam &#34; a)) ; =&gt; &#34;mam 0&#34; (when-first [a [false 2 3]] (str &#34;mam &#34; a)) ; =&gt; &#34;mam false&#34; (when-first [a [nil 2 3]] (str &#34;mam &#34; a)) ; =&gt; &#34;mam &#34; (when-first [a &#34;123&#34;] (str &#34;mam &#34; a)) ; =&gt; &#34;mam 1&#34; (when-first [a nil] (str &#34;mam &#34; a)) ; =&gt; nil (when-first [a []] (str &#34;mam &#34; a)) ; =&gt; nil (when-first [a &#34;&#34;] (str &#34;mam &#34; a)) ; =&gt; nil

Uwaga: Makro when-first wywołuje funkcję seq na wartości wyrażenia inicjującego (drugim elemencie pary powiązaniowej) i mogą pojawiać się błędy, jeżeli taka operacja nie jest możliwa (np. podano liczbę całkowitą lub wartość logiczną).

Leksykalny zasięg powiązania utworzonego w wektorze powiązań formy when-first jest ograniczony do S-wyrażeń podanych jako jej argumenty.

Powiązanie wartościowych, when-some

Makro when-some jest wersją formy specjalnej let, które wewnętrznie korzysta z makra when. Umożliwia tworzenie jednego powiązania leksykalnego widocznego w wyrażeniach, które będą wartościowane pod warunkiem, że pochodząca z wyrażenia inicjującego wartość będzie różna od nil.

Pierwszy argument when-some powinien być wektorem powiązań, zawierającym dokładnie jedną parę powiązaniową, a każdy następny zostanie potraktowany jak wyrażenie, które ma być obliczone i w którym można korzystać z formy symbolowej odwołującej się do powiązanej w wektorze wartości.

Makro zwraca wartość ostatnio wartościowanego wyrażenia lub nil, gdy nie doszło do wartościowania, ponieważ warunek nie został spełniony.

Użycie:

  • (when-some wektor-powiązań & wyrażenie…).
Przykłady użycia makra when-some
1(when-some [a 0]     (str "mam " a))  ; => "mam 0"
2(when-some [a 1]     (str "mam " a))  ; => "mam 1"
3(when-some [a false] (str "mam " a))  ; => "mam false"
4(when-some [a nil]   (str "mam " a))  ; => nil
(when-some [a 0] (str &#34;mam &#34; a)) ; =&gt; &#34;mam 0&#34; (when-some [a 1] (str &#34;mam &#34; a)) ; =&gt; &#34;mam 1&#34; (when-some [a false] (str &#34;mam &#34; a)) ; =&gt; &#34;mam false&#34; (when-some [a nil] (str &#34;mam &#34; a)) ; =&gt; nil

Leksykalny zasięg powiązania utworzonego w wektorze powiązań formy when-some jest ograniczony do S-wyrażeń podanych jako jej argumenty.

Powiązanie wartościowych, if-some

Makro if-some jest wersją formy specjalnej let, które wewnętrznie korzysta z formy specjalnej if. Umożliwia tworzenie jednego powiązania leksykalnego widocznego w wyrażeniu, które będzie wartościowane pod warunkiem, że pochodząca z wyrażenia inicjującego wartość będzie różna od nil. Opcjonalnie można również podać drugie wyrażenie, które zostanie obliczone w przeciwnym razie.

Pierwszy argument if-some powinien być wektorem powiązań, zawierającym dokładnie jedną parę powiązaniową, a następny (także obowiązkowy) zostanie potraktowany jak wyrażenie, które ma być obliczone i w którym można korzystać z formy symbolowej odwołującej się do powiązanej w wektorze wartości, jeżeli wyrażenie inicjujące nie ma wartości nil. Opcjonalny, trzeci argument powinien zawierać drugie wyrażenie, które zostanie wykonane, gdy wartością wyrażenia inicjującego będzie nil. Warto pamiętać, że nie będzie w nim widoczne powiązanie, ponieważ nie zostanie ono stworzone.

Makro zwraca wartość ostatnio wartościowanego wyrażenia lub nil, gdy nie doszło do wartościowania, ponieważ warunek nie został spełniony.

Użycie:

  • (if-some wektor-powiązań wyrażenie-nie-nil & wyrażenie-nil).
Przykłady użycia makra if-some
1(if-some [a 0]     (str "mam " a))         ; => "mam 0"
2(if-some [a 1]     (str "mam " a))         ; => "mam 1"
3(if-some [a false] (str "mam " a))         ; => "mam false"
4(if-some [a nil]   (str "mam " a))         ; => nil
5(if-some [a nil]   (str "mam " a) "brak")  ; => "brak"
(if-some [a 0] (str &#34;mam &#34; a)) ; =&gt; &#34;mam 0&#34; (if-some [a 1] (str &#34;mam &#34; a)) ; =&gt; &#34;mam 1&#34; (if-some [a false] (str &#34;mam &#34; a)) ; =&gt; &#34;mam false&#34; (if-some [a nil] (str &#34;mam &#34; a)) ; =&gt; nil (if-some [a nil] (str &#34;mam &#34; a) &#34;brak&#34;) ; =&gt; &#34;brak&#34;

Leksykalny zasięg powiązania utworzonego w wektorze powiązań formy if-some jest ograniczony do S-wyrażeń podanych jako jej argumenty.

Powiązanie w pętli, loop i recur

Forma specjalna loop działa podobnie do let, ale pozwala na rekurencyjne wykonywanie fragmentu programu. Przyjmuje jeden obowiązkowy argument, którym powinien być wektor powiązań i zero lub więcej argumentów będących wyrażeniami, w których można korzystać z powiązań leksykalnych utworzonych w wektorze. Wartością zwracaną jest wartość ostatnio obliczonego wyrażenia.

Powiązania używane w wyrażeniach wewnątrz loop mogą być aktualizowane w wywołaniu recur. Argumenty przekazywane do recur staną się nowymi wartościami powiązań o odpowiadających im w wektorze pozycjach podczas kolejnego, rekursywnego wywołania wyrażeń z loop. Dzięki temu możliwa jest tzw. rekurencja ogonowa, która nie wyczerpuje zasobów pamięciowych stosu.

Użycie:

  • (loop wektor-powiązań & wyrażenie…).
Przykład użycia formy specjalnej loop
1(loop [x 1]            ; pętla i powiązanie leksykalne
2  (when (< x 10)       ; warunek zakończenia rekurencji
3    (println x)        ; wyświetlenie; powiązanie widoczne tylko w pętli
4    (recur (inc x))))  ; zmiana powiązania x i skok na początek
(loop [x 1] ; pętla i powiązanie leksykalne (when (&lt; x 10) ; warunek zakończenia rekurencji (println x) ; wyświetlenie; powiązanie widoczne tylko w pętli (recur (inc x)))) ; zmiana powiązania x i skok na początek

Leksykalny zasięg powiązań utworzonych w wektorze powiązań formy loop jest ograniczony do wyrażeń inicjujących jej wektora (w kolejności ich występowania) oraz S-wyrażeń podanych jako jej argumenty. Zasięg każdego powiązania w wektorze rozpoczyna się od miejsca jego utworzenia – w wyrażeniach inicjujących wektora możemy korzystać z powiązań powstałych na wcześniejszych pozycjach.

Zobacz także:

Powiązanie parametryczne

Z leksykalnym zasięgiem spotkamy się również, gdy zdefiniujemy funkcję, która przyjmuje jakieś argumenty. Mówimy wtedy o powiązaniach parametrycznych (ang. parameter bindings), czyli o tworzeniu form powiązaniowych symboli w wektorach parametrycznych funkcji.

Przykład użycia parametrów funkcji
1(defn funk [a b]  ; definicja funkcji funk; parametry a i b
2  (+ a b))        ; widoczne tylko w ciele funkcji (w granicach S-wyrażenia)
3
4(fn [a b]         ; definicja funkcji anonimowej; parametry a i b
5  (+ a b))        ; widoczne tylko w ciele funkcji (w granicach S-wyrażenia)
(defn funk [a b] ; definicja funkcji funk; parametry a i b (+ a b)) ; widoczne tylko w ciele funkcji (w granicach S-wyrażenia) (fn [a b] ; definicja funkcji anonimowej; parametry a i b (+ a b)) ; widoczne tylko w ciele funkcji (w granicach S-wyrażenia)

Leksykalny zasięg powiązań parametrycznych z wartościami przekazywanymi jako argumenty podczas wywołań jest ograniczony do ciała funkcji.

Zmienne lokalne, with-local-vars

Obiekty typu Var możemy w drodze wyjątku objąć lokalnym zasięgiem leksykalnym. Z tego typu konstrukcji skorzystamy, gdy będziemy chcieli wyrazić jakiś problem imperatywnie i w związku z tym zajdzie konieczność użycia odpowiednika lokalnych zmiennych. Służy do tego makro with-local-vars, które dokładniej omówiono w rozdziale poświęconym obiektom typu Var i zmiennym.

Użycie:

  • (with-local-vars wektor-powiązań wyrażenie)
Przykład użycia makra with-local-vars
1(with-local-vars [a 1] @a)
2; => 1
(with-local-vars [a 1] @a) ; =&gt; 1

Leksykalny zasięg symboli wprowadzonych przez with-local-vars jest ograniczony do jego ciała. Inaczej niż w let, wyrażenia inicjujące nie mogą zależeć od wartości wprowadzonych wcześniej w tym samym wektorze: podczas ich wartościowania lokalne Vary nie mają jeszcze tych powiązań wątku. Dereferencja takiego lokalnego Vara zwraca wewnętrzny znacznik Var$Unbound, a nie wartość wcześniejszego inicjatora. Wartości zależne należy obliczyć przed wejściem do with-local-vars albo w jego ciele.

Zobacz także:

Powiązanie wejścia/wyjścia, with-open

Makro with-open tworzy powiązania leksykalne podobnie do let, a następnie gwarantuje wywołanie close() dla każdego zainicjowanego zasobu przy opuszczaniu ciała. Zasoby są zamykane w odwrotnej kolejności, również wtedy, gdy wartościowanie zgłosi wyjątek.

Użycie:

  • (with-open wektor-powiązań & wyrażenie…),

gdzie wektor-powiązań to:

  • [forma-powiązaniowa-symbolu wyrażenie-inicjujące …].

Pierwszym argumentem, jaki należy przekazać konstrukcji with-open, jest wektor powiązań (ang. binding vector). Jest to wektorowe S-wyrażenie, które powinno składać się z tzw. par powiązaniowych (ang. binding pairs).

Pierwsze elementy tych par powinny być formami powiązaniowymi symboli (a nie jak w przypadku let dowolnymi formami powiązaniowymi), a drugie tzw. wyrażeniami inicjującymi (ang. initialization expressions), które zostaną przeliczone do wartości stałych.

Przykład użycia makra with-open
1(with-open [czytnik (clojure.java.io/reader "plik.txt")]
2  (doall (line-seq czytnik)))
(with-open [czytnik (clojure.java.io/reader &#34;plik.txt&#34;)] (doall (line-seq czytnik)))

W wyrażeniach inicjujących możemy odwoływać się do symboli, które zostały powiązane z wartościami na wcześniejszych pozycjach wektora powiązaniowego.

Kolejne, opcjonalne argumenty with-open to S-wyrażenia do przeliczenia, w których można korzystać z powiązanych wcześniej symboli. Gdy dany symbol zostanie podany, jego forma symbolowa zostanie przeliczona do wartości.

Podobnie jak w formie let mamy do czynienia z lokalnymi powiązaniami służącymi do identyfikacji zainicjowanych zasobów. Makro emituje wywołania close() w zagnieżdżonych blokach finally, dzięki czemu poprawnie zainicjowane zasoby są zamykane nawet wtedy, gdy ciało lub późniejsze zamknięcie zgłosi wyjątek.

Zasięg dynamiczny

Powiązanie dynamiczne w Clojure tymczasowo dostarcza wartość istniejącemu dynamicznemu Varowi. W wyniku rozpoznania leksykalnego symbol nadal wskazuje ten sam Var odwzorowany w przestrzeni nazw, lecz dereferencja Vara wybiera najbardziej wewnętrzne aktywne powiązanie wątku. Wybór ten jest widoczny dla funkcji wywoływanych z ciała, nawet jeżeli zostały zdefiniowane gdzie indziej — stąd określenie „zasięg dynamiczny”.

Makro binding odkłada żądane powiązania wątku przed wartościowaniem ciała i przywraca poprzednie w ścieżce finally. Zagnieżdżone formy binding przesłaniają więc zewnętrzne zgodnie z porządkiem stosu. Gdy żadne powiązanie wątku nie jest aktywne, używane jest powiązanie główne Vara.

Powiązania wątku nie są odwzorowaniami przestrzeni nazw i nie zmieniają współdzielonego powiązania głównego Vara. Nowe wątki nie dziedziczą automatycznie dowolnych powiązań wątku, choć mechanizmy Clojure, takie jak future’y i dyspozycja agentów, mogą jawnie przenosić ramkę powiązań wywołującego. Kod powinien polegać na udokumentowanym zachowaniu używanego mechanizmu współbieżności, zamiast traktować powiązania dynamiczne jak stan globalny procesu.

Tworzenie powiązań dynamicznych

Do tworzenia powiązań o zasięgu dynamicznym służy makro binding, które dokładniej omówione jest w rozdziale VII. Poniżej znajdziemy przykład użycia, który udowadnia przy okazji, że powiązania tego typu utrzymywane są w obiektach referencyjnych, a nie w przestrzeniach nazw:

Przykład użycia powiązań dynamicznych
 1(def ^:dynamic *x* 5)                   ; zmienna dynamiczna *x*
 2
 3(def obiekt-x                           ; zmienna globalna obiekt-x
 4  (var *x*))                            ; wskazuje na obiekt Var zmiennej *x*
 5
 6(defn chwal-się []                      ; wyświetla wartość *x*
 7  (println " *x* po nazwie:"
 8           *x* "\n"                     ; odczyt powiązanej wartości
 9           "*x* po obiekcie:"
10           @obiekt-x "\n"))             ; dereferencja obiektu
11
12(defn testuj []                         ; funkcja testująca
13  (binding [*x* 10]                     ; zasięg dynamiczny *x*
14    (println "* zasięg dynamiczny")
15    (chwal-się))                        ; wywołanie funkcji w zasięgu
16  (println "* powiązanie główne:")       ; dynamicznym
17  (chwal-się))                          ; wywołanie funkcji poza
18                                        ; dynamicznym zasięgiem
19(testuj)
(def ^:dynamic *x* 5) ; zmienna dynamiczna *x* (def obiekt-x ; zmienna globalna obiekt-x (var *x*)) ; wskazuje na obiekt Var zmiennej *x* (defn chwal-się [] ; wyświetla wartość *x* (println &#34; *x* po nazwie:&#34; *x* &#34;\n&#34; ; odczyt powiązanej wartości &#34;*x* po obiekcie:&#34; @obiekt-x &#34;\n&#34;)) ; dereferencja obiektu (defn testuj [] ; funkcja testująca (binding [*x* 10] ; zasięg dynamiczny *x* (println &#34;* zasięg dynamiczny&#34;) (chwal-się)) ; wywołanie funkcji w zasięgu (println &#34;* powiązanie główne:&#34;) ; dynamicznym (chwal-się)) ; wywołanie funkcji poza ; dynamicznym zasięgiem (testuj)

Zobacz także:

Dekompozycja

Dekompozycja (ang. decomposition), zwana też destrukturyzacją (ang. destructuring) jest mechanizmem tworzenia powiązań wartości z symbolami, w którym wartości te pochodzą ze struktur złożonych z wielu elementów, a do przypisywania wybranych z nich do konkretnych symboli używa się specyficznej składni zamiast wywoływać funkcje czy makra.

Z dekompozycji możemy korzystać w wektorze powiązań formy specjalnej let i makr o podobnym kontrakcie, wektorze parametrycznym formy fn oraz makra defn, a także w konstrukcjach, które jawnie obsługują formy destrukturyzacyjne (np. for). Zamiast pojedynczych form powiązaniowych symboli pojawiają się wtedy formy powiązaniowe wektorów, formy powiązaniowe map albo ich kombinacje. Makro binding jest wyjątkiem: wymaga prostych symboli dynamicznych i nie obsługuje destrukturyzacji.

Aby zademonstrować korzyści ze stosowania destrukturyzacji, spójrzmy na prosty przykład, w którym najpierw dokonujemy powiązania elementów wektora z symbolami (korzystając z funkcji operujących na wektorze), a następnie używamy w tym celu celu dekompozycji.

Porównanie dekompozycji i samodzielnego powiązywania wartości
 1(def dane [1 2 3])  ; wektor z trzema wartościami
 2
 3;; ręczne powiązywanie wybranych elementów z symbolami 
 4
 5(let [a (first dane)
 6      b (first (rest dane))
 7      c (first (rest (rest dane)))]
 8  (list a b c))
 9; => (1 2 3)
10
11;; dekompozycja
12
13(let [[a b c] dane] (list a b c))
14; => (1 2 3)
(def dane [1 2 3]) ; wektor z trzema wartościami ;; ręczne powiązywanie wybranych elementów z symbolami (let [a (first dane) b (first (rest dane)) c (first (rest (rest dane)))] (list a b c)) ; =&gt; (1 2 3) ;; dekompozycja (let [[a b c] dane] (list a b c)) ; =&gt; (1 2 3)

W przedostatniej linii widzimy, że zamiast pojedynczego symbolu użyliśmy wektorowego S-wyrażenia zawierającego listę symboli, które zostały powiązane z odpowiadającymi im pozycyjnie wartościami wektora o nazwie dane.

Dekompozycja jest rodzajem powiązywania symboli z wartościami. Również mamy do czynienia z parami powiązaniowymi przy czym:

  • w miejscu pojedynczego symbolu pojawia się (zawierające różne symbole) wyrażenie powiązaniowe (wektorowe wyrażenie powiązaniowe lub mapowe wyrażenie powiązaniowe), czyli forma powiązaniowa wektora lub mapy;

  • wartością przypisanego wyrażenia inicjującego jest wieloelementowa struktura (np. wektor, mapa, lista, rekord itp.).

Nazwy wyrażeń i form powiązaniowych na przykładzie wywołania let
1(let [         ; wektor powiązań:
2               ; · pierwsza para:
3      dane     ;   · symbol (forma powiązaniowa symbolu)
4      [1 2 3]  ;   · wyrażenie inicjujące (forma wektorowa)
5               ; · druga para:
6      [a b c]  ;   · wektorowe wyrażenie powiązaniowe (forma powiązaniowa wektora)
7      dane]    ;   · wyrażenie inicjujące (forma symbolowa)
8  (list a b c))
(let [ ; wektor powiązań: ; · pierwsza para: dane ; · symbol (forma powiązaniowa symbolu) [1 2 3] ; · wyrażenie inicjujące (forma wektorowa) ; · druga para: [a b c] ; · wektorowe wyrażenie powiązaniowe (forma powiązaniowa wektora) dane] ; · wyrażenie inicjujące (forma symbolowa) (list a b c))

Dekompozycja pozycyjna

Dekompozycja pozycyjna (ang. positional decomposition), zwana też destrukturyzacją pozycyjną (ang. positional destructuring) umożliwia tworzenie powiązań symboli z wartościami wybranych elementów struktur o sekwencyjnym interfejsie dostępu (np. wektorów, list czy nawet łańcuchów znakowych). Przypomina korzystanie z wzorców dopasowania i polega na kojarzeniu podanych w pewnym porządku symboli z odpowiadającymi im pozycyjnie elementami struktury podanej w wyrażeniu inicjującym.

Dekompozycji pozycyjnej możemy używać zarówno w wektorach powiązań konstrukcji obsługujących formy destrukturyzacyjne (takich jak let), jak również w wektorach parametrycznych definicji funkcji.

Tak naprawdę możemy destrukturyzować nie tylko sekwencje, lecz dowolne kolekcje, na których da się operować funkcją nth.

Wektorowa forma powiązaniowa

Korzystanie z dekompozycji pozycyjnej wymaga umieszczenia wektorowego wyrażenia powiązaniowego (ang. vector binding expression) w miejscu, w którym zwykle podajemy pojedynczy symbol (jako pierwszy element pary powiązaniowej). Powinno to być wektorowe S-wyrażenie zawierające formy powiązaniowe (np. niezacytowane symbole), których pozycje odpowiadają pozycjom elementów ze źródłowej struktury (podanej jako wyrażenie inicjujące).

Użycie:

  • [[symbol…] wyrażenie-inicjujące].
Przykłady dekompozycji pozycyjnej w konstrukcji let
 1(let [[a b c] [1 2 3]]        ; a -> 1, b -> 2, c -> 3
 2  (list a b c))
 3; => (1 2 3)
 4
 5(let
 6  [wektor    [4 5 6]      ; powiązanie z wektorem 
 7  sekwencja (seq wektor)  ; powiązanie z sekwencją na bazie wektora
 8  [a b c]   [1 2 3]       ; powiązania z dekompozycji wyrażenia wektorowego
 9  [d e f]   wektor        ; powiązania z dekompozycji wektora
10  [g    ]   sekwencja]    ; powiązania z dekompozycji sekwencji
11
12  (list a b c d e f g))   ; utworzenie listy z wartościami powiązań
13; => (1 2 3 4 5 6 4)
(let [[a b c] [1 2 3]] ; a -&gt; 1, b -&gt; 2, c -&gt; 3 (list a b c)) ; =&gt; (1 2 3) (let [wektor [4 5 6] ; powiązanie z wektorem sekwencja (seq wektor) ; powiązanie z sekwencją na bazie wektora [a b c] [1 2 3] ; powiązania z dekompozycji wyrażenia wektorowego [d e f] wektor ; powiązania z dekompozycji wektora [g ] sekwencja] ; powiązania z dekompozycji sekwencji (list a b c d e f g)) ; utworzenie listy z wartościami powiązań ; =&gt; (1 2 3 4 5 6 4)

W przypadku wektorów parametrycznych, z którymi mamy do czynienia np. w definicjach funkcji, wyrażeniem inicjującym będzie zestaw przekazywanych argumentów.

Przykład wektora parametrycznego w definicji funkcji
1(defn funkcja [a b c]
2  (list a b c))
3
4(funkcja 1 2 3)
5; => (1 2 3)
(defn funkcja [a b c] (list a b c)) (funkcja 1 2 3) ; =&gt; (1 2 3)

Ignorowanie elementów

Zauważmy, że w linii nr 9 przedostatniego przykładu podajemy tylko jeden symbol (g), natomiast sekwencja źródłowa zawiera trzy wartości (4, 5, 6). Zgodnie z oczekiwaniami powiązana z symbolem zostanie pierwsza z nich. Jednak czy istnieje możliwość, aby pobrać tylko wybraną, ignorując pozostałe? Z pomocą przychodzi tu symbol _, który oznacza, że element o odpowiadającej mu pozycji powinien być zignorowany.

Użycie:

  • [[… _…] wyrażenie-inicjujące].
Przykład użycia symbolu _ w dekompozycji pozycyjnej
1(let [[_ b _] [1 2 3]]  ; powiązania z dekompozycji
2  b)                    ; wartościowanie formy symbolowej
3; => 2
(let [[_ b _] [1 2 3]] ; powiązania z dekompozycji b) ; wartościowanie formy symbolowej ; =&gt; 2

Warto wiedzieć, że symbol _ ma specjalne znaczenie tylko na zasadzie konwencji. W jego miejsce można podać dowolny inny symbol, który nie będzie używany, a jego wartość może zostać wielokrotnie przesłonięta bez uszczerbku na logice aplikacji.

Grupowanie elementów

Ciekawym przypadkiem jest grupowanie wszystkich pozostałych, nieprzypisanych pozycyjnie wartości w jednym, wariadycznym powiązaniu. Służy do tego symbol ampersandu umieszczony przed nazwą symbolu.

Użycie:

  • [[… & symbol] wyrażenie-inicjujące].
Przykład użycia symbolu & w dekompozycji pozycyjnej
1(let [[_ & reszta] [1 2 3]]  ; powiązania z dekompozycji
2  reszta)                    ; wartościowanie formy symbolowej
3; => (2 3)
(let [[_ &amp; reszta] [1 2 3]] ; powiązania z dekompozycji reszta) ; wartościowanie formy symbolowej ; =&gt; (2 3)

Dostęp do oryginalnej sekwencji

Może zdarzyć się tak, że pomimo dekompozycji będziemy potrzebowali dostępu do oryginalnie przekazywanej struktury danych wyrażenia inicjującego. Z pomocą przychodzi tu dyrektywa :as, która powinna być umieszczona w wyrażeniu destrukturyzacyjnym. Tuż za nią powinien znajdować się niezacytowany symbol, z którym struktura powinna być powiązana.

Użycie:

  • [[… :as symbol] wyrażenie-inicjujące].
Przykład użycia dyrektywy :as w dekompozycji pozycyjnej
1(let [[a b c :as wszystko] [1 2 3]] ; powiązania z dekompozycji
2  wszystko)                         ; wartościowanie formy symbolowej
3; => [1 2 3]
(let [[a b c :as wszystko] [1 2 3]] ; powiązania z dekompozycji wszystko) ; wartościowanie formy symbolowej ; =&gt; [1 2 3]

Dyrektywa :as i przypisany do niej symbol powinny być podane jako ostatnia para w wektorze dekompozycyjnym.

Dekompozycja asocjacyjna

Dekompozycja asocjacyjna (ang. associative decomposition), zwana też destrukturyzacją asocjacyjną (ang. associative destructuring) umożliwia tworzenie powiązań symboli z wartościami pochodzącymi z wybranych elementów struktur o asocjacyjnym interfejsie dostępu (np. map czy rekordów).

Dekompozycji asocjacyjnej możemy używać zarówno w wektorach powiązań konstrukcji obsługujących destrukturyzację (np. formy specjalnej let), jak również w wektorach parametrycznych definicji funkcji.

Mapowa forma powiązaniowa

Struktury asocjacyjne (np. mapy) wyrażają relację klucz–wartość, a ich dekompozycja polega na określeniu kluczy, pod którymi znaleźć można wartości powiązywane z podanymi symbolami.

Do wyrażania tej operacji służy mapowe wyrażenie powiązaniowe (ang. map binding expression), które skrótowo można nazywać mapą powiązaniową (ang. binding map). Jest to mapowe S-wyrażenie, które należy umieścić jako pierwszy element każdej pary powiązaniowej w wektorze powiązań lub zamiast pojedynczego parametru w wektorze parametrycznym funkcji. Kluczami mapy mogą być formy powiązaniowe (np. symboli, map czy wektorów), a wartościami klucze źródłowej struktury, pod którymi odnajdziemy właściwe wartości inicjujące lub dalsze struktury.

Źródłową strukturą, z której pobierane będą wartości w celu ich powiązania z symbolami lub dalszego destrukturyzowania, będzie mapowe wyrażenie inicjujące podane jako drugi element każdej pary powiązaniowej.

Użycie:

  • [{symbol klucz …} wyrażenie-inicjujące].
Przykład użycia mapy powiązaniowej
1;;          W E K T O R   P O W I Ą Z A Ń    (składający się z par)
2;;     mapa powiązaniowa  wyrażenie inicjujące  (pary powiązaniowe)
3
4(let [ {a :a b :b c :c}    {:a 1, :b 2, :c 3}  ]
5  (list a b c))
6; => (1 2 3)
;; W E K T O R P O W I Ą Z A Ń (składający się z par) ;; mapa powiązaniowa wyrażenie inicjujące (pary powiązaniowe) (let [ {a :a b :b c :c} {:a 1, :b 2, :c 3} ] (list a b c)) ; =&gt; (1 2 3)

Specyfikatorami kluczy mogą być również inne wartości, nie tylko słowa kluczowe.

Przykład użycia mapy powiązaniowej z symbolami jako specyfikatorami kluczy
1(let [{a 'a b 'b c 'c} '{a 1, b 2, c 3}]
2  (list a b c))
3; => (1 2 3)
(let [{a &#39;a b &#39;b c &#39;c} &#39;{a 1, b 2, c 3}] (list a b c)) ; =&gt; (1 2 3)

Zauważmy, że w powyższym przykładzie zastosowaliśmy cytowanie mapowego S-wyrażenia, aby nie musieć z osobna cytować każdego podanego w nim symbolu.

W przypadku wektorów parametrycznych funkcji wyrażenie inicjujące pochodzi z przekazywanych do funkcji argumentów, a kluczami są ich symboliczne nazwy.

Przykład użycia mapy powiązaniowej w wektorze parametrycznym
 1;;              WEKTOR  PARAMETRYCZNY
 2;;                mapa  powiązaniowa
 3
 4(defn funkcja [ & {a :a, b :b, c :c} ]
 5  (list a b c))
 6
 7;;        argumenty (wyrażenie inicjujące)
 8
 9(funkcja         :a 1, :b 2, :c 3)
10; => (1 2 3)
;; WEKTOR PARAMETRYCZNY ;; mapa powiązaniowa (defn funkcja [ &amp; {a :a, b :b, c :c} ] (list a b c)) ;; argumenty (wyrażenie inicjujące) (funkcja :a 1, :b 2, :c 3) ; =&gt; (1 2 3)

Klucze mapy powiązaniowej

Jeżeli nazwy symboli, z którymi będą powiązywane wartości pochodzące z podanej struktury asocjacyjnej, mają być takie same jak nazwy kluczy w tej mapie, to możemy skorzystać z dyrektywy :keys. Pozwala ona uniknąć powtórzeń i uczytelnia kod.

W mapie powiązaniowej należy podać parę, której kluczem jest słowo kluczowe :keys, a przypisaną wartością wektorowe S-wyrażenie zawierające niezacytowane symbole lub słowa kluczowe określające klucze dekomponowanej struktury, których wartości chcemy powiązać z symbolami o takich samych nazwach.

Użycie:

  • [{:keys [klucz…]} wyrażenie-inicjujące].
Przykłady użycia dyrektywy :keys
1(let [{:keys [:a :b :c]} {:a 1, :b 2, :c 3}]
2  (list a b c))
3; => (1 2 3)
4
5(let [{:keys [a b c]} {:a 1, :b 2, :c 3}]
6  (list a b c))
7; => (1 2 3)
(let [{:keys [:a :b :c]} {:a 1, :b 2, :c 3}] (list a b c)) ; =&gt; (1 2 3) (let [{:keys [a b c]} {:a 1, :b 2, :c 3}] (list a b c)) ; =&gt; (1 2 3)

Kluczami dekomponowanej struktury inicjującej mogą być również łańcuchy znakowe lub symbole. W takich przypadkach można zamiast z :keys użyć dyrektywy :strs albo :syms. W obydwu przypadkach, specyfikując nazwy, należy skorzystać z niezacytowanych symboli.

Użycie:

  • [{:strs [klucz…]} wyrażenie-inicjujące],
  • [{:syms [klucz…]} wyrażenie-inicjujące].
Przykłady użycia dyrektyw :strs i :syms
1(let [{:strs [a b c]} {"a" 1, "b" 2, "c" 3}]
2  (list a b c))
3; => (1 2 3)
4
5(let [{:syms [a b c]} '{a 1, b 2, c 3}]
6  (list a b c))
7; => (1 2 3)
(let [{:strs [a b c]} {&#34;a&#34; 1, &#34;b&#34; 2, &#34;c&#34; 3}] (list a b c)) ; =&gt; (1 2 3) (let [{:syms [a b c]} &#39;{a 1, b 2, c 3}] (list a b c)) ; =&gt; (1 2 3)

Dostęp do oryginalnej asocjacji

Może zdarzyć się, że pomimo dekompozycji będziemy potrzebowali dostępu do oryginalnie przekazywanej struktury danych. Podobnie jak w przypadku dekompozycji pozycyjnej z pomocą przychodzi tu dyrektywa :as. Powinna ona być umieszczona w mapie powiązaniowej, a przypisaną do niej wartością musi być niezacytowany symbol, z którym powiązana zostanie struktura wyrażenia inicjującego.

Użycie:

  • [{:as symbol} wyrażenie-inicjujące].
Przykład użycia dyrektywy :as w dekompozycji asocjacyjnej
1(let [{:keys [a b c]
2       :as wszystko} [1 2 3]] ; powiązania z dekompozycji
3  wszystko)                   ; wartościowanie formy symbolowej
4; => [1 2 3]
(let [{:keys [a b c] :as wszystko} [1 2 3]] ; powiązania z dekompozycji wszystko) ; wartościowanie formy symbolowej ; =&gt; [1 2 3]

Dyrektywa :as i przypisany do niej symbol powinny być podane jako ostatnia para w wektorze dekompozycyjnym.

Wartości domyślne

W mapie powiązaniowej możemy określać wartości domyślne, które zostaną powiązane z symbolami, jeżeli w źródłowej strukturze nie odnaleziono podanych kluczy. Służy do tego dyrektywa :or.

Po słowie kluczowym :or należy podać mapę określającą wartości domyślne dla lokalnych symboli wprowadzanych przez formę powiązaniową. Kluczami mapy :or muszą być te symbole, a nie odpowiadające im słowa kluczowe ze struktury źródłowej.

Użycie:

  • [{:or {symbol-lokalny wartość …}} wyrażenie-inicjujące].
Przykład użycia dyrektywy :or
1(let [{:keys [:a :b :c]
2       :or {a 1, c 3}} {:b 2}]
3  (list a b c))
4; => (1 2 3)
(let [{:keys [:a :b :c] :or {a 1, c 3}} {:b 2}] (list a b c)) ; =&gt; (1 2 3)

Dekompozycja asocjacyjna wektorów

Istnieje możliwość zastosowania dekompozycji asocjacyjnej w odniesieniu do wektorów. W mapie powiązaniowej zamiast kluczy należy wtedy podać pozycje elementów źródłowej struktury sekwencyjnej wyrażone liczbami całkowitymi.

Użycie:

  • [{symbol pozycja …} wyrażenie-inicjujące].
Przykład dekompozycji asocjacyjnej wektora
1(let [{a 0 b 1 c 2} ["pierwszy" "drugi" "trzeci"]]
2  (list a b c))
3; => ("pierwszy" "drugi" "trzeci")
(let [{a 0 b 1 c 2} [&#34;pierwszy&#34; &#34;drugi&#34; &#34;trzeci&#34;]] (list a b c)) ; =&gt; (&#34;pierwszy&#34; &#34;drugi&#34; &#34;trzeci&#34;)

Wekotory jako klucze w dekompozycji

Ciekawym przykładem dekompozycji asocjacyjnej może być mapa powiązaniowe, w której kluczami są wektory.

Przykład dekompozycji asocjacyjnej z wektorowymi kluczami
1(let [{[a b c] :litery} {:litery [1 2 3]}]
2  (list a b c))
3; => (1 2 3)
(let [{[a b c] :litery} {:litery [1 2 3]}] (list a b c)) ; =&gt; (1 2 3)

Widzimy, że wartością wektorowego klucza w mapowej formie powiązaniowej jest słowo kluczowe (:litery), które będzie następnie odszukane w mapowym wyrażeniu inicjującym, a w stosunku do znalezionej wartości przeprowadzona zostanie dekompozycja pozycyjna. Ten rodzaj destrukturyzacji jest prostym przykładem możliwości stosowania zagnieżdżonych struktur w formach dekompozycyjnych.

Struktury zagnieżdżone

Dekompozycja struktur zagnieżdżonych możliwa jest dzięki składni pozwalającej zagnieżdżać mapowe i wektorowe wyrażenia powiązaniowe.

Przykład dekompozycji zagnieżdżonej struktury
 1(def dane-osobowe
 2  {:imię     "Paweł"
 3   :nazwisko "Wilk"
 4   :płeć     :m
 5   :kontakty {:telefony [123456, 543210]
 6              :e-maile  ["pw-at-gnu.org"]}})
 7
 8(let [{:keys [imię nazwisko płeć], {[telefon] :telefony
 9                                    [e-mail]  :e-maile} :kontakty}
10      dane-osobowe
11      nazwa-płci (if (= płeć :m) "mężczyzna" "kobieta")]
12  (println "Imię i nazwisko: " imię nazwisko)
13  (println "Płeć:            "    nazwa-płci)
14  (println "Telefon:         "       telefon)
15  (println "E-mail:          "        e-mail))
(def dane-osobowe {:imię &#34;Paweł&#34; :nazwisko &#34;Wilk&#34; :płeć :m :kontakty {:telefony [123456, 543210] :e-maile [&#34;pw-at-gnu.org&#34;]}}) (let [{:keys [imię nazwisko płeć], {[telefon] :telefony [e-mail] :e-maile} :kontakty} dane-osobowe nazwa-płci (if (= płeć :m) &#34;mężczyzna&#34; &#34;kobieta&#34;)] (println &#34;Imię i nazwisko: &#34; imię nazwisko) (println &#34;Płeć: &#34; nazwa-płci) (println &#34;Telefon: &#34; telefon) (println &#34;E-mail: &#34; e-mail))

Efektem działania powyższego przykładu będzie wyświetlenie następujących linii tekstu:

Imię i nazwisko: Paweł Wilk
Płeć:            mężczyzna
Telefon:		 123456
E-mail:			 pw-at-gnu.org

Zbadajmy poszczególne fragmenty wektora powiązań, aby lepiej zrozumieć, z jakimi operacjami mieliśmy do czynienia. Mamy w nim dwie pary powiązaniowe:

  • Mapa powiązaniowa, w której zachodzi dekompozycja i przypisane do niej wyrażenie inicjujące, którym jest forma symbolowa (dane-osobowe) reprezentująca zagnieżdżoną mapę z danymi osobowymi:
1{:keys [imię nazwisko płeć], {[telefon] :telefony
2                              [e-mail]  :e-maile} :kontakty}
3dane-osobowe
{:keys [imię nazwisko płeć], {[telefon] :telefony [e-mail] :e-maile} :kontakty} dane-osobowe
  • Forma powiązaniowa symbolu (nazwa-płci) i przypisane jej wyrażenie inicjujące, którym jest forma specjalna if (w zależności od wartości powiązanej z symbolem płeć emituje odpowiedni łańcuch tekstowy):
1nazwa-płci (if (= płeć :m) "mężczyzna" "kobieta")
nazwa-płci (if (= płeć :m) &#34;mężczyzna&#34; &#34;kobieta&#34;)

Druga para nie ma znaczenia dla destrukturyzacji, więc nie będziemy jej dalej omawiać. Przyjrzymy się za to bliżej parze pierwszej, gdzie mamy do czynienia z mapą powiązaniową złożoną z dwóch elementów (dwóch par typu klucz–wartość):

  • Dyrektywa :keys powiązująca z odpowiednimi symbolami wartości kluczy :imię, :nazwisko:płeć (z mapy identyfikowanej symbolem dane-osobowe):
1:keys [imię nazwisko płeć]
:keys [imię nazwisko płeć]
  • Mapa powiązaniowa, która dokonuje dekompozycji struktury identyfikowanej kluczem :kontakty z mapy dane-osobowe:
1{[telefon] :telefony
2 [e-mail]  :e-maile}
3:kontakty
{[telefon] :telefony [e-mail] :e-maile} :kontakty

Widzimy, że mapa powiązaniowa nie zawiera prostych form powiązaniowych (wyrażonych niezacytowanymi symbolami), lecz dwa wektorowe wyrażenia powiązaniowe, które są kolejnym poziomem destrukturyzacji. Mamy do czynienia z dekompozycją pozycyjną, a dokładniej z przypisaniem symbolowi telefon pierwszego elementu struktury identyfikowanej kluczem :telefony oraz symbolowi e-mail pierwszego elementu struktury identyfikowanej kluczem :e-maile. Obie te struktury (wektor zawierający numery telefonów i wektor zawierający adresy e-mailowe) powinny być elementami mapy identyfikowanej kluczem :kontakty w strukturze nadrzędnej.

Klucze w pełni kwalifikowane

W Clojure od wersji 1.6 możemy korzystać z kluczy i symboli o dookreślonych przestrzeniach nazw.

Przykłady dekompozycji z kluczami o dookreślonych przestrzeniach nazw
 1(def dane-osobowe
 2  {:imię "Paweł"
 3   :kontakty/telefony [123456, 543210]
 4   :kontakty/e-maile  ["pw-at-gnu.org"]})
 5
 6(let [{:keys [imię],
 7       [telefon] :kontakty/telefony,
 8       [e-mail]  :kontakty/e-maile} dane-osobowe]
 9  (println "Imię:   "    imię)
10  (println "Telefon:" telefon)
11  (println "E-mail: "  e-mail))
12
13(let [{:keys [imię kontakty/telefony kontakty/e-maile]} dane-osobowe]
14  (println "Imię:    "     imię)
15  (println "Telefony:" telefony)
16  (println "E-maile: "  e-maile))
(def dane-osobowe {:imię &#34;Paweł&#34; :kontakty/telefony [123456, 543210] :kontakty/e-maile [&#34;pw-at-gnu.org&#34;]}) (let [{:keys [imię], [telefon] :kontakty/telefony, [e-mail] :kontakty/e-maile} dane-osobowe] (println &#34;Imię: &#34; imię) (println &#34;Telefon:&#34; telefon) (println &#34;E-mail: &#34; e-mail)) (let [{:keys [imię kontakty/telefony kontakty/e-maile]} dane-osobowe] (println &#34;Imię: &#34; imię) (println &#34;Telefony:&#34; telefony) (println &#34;E-maile: &#34; e-maile))
Przykład dekompozycji z kluczami z bieżącej przestrzeni nazw
 1(ns user)
 2(def dane-osobowe
 3  {:imię "Paweł"
 4   ::telefony [123456, 543210]
 5   ::e-maile  ["pw-at-gnu.org"]})
 6
 7(let [{:keys [imię ::telefony user/e-maile]} dane-osobowe]
 8  (println "Imię:    "     imię)
 9  (println "Telefony:" telefony)
10  (println "E-maile: "  e-maile))
(ns user) (def dane-osobowe {:imię &#34;Paweł&#34; ::telefony [123456, 543210] ::e-maile [&#34;pw-at-gnu.org&#34;]}) (let [{:keys [imię ::telefony user/e-maile]} dane-osobowe] (println &#34;Imię: &#34; imię) (println &#34;Telefony:&#34; telefony) (println &#34;E-maile: &#34; e-maile))
Przykład dekompozycji z symbolami o dookreślonych przestrzeniach nazw
 1(def dane-osobowe
 2  {'imię "Paweł"
 3   'kontakty/telefony [123456, 543210]
 4   'kontakty/e-maile  ["pw-at-gnu.org"]})
 5
 6(let [{:syms [imię],
 7       [telefon] 'kontakty/telefony,
 8       [e-mail]  'kontakty/e-maile} dane-osobowe]
 9  (println "Imię:   "    imię)
10  (println "Telefon:" telefon)
11  (println "E-mail: "  e-mail))
(def dane-osobowe {&#39;imię &#34;Paweł&#34; &#39;kontakty/telefony [123456, 543210] &#39;kontakty/e-maile [&#34;pw-at-gnu.org&#34;]}) (let [{:syms [imię], [telefon] &#39;kontakty/telefony, [e-mail] &#39;kontakty/e-maile} dane-osobowe] (println &#34;Imię: &#34; imię) (println &#34;Telefon:&#34; telefon) (println &#34;E-mail: &#34; e-mail))

W powyższym przykładzie symbole o dookreślonych przestrzeniach nazw zostały zacytowane w mapie powiązaniowej, ponieważ w przeciwnym wypadku byłyby potraktowane jak formy symbolowe.

Diagnozowanie dekompozycji

Destrukturyzacja skomplikowanych kolekcji danych może być obarczona ryzykiem pomyłki. W takich przypadkach warto korzystać ze sposobów, które umożliwiają podgląd procesu dekompozycji.

Dekompozycja do tekstu, destructure

Dzięki funkcji destructure możemy obserwować, jaki efekt będzie miała dekompozycja podanych struktur.

Użycie:

  • (destructure powiązania).

Funkcja przyjmuje jeden obowiązkowy argument, którym powinien być wektor powiązań w forme stałej.

Wartością zwracaną jest wektor powiązań, w którym zawarte są reprezentacje S-wyrażeń używane w procesie destrukturyzacji (formy stałe).

Przykład użycia funkcji destructure
 1(def dane-osobowe
 2  {'imię "Paweł"
 3   'kontakty/telefony [123456, 543210]
 4   'kontakty/e-maile  ["pw-at-gnu.org"]})
 5
 6(destructure '[{:syms
 7                [imię],
 8                [telefon] 'kontakty/telefony,
 9                [e-mail]  'kontakty/e-maile} dane-osobowe])
10
11; => [map__10728
12; =>  dane-osobowe
13; =>  map__10728
14; =>  (if
15; =>   (clojure.core/seq? map__10728)
16; =>   (clojure.lang.PersistentHashMap/create
17; =>     (clojure.core/seq map__10728))
18; =>   map__10728)
19; =>  vec__10729
20; =>  (clojure.core/get map__10728 (quote kontakty/telefony))
21; =>  telefon
22; =>  (clojure.core/nth vec__10729 0 nil)
23; =>  vec__10730
24; =>  (clojure.core/get map__10728 (quote kontakty/e-maile))
25; =>  e-mail
26; =>  (clojure.core/nth vec__10730 0 nil)
27; =>  imię
28; =>  (clojure.core/get map__10728 (quote imię))]
(def dane-osobowe {&#39;imię &#34;Paweł&#34; &#39;kontakty/telefony [123456, 543210] &#39;kontakty/e-maile [&#34;pw-at-gnu.org&#34;]}) (destructure &#39;[{:syms [imię], [telefon] &#39;kontakty/telefony, [e-mail] &#39;kontakty/e-maile} dane-osobowe]) ; =&gt; [map__10728 ; =&gt; dane-osobowe ; =&gt; map__10728 ; =&gt; (if ; =&gt; (clojure.core/seq? map__10728) ; =&gt; (clojure.lang.PersistentHashMap/create ; =&gt; (clojure.core/seq map__10728)) ; =&gt; map__10728) ; =&gt; vec__10729 ; =&gt; (clojure.core/get map__10728 (quote kontakty/telefony)) ; =&gt; telefon ; =&gt; (clojure.core/nth vec__10729 0 nil) ; =&gt; vec__10730 ; =&gt; (clojure.core/get map__10728 (quote kontakty/e-maile)) ; =&gt; e-mail ; =&gt; (clojure.core/nth vec__10730 0 nil) ; =&gt; imię ; =&gt; (clojure.core/get map__10728 (quote imię))]

Rezultat możemy uczytelnić i przedstawić jako kod:

 1(let [mapa-danych       dane-osobowe
 2      mapa-danych       (if (seq? mapa-danych)
 3                          (apply hash-map (seq mapa-danych))
 4                          mapa-danych)
 5      wektor-telefonów  (get mapa-danych 'kontakty/telefony)
 6      wektor-e-maili    (get mapa-danych 'kontakty/e-maile)
 7      imię              (get mapa-danych 'imię)
 8      telefon           (nth wektor-telefonów 0 nil)
 9      e-mail            (nth wektor-e-maili   0 nil)]
10  {:imię    imię
11   :telefon telefon
12   :e-mail  e-mail})
13
14; => {:e-mail "pw-at-gnu.org" :imię "Paweł" :telefon 123456}
(let [mapa-danych dane-osobowe mapa-danych (if (seq? mapa-danych) (apply hash-map (seq mapa-danych)) mapa-danych) wektor-telefonów (get mapa-danych &#39;kontakty/telefony) wektor-e-maili (get mapa-danych &#39;kontakty/e-maile) imię (get mapa-danych &#39;imię) telefon (nth wektor-telefonów 0 nil) e-mail (nth wektor-e-maili 0 nil)] {:imię imię :telefon telefon :e-mail e-mail}) ; =&gt; {:e-mail &#34;pw-at-gnu.org&#34; :imię &#34;Paweł&#34; :telefon 123456}
Jesteś w sekcji Poczytaj mi Clojure
Tematyka:

Taksonomie: