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ść.
(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 wczesne i wią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:
- „Obsługa powiązań” w tym rozdziale.
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ń:
- leksykalny – zależny od umiejscowienia identyfikatora w kodzie źródłowym,
- dynamiczny – zależny od kontekstu wywołań zmiennych dynamicznych,
- przestrzeni nazw/globalny – wyrażany globalnymi Varami i ograniczony regułami kwalifikowania nazw oraz widoczności.
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/sumuj
i liczby-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 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
Classreprezentują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ń 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ówVarz 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.
create-ns
(create-ns 'nowa)
; => #<Namespace nowa>
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 resolve i ns-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.
in-ns
(in-ns 'nowa)
; => #<Namespace nowa>
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ą.
namespace
1(namespace 'przestrzeń/jakaś-nazwa)
2; => "przestrzeń"
3
4(namespace :przykład/klucz)
5; => "przykład"
(namespace 'przestrzeń/jakaś-nazwa)
; => "przestrzeń"
(namespace :przykład/klucz)
; => "przykład"
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).
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 'user 'replace) ; => #'clojure.core/replace
(ns-resolve *ns* 'replace) ; => #'clojure.core/replace
(ns-resolve 'clojure.string 'replace) ; => #'clojure.string/replace
(ns-resolve 'clojure.string 'cośtam) ; => nil
(ns-resolve *ns* #{'replace 'coś} 'replace) ; => 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).
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 'replace)
; => #'clojure.core/replace
;; podajemy środowisko zawierające symbol
(resolve #{'replace} 'replace)
; => nil
;; podajemy symbol z dookreśloną przestrzenią nazw
(resolve 'clojure.string/replace)
; => #'clojure.string/replace
Zarządzanie przestrzeniami
Dzięki funkcjom all-ns i find-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).
all-ns
(all-ns)
; => (#<Namespace reply.main> #<Namespace clojure.tools.nrepl.misc> … )
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).
find-ns
(find-ns 'user)
; => #<Namespace user>
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).
remove-ns
(create-ns 'tymczasowa.przestrzeń)
(some? (remove-ns 'tymczasowa.przestrzeń))
; => 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ą.
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 'clojure.string)
; => nil
;; stwórz odniesienie tylko do identyfikatora 'replace'
(refer 'clojure.string :only '[replace])
; => nil
;; stwórz odniesienia do wszystkich Varów z clojure.string
;; za wyjątkiem 'replace' i 'reverse'
(refer 'clojure.string :exclude '[replace reverse])
; => nil
;; stwórz odniesienia do wszystkich Varów z clojure.string
;; za wyjątkiem 'replace', a 'reverse' zamień na 'nazad'
(refer 'clojure.string
:exclude '[replace]
:rename '{reverse nazad})
; => 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.
Z 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ą.
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* 'apply)
(ns-unmap *ns* 'format)
;; stwórz odniesienia do wszystkich Varów z clojure.core
;; za wyjątkiem apply i format
(refer-clojure :exclude '[apply format])
; => nil
;; brak ostrzeżenia przy definiowaniu apply
;; w bieżącej przestrzeni nazw
(defn apply [] "aplikacja")
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).
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 '(java.util Date Dictionary))
;; użycie obiektu
(Date.)
; => #inst "2015-04-02T11:35:43.980-00:00"
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).
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 'user 'zmienna) ; => #'user/zmienna
(intern 'user 'zmienna 5) ; => #'user/zmienna
user/zmienna ; => 5
zmienna ; => 5
(intern (find-ns 'user) 'a 10) ; => #'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.
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 'user '^:private zmienna) ; => #'user/zmienna
(intern 'user '^{:private true} zmienna-2) ; => #'user/zmienna
(intern 'user (with-meta 'zmienna-3 {:private true})) ; => #'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ść.
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 'user 'xx 5) ; utworzenie i powiązanie z wartością 5
(pprint #'xx) ; wyświetlenie obiektu referencyjnego
; >> #<Var@7a0ad359: 5>
(intern 'user 'xx 7) ; powiązanie z wartością 7
(pprint #'xx) ; wyświetlenie obiektu referencyjnego
; >> #<Var@7a0ad359: 7>
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?).
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) ; => #'user/zmienna
(def zmienna 5) ; => #'user/zmienna
(def zmienna "dokumentacja" 5) ; => #'user/zmienna
user/zmienna ; => 5
zmienna ; => 5
;; dostęp do dokumentacji – funkcja doc
(doc zmienna)
; >> user/zmienna
; >> dokumentacja zmiennej
; => 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.
def do tworzenia globalnej zmiennej prywatnej
1(def ^:private zmienna) ; => #'user/zmienna
2(def ^{ :private true } zmienna 5) ; => #'user/zmienna
(def ^:private zmienna) ; => #'user/zmienna
(def ^{ :private true } zmienna 5) ; => #'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).
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 #'xx) ; wyświetlenie obiektu
; >> #<Var@4eee52c: 5>
(def xx 7) ; powiązanie z wartością 7
(pprint #'xx) ; wyświetlenie obiektu
; >> #<Var@4eee52c: 7>
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).
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) ; => #'user/zmienna
(defonce ^:flaszka zmienna 1000) ; => nil
user/zmienna ; => 5
zmienna ; => 5
(meta #'zmienna)
; => { :ns #<Namespace user>, :name zmienna,
; => :flaszka true, :file "NO_SOURCE_PATH",
; => :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).
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)
; => #'user/x
;; sięgamy po jej wartość
x
; => 5
;; dodatkowo tworzymy odwołanie do obiektu Var tej zmiennej
(def y (var x))
;; usuwamy odwzorowanie
(ns-unmap 'user 'x)
; => nil
;; sprawdzamy, czy identyfikator x jest wciąż widoczny
(resolve 'x)
; => nil
;; sprawdzamy, czy sam obiekt Var istnieje,
;; choć nie jest już powiązany z symbolem x
(deref y)
; => 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 i :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).
alias
(alias 'st 'clojure.string)
(st/reverse "abcdef")
; => "fedcba"
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).
ns-unalias
(ns-unalias 'user 'st)
; => 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).
ns-name
(ns-name 'user) ; => user
(ns-name (find-ns 'user)) ; => 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).
ns-aliases
(ns-name (get (ns-aliases 'clojure.core) 'jio))
; => 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).
ns-interns
(identical? (get (ns-interns 'clojure.core) 'map)
#'clojure.core/map)
; => 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).
ns-refers
(identical? (get (ns-refers 'user) 'map)
#'clojure.core/map)
; => 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).
ns-imports
(identical? (get (ns-imports 'clojure.core) 'String)
java.lang.String)
; => 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).
ns-map
(every? #(or (var? %) (class? %))
(vals (ns-map 'clojure.core)))
; => 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).
ns-publics
(identical? (get (ns-publics 'clojure.core) 'map)
#'clojure.core/map)
; => 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 refer
i import. 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.
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 "projekt/core")
; => nil
;; ładowanie pliku głównego
;; biblioteki clojure.string
(load "/clojure/string")
; => 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 funkcjialiasi wytwarza odniesienie do ładowanej biblioteki pod podaną nazwą w bieżącej przestrzeni nazw; -
:refer wektor-symboli-lub-:all– korzysta z funkcjirefer, 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 zuselubrequire); -
: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.bnaa/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…).
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 'clojure.string)
; => nil
;; możemy podać kilka bibliotek
(require 'clojure.string 'clojure.test 'clojure.set)
;; możemy podać kilka bibliotek o wspólnym przedrostku
(require '[clojure string test set])
;; wczyta bibliotekę i utworzy przestrzeń nazw clojure.string
;; nawet, jeżeli już była wczytana
(require 'clojure.string :reload :verbose)
; => (clojure.core/load "/clojure/string")
; => 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 '[clojure.string :as st] :verbose)
; => (clojure.core/in-ns 'user)
; => (clojure.core/alias 'st 'clojure.string)
; => 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łujerequire,(:use …)– wywołujeuse,(:import …)– wywołujeimport,(:load …)– wywołujeload,(:gen-class …)– wywołujegen-class,(:refer-clojure …)– wywołujerefer-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.
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 doc
i dir), 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
funkcja i inna) 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 (Date i Random)
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:
- nadawaniu wartościom nazw z użyciem form powiązaniowych symboli,
- tworzeniu odniesień do wartości z użyciem tzw. typów referencyjnych,
- odczytywaniu wartości powiązań z użyciem:
- form symbolowych (w przypadku identyfikatorów);
- form dereferenyjnych (w przypadku typów referencyjnych).
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:
-
w przestrzeniach nazw (ang. namespaces):
-
w powiązaniach leksykalnych (ang. lexical bindings):
- z lokalnymi wartościami (formy
let,loopi podobne); - z lokalnymi obiektami Var (forma
with-local-vars); - z argumentami funkcji w ich definicjach
(tzw. powiązania parametryczne – formyfn,defn);
- z lokalnymi wartościami (formy
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żą:
- zmienne globalne, lokalne
i dynamiczne (typ
Var), - Atomy (typ
Atom), - Agenty (typ
Agent), - Refy (typ
Ref), - Volatile’e (typ
Volatile).
Future’y, Promise’y i Delay’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ów i klas 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.
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
; => 5 ; rezultat wywołania
(ns inna) ; przełączenie przestrzeni nazw
(funk) ; próba wywołania funkcji
; >> Unable to resolve symbol: funk in this context
(nasza/funk) ; wywołanie funkcji symbolem z dookreśloną przestrzenią
; => 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
Z 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 symboli w kodzie ź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
letlub 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:
-
pojedynczych symboli,
np.aczybdla wektora[a 5 b 10]; -
wektorowych wyrażeń powiązaniowych,
np.[a b]dla wektora[[a b] [5 10]]; -
map powiązaniowych,
np.{a :a b :b}dla wektora[{a :a b :b} {:a 5 :b 10}].
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.
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
; => 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:
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)
; => 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?)
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) ; => 1
(if-let [a 0] a) ; => 0
(if-let [a false] a) ; => nil
(if-let [a nil] a) ; => nil
(if-let [a nil] a "brak") ; => "brak"
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ć:
(let [x (fn [a] (+ 2 a))]
(x 2))
; => 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
; => 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
; >> java.lang.RuntimeException:
; >> 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.
letfn
(letfn [(y [a] (+ 3 (x a)))
(x [a] (+ 2 a))]
(y 2))
; => 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:
- „Funkcje”, rozdział III.
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…).
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 "mam " a)) ; => "mam 0"
(when-let [a 1] (str "mam " a)) ; => "mam 1"
(when-let [a nil] (str "mam " a)) ; => nil
(when-let [a false] (str "mam " a)) ; => 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…).
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 "mam " a)) ; => "mam 0"
(when-first [a [false 2 3]] (str "mam " a)) ; => "mam false"
(when-first [a [nil 2 3]] (str "mam " a)) ; => "mam "
(when-first [a "123"] (str "mam " a)) ; => "mam 1"
(when-first [a nil] (str "mam " a)) ; => nil
(when-first [a []] (str "mam " a)) ; => nil
(when-first [a ""] (str "mam " a)) ; => 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…).
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 "mam " a)) ; => "mam 0"
(when-some [a 1] (str "mam " a)) ; => "mam 1"
(when-some [a false] (str "mam " a)) ; => "mam false"
(when-some [a nil] (str "mam " a)) ; => 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).
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 "mam " a)) ; => "mam 0"
(if-some [a 1] (str "mam " a)) ; => "mam 1"
(if-some [a false] (str "mam " a)) ; => "mam false"
(if-some [a nil] (str "mam " a)) ; => nil
(if-some [a nil] (str "mam " a) "brak") ; => "brak"
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…).
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 (< 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:
- „Punkt początkowy”, rozdział XII.
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.
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)
with-local-vars
(with-local-vars [a 1] @a)
; => 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:
- „Zmienne lokalne”, rozdział VII.
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.
with-open
(with-open [czytnik (clojure.java.io/reader "plik.txt")]
(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:
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 " *x* po nazwie:"
*x* "\n" ; odczyt powiązanej wartości
"*x* po obiekcie:"
@obiekt-x "\n")) ; dereferencja obiektu
(defn testuj [] ; funkcja testująca
(binding [*x* 10] ; zasięg dynamiczny *x*
(println "* zasięg dynamiczny")
(chwal-się)) ; wywołanie funkcji w zasięgu
(println "* powiązanie główne:") ; dynamicznym
(chwal-się)) ; wywołanie funkcji poza
; dynamicznym zasięgiem
(testuj)
Zobacz także:
- „Zmienne dynamiczne”, rozdział VII.
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.
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))
; => (1 2 3)
;; dekompozycja
(let [[a b c] dane] (list a b c))
; => (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.).
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].
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 -> 1, b -> 2, c -> 3
(list a b c))
; => (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ń
; => (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.
(defn funkcja [a b c]
(list a b c))
(funkcja 1 2 3)
; => (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].
_ w dekompozycji pozycyjnej
(let [[_ b _] [1 2 3]] ; powiązania z dekompozycji
b) ; wartościowanie formy symbolowej
; => 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].
& w dekompozycji pozycyjnej
1(let [[_ & reszta] [1 2 3]] ; powiązania z dekompozycji
2 reszta) ; wartościowanie formy symbolowej
3; => (2 3)
(let [[_ & reszta] [1 2 3]] ; powiązania z dekompozycji
reszta) ; wartościowanie formy symbolowej
; => (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].
: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
; => [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].
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))
; => (1 2 3)
Specyfikatorami kluczy mogą być również inne wartości, nie tylko słowa kluczowe.
(let [{a 'a b 'b c 'c} '{a 1, b 2, c 3}]
(list a b c))
; => (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.
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 [ & {a :a, b :b, c :c} ]
(list a b c))
;; argumenty (wyrażenie inicjujące)
(funkcja :a 1, :b 2, :c 3)
; => (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].
: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))
; => (1 2 3)
(let [{:keys [a b c]} {:a 1, :b 2, :c 3}]
(list a b c))
; => (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].
: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]} {"a" 1, "b" 2, "c" 3}]
(list a b c))
; => (1 2 3)
(let [{:syms [a b c]} '{a 1, b 2, c 3}]
(list a b c))
; => (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].
: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
; => [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].
:or
(let [{:keys [:a :b :c]
:or {a 1, c 3}} {:b 2}]
(list a b c))
; => (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].
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} ["pierwszy" "drugi" "trzeci"]]
(list a b c))
; => ("pierwszy" "drugi" "trzeci")
Wekotory jako klucze w dekompozycji
Ciekawym przykładem dekompozycji asocjacyjnej może być mapa powiązaniowe, w której kluczami są wektory.
(let [{[a b c] :litery} {:litery [1 2 3]}]
(list a b c))
; => (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.
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ę "Paweł"
:nazwisko "Wilk"
:płeć :m
:kontakty {:telefony [123456, 543210]
:e-maile ["pw-at-gnu.org"]}})
(let [{:keys [imię nazwisko płeć], {[telefon] :telefony
[e-mail] :e-maile} :kontakty}
dane-osobowe
nazwa-płci (if (= płeć :m) "mężczyzna" "kobieta")]
(println "Imię i nazwisko: " imię nazwisko)
(println "Płeć: " nazwa-płci)
(println "Telefon: " telefon)
(println "E-mail: " 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:
{: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 specjalnaif(w zależności od wartości powiązanej z symbolempłećemituje odpowiedni łańcuch tekstowy):
1nazwa-płci (if (= płeć :m) "mężczyzna" "kobieta")
nazwa-płci (if (= płeć :m) "mężczyzna" "kobieta")
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
:keyspowiązująca z odpowiednimi symbolami wartości kluczy:imię,:nazwiskoi:płeć(z mapy identyfikowanej symbolemdane-osobowe):
1:keys [imię nazwisko płeć]
:keys [imię nazwisko płeć]
- Mapa powiązaniowa, która dokonuje dekompozycji struktury identyfikowanej
kluczem
:kontaktyz mapydane-osobowe:
{[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.
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ę "Paweł"
:kontakty/telefony [123456, 543210]
:kontakty/e-maile ["pw-at-gnu.org"]})
(let [{:keys [imię],
[telefon] :kontakty/telefony,
[e-mail] :kontakty/e-maile} dane-osobowe]
(println "Imię: " imię)
(println "Telefon:" telefon)
(println "E-mail: " e-mail))
(let [{:keys [imię kontakty/telefony kontakty/e-maile]} dane-osobowe]
(println "Imię: " imię)
(println "Telefony:" telefony)
(println "E-maile: " e-maile))
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ę "Paweł"
::telefony [123456, 543210]
::e-maile ["pw-at-gnu.org"]})
(let [{:keys [imię ::telefony user/e-maile]} dane-osobowe]
(println "Imię: " imię)
(println "Telefony:" telefony)
(println "E-maile: " e-maile))
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
{'imię "Paweł"
'kontakty/telefony [123456, 543210]
'kontakty/e-maile ["pw-at-gnu.org"]})
(let [{:syms [imię],
[telefon] 'kontakty/telefony,
[e-mail] 'kontakty/e-maile} dane-osobowe]
(println "Imię: " imię)
(println "Telefon:" telefon)
(println "E-mail: " 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).
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
{'imię "Paweł"
'kontakty/telefony [123456, 543210]
'kontakty/e-maile ["pw-at-gnu.org"]})
(destructure '[{:syms
[imię],
[telefon] 'kontakty/telefony,
[e-mail] 'kontakty/e-maile} dane-osobowe])
; => [map__10728
; => dane-osobowe
; => map__10728
; => (if
; => (clojure.core/seq? map__10728)
; => (clojure.lang.PersistentHashMap/create
; => (clojure.core/seq map__10728))
; => map__10728)
; => vec__10729
; => (clojure.core/get map__10728 (quote kontakty/telefony))
; => telefon
; => (clojure.core/nth vec__10729 0 nil)
; => vec__10730
; => (clojure.core/get map__10728 (quote kontakty/e-maile))
; => e-mail
; => (clojure.core/nth vec__10730 0 nil)
; => imię
; => (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 'kontakty/telefony)
wektor-e-maili (get mapa-danych 'kontakty/e-maile)
imię (get mapa-danych 'imię)
telefon (nth wektor-telefonów 0 nil)
e-mail (nth wektor-e-maili 0 nil)]
{:imię imię
:telefon telefon
:e-mail e-mail})
; => {:e-mail "pw-at-gnu.org" :imię "Paweł" :telefon 123456}