Poczytaj mi Clojure – cz. 6: Zmienne globalne i typ Var

Zmienna globalna to podstawowy typ referencyjny języka Clojure. Dzięki niej można nadawać stałą tożsamość zmieniającym się w czasie wartościom. W praktyce zmienne globalne, a także stanowiące ich wariant zmienne dynamiczne, pozwalają identyfikować funkcje, inne obiekty referencyjne i wartości wyrażające konfigurację programu.

Zmienne globalne i typ Var

W Clojure istnieje wiele sposobów tworzenia powiązań obiektów z wartościami. Różnią się one celem zastosowania, sposobami obsługi współbieżności i możliwościami sterowania zasięgiem. Poza [leksykalnymi powiązaniami][powiązania leksykalne], które służą do nadawania symbolicznych nazw i nie pozwalają na zmiany raz ustalonych odniesień do wartości, możemy skorzystać z globalnych powiązań, które bazują na użyciu referencyjnych obiektów typu Var.

Zmienna globalna (ang. global variable) to obiekt typu Var umieszczony w jednej z globalnych map zwanych przestrzeniami nazw. Kluczami tych map są symbole. Dzięki temu można nazywać globalne zmienne i na podstawie symbolicznych identyfikatorów odwoływać się do reprezentujących je obiektów.

Zmienne globalne służą do tworzenia tożsamości o ustalonych nazwach, których powiązania z konkretnymi wartościami mogą zmieniać się w czasie.

W obiektach typu Var nie przechowuje się wartości. W przeciwieństwie do zmiennych znanych z imperatywnych języków programowania zmienne globalne z Clojure nie zawierają wartościowych informacji, ale odnoszą się do nich (wskazują na nie). Takie podejście jest konsekwencją założenia, że wartości są niezmienne. Odniesienie do umieszczonej w pamięci wartości nazywamy referencją.

Obsługa zmiennych globalnych

Obiekty typu Var są najczęściej używanym typem referencyjnym w programach pisanych w Clojure, ponieważ służą między innymi do:

  • wskazywania funkcji i makr,
  • wskazywania danych konfiguracyjnych,
  • wskazywania innych obiektów referencyjnych.

Owo wskazywanie polega na:

  • nazywaniu obiektu typu Var z użyciem symbolu,
  • utrzymywaniu w obiekcie odwołania do wartości bieżącej.

Czym są podane wyżej wartości bieżące? Są to dowolne wartości, które zostaną w różnych kwantach czasu powiązane ze zmienną globalną przez aktualizowanie obecnego w obiekcie typu Var odniesienia. Pierwszą nadaną wartość nazywa się powiązaniem głównym (ang. root binding). Jest ona współdzielona między wszystkimi wątkami wykonywania i używana w przypadku, gdy nie istnieją powiązania specyficzne dla wątków. Powiązanie główne można zmieniać.

Od chwili powiązania zmiennej z wartością początkową jesteśmy wstanie dokonywać na niej różnych operacji, które będą prowadziły do odczytu aktualnie wskazywanej wartości lub jej zaktualizowania (podmiany na inną).

Charakter zmiennych globalnych można zobrazować, porównując je do konwencjonalnych zmiennych, znanych np. z języka C. Gdy dokonujemy tam przypisania wartości do zmiennej, dochodzi do umieszczenia danych w zajmowanym przez nią obszarze pamięci. Aktualizacja polega na modyfikacji obecnych tam danych, ewentualnie na ręcznym przydzieleniu nowego miejsca, jeśli nowa struktura byłaby za duża. W przypadku Clojure zmienna globalna nie przechowuje żadnej wartości, a aktualizacja polega na zmianie odwołania do wartości, która już w pamięci istnieje. Dzięki temu wartości mogą pozostawać niezmienne (niemutowalne), a mimo to da się reprezentować różne stany.

Sama referencja znajdująca się w obiekcie typu Var jest elementem mutowalnym, czyli takim, którego zawartość zajmuje stałe miejsce i poddaje się modyfikacjom. Taki wyjątek od reguły jest niezbędny, aby utrzymywać stałą tożsamość obiektu referencyjnego. Mechanizmy języka dbają o to, aby mutacje referencji były bezpieczne pod względem przetwarzania współbieżnego.

Definicja praktyczna

W praktyce zmienną globalną zastosujemy po to, aby nadać symboliczną nazwę szeregowi wartości, które mogą zmieniać się w czasie, ale niezbyt często lub wcale.

Rolą zmiennych globalnych jest przede wszystkim identyfikowanie pojedynczych wartości, struktur i podprogramów (poziom implementacji), a nie przechowywanie odwołań do często zmieniających się zbiorów danych (poziom logiki biznesowej aplikacji). Oczywiście nie wyklucza to stosowania globalnych zmiennych do symbolicznego nazywania innych obiektów referencyjnych.

Zmienne globalne przeznaczone są do utrzymywania odrębnych tożsamości w obrębie lokalnego wątku, z możliwością powiązania z wartością domyślną, która będzie współdzielona między wątkami. Odwołanie do wartości domyślnej to wspomniane wcześniej powiązanie główne, tworzone zazwyczaj podczas definiowania zmiennej.

Zasięg zmiennych globalnych

Zmienne globalne mają zasięg nieograniczony (ang. indefinite scope), to znaczy, że od momentu zdefiniowania takiej zmiennej, będzie ona dostępna w całym programie. Dodatkowe sterowanie widocznością zmiennych globalnych (a w zasadzie ich symbolicznych identyfikatorów) możliwe jest z użyciem przestrzeni nazw.

Opcjonalnie zmienne globalne mogą być tzw. zmiennymi dynamicznymi i mieć zasięg dynamiczny (ang. dynamic scope), jeśli podczas definiowania ich ustawimy odpowiednią opcję metadanych symbolu i (tym samym) kojarzonego z nim obiektu Var, a potem skorzystamy ze specjalnego makra binding.

W wyjątkowych okolicznościach utworzone w specjalny sposób obiekty typu Var, mogą również mieć określony zasięg leksykalny (ang. lexical scope) dodatkowo izolowany w bieżącym wątku. Przypominają wtedy zmienne znane z imperatywnych języków programowania.

Zastosowania typu Var

Zmienne globalne to najpopularniejszy, ale nie jedyny sposób korzystania z obiektów typu Var. W zależności od potrzeb możemy używać egzemplarzy tej klasy w różnych konstrukcjach, które będą różniły się przede wszystkim rodzajem zasięgupoziomem izolacji w wątku.

Poniższa tabela zawiera porównanie rodzajów struktur, które korzystają z obiektów typu Var:

Nazwa struktury Przeznaczenie Współdzielenie Zasięg
Zmienna globalna Identyfikuje i nazywa elementy konfiguracji, funkcje używane w programie i inne obiekty referencyjne. Powiązanie główne jest współdzielone między wszystkimi wątkami. Zasięg nieograniczony
Zmienna dynamiczna Identyfikuje i nazywa elementy konfiguracji, funkcje używane w programie i inne obiekty referencyjne – z możliwością dynamicznego przesłaniania wartości bieżącej. Powiązanie główne jest współdzielone między wszystkimi wątkami, ale można tworzyć powiązania izolowane w bieżącym wątku, które przesłonią powiązanie główne. Zasięg dynamiczny
Zmienna lokalna Identyfikuje i nazywa często zmieniające się wartości w sekcjach programu wymagających zastosowania podejścia imperatywnego. Powiązanie główne jest izolowane w bieżącym wątku. Zasięg leksykalny

Użytkowanie zmiennych globalnych

Tworzenie zmiennych

Tworzenie zmiennej globalnej polega na umieszczeniu w przestrzeni nazw obiektu typu Var. Będzie on w niej identyfikowany symbolem i zyska w ten sposób nazwę. Nie spotkamy globalnych zmiennych, które są nienazwane (ang. unnamed), chociaż możemy stwarzać niepowiązane (ang. unbound) obiekty typu Var bez wartości początkowej, tzn. pozbawione powiązania głównego.

Internalizowanie obiektów Var, intern

Proces wpisywania obiektu typu Var do przestrzeni nazw, na którym polega tworzenie zmiennych globalnych, nazywamy internalizowaniem (ang. interning). Funkcja intern tworzy obiekt typu Var i umieszcza go w podanej jako pierwszy argument przestrzeni nazw, gdzie zostaje skojarzony z symboliczną nazwą podaną jako drugi argument.

Metadane zawarte w symbolu zostaną skopiowane do nowo powstałego obiektu zmiennej globalnej, a niektóre z nich będą miały wpływ na jej zachowanie się. Opis metadanych, które są używane przez zmienne globalne znajduje się w rozdziale opisującym przestrzenie nazw, w sekcji poświęconej formule specjalnej def, a poniżej znajduje się zestawienie najważniejszych z nich:

Klucz Typ Znaczenie
:private Boolean Flaga logiczna, która wskazuje, że zmienna ma być prywatna.
:dynamic Boolean Flaga logiczna, która wskazuje, że zmienna ma być dynamiczna.
:doc łańcuch 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).

Funkcja zwraca obiekt typu Var, który jest tożsamy z obiektem rezydującym w przestrzeni nazw. Jeżeli przestrzeń nazw nie istnieje, to jest zgłaszany wyjątek. Podobnie, gdy w przestrzeni nazw już istnieje odwzorowanie podanego symbolu, ale nie dotyczy ono obiektu typu Var.

Uwaga: Funkcja intern zastępuje obiektami typu Var istniejące już pod podaną nazwą odniesienia do klas Javy, nawet jeśli nie ustawiono wartości początkowej.

Użycie:

  • (intern przestrzeń symboliczna-nazwa wartość-początkowa?)
Przykłady użycia funkcji intern
1
2
(intern 'user 'zmienna)    ; => #'user/zmienna
(intern 'user 'zmienna 5)  ; => #'user/zmienna

Nieczęsto wystąpi potrzeba bezpośredniego internalizowania obiektów typu Var z użyciem funkcji intern. Znacznie wygodniejszym sposobem tworzenia zmiennych globalnych jest definiowanie zmiennych z wykorzystaniem formuły specjalnej def.

Zobacz także:

Deklarowanie zmiennych, declare

Makro declare przyjmuje zero lub więcej argumentów podanych jako niezacytowane symbole i dla każdego z nich dokonuje w bieżącej przestrzeni nazw internalizacji zmiennej globalnej bez ustawiania jej powiązania głównego. Jeżeli zmienna już istnieje, nie będzie powzięta żadna czynność.

Makro zwraca obiekt typu Var ostatniej internalizowanej zmiennej lub wartość nil, jeśli nie podano argumentów. Gdy podano symbol z dookreśloną przestrzenią nazw i jest ona różna od przestrzeni bieżącej, to zgłoszony będzie wyjątek. Podobnie, gdy w przestrzeni nazw już istnieje odwzorowanie podanego symbolu, ale nie dotyczy ono obiektu typu Var.

Użycie:

  • (declare & symbol…).
Przykład użycia funkcji declare
1
2
(declare)        ; => nil
(declare a b c)  ; => #'user/c 

Definiowanie zmiennych, def

Aby utworzyć zmienną globalną i opcjonalnie wytworzyć powiązanie główne z jakąś wartością, należy skorzystać z formuły specjalnej def. Przyjmuje ona jeden obowiązkowy argument, którym powinien być niezacytowany symbol określający nazwę zmiennej globalnej. Symbol ten zostanie umieszczony w przestrzeni nazw i będzie przyporządkowany jako identyfikator obiektu typu Var.

Dodatkowe argumenty przyjmowane przez def to:

  • opcjonalny łańcuch dokumentujący, który powinien być łańcuchem tekstowym wyrażonym literalnie, czyli napisem ujętym w znaki cudzysłowu maszynowego;
  • opcjonalna wartość powiązania głównego.

Ponadto symbol można “wyposażyć” w metadane, które wpływają na właściwości zmiennej lub mają znaczenie w kontekście logiki konkretnego programu.

Formuła specjalna def zwraca obiekt typu Var, który jest tożsamy z obiektem umieszczonym w przestrzeni nazw.

Użycie:

  • (def symbol łańcuch-dokumentujący? wartość-początkowa?).
Przykład użycia formuły specjalnej def
1
2
(def nazwa "nikow")         ; powiązanie z łańcuchem tekstowym
(def funka (fn [] "nikow")) ; powiązanie z obiektem funkcji

Obliczenie wyrażeń z powyższego przykładu sprawi, że:

  • w bieżącej przestrzeni nazw pojawi się odwzorowanie kojarzące symbol nazwa z internalizowanym tam obiektem Var;

  • obiekt typu Var zostanie powiązany z obiektem łańcucha tekstowego "nikow" (staje się to jego powiązaniem głównym);

  • w bieżącej przestrzeni nazw pojawi się odwzorowanie kojarzące symbol funka z kolejnym utworzonym obiektem Var;

  • kolejny obiekt typu Var zostanie powiązany z anonimową funkcją, która jako wartość zwraca łańcuch tekstowy "nikow".

Nic nie stoi na przeszkodzie, aby utworzyć globalną zmienną bez powiązania głównego. Będzie można potem znów użyć def, aby je utworzyć.

Przykład zmiennej globalnej bez powiązania głównego
1
2
3
4
5
;; zmienna niepowiązana
(def druga-nazwa)

;; powiązanie z wartością 10
(def druga-nazwa 10)

Jeśli spróbujemy utworzyć globalną zmienną bez powiązania głównego, a identyfikowany podanym symbolem obiekt Var już istnieje, nie zostanie wykonana żadna znacząca czynność.

Przykład próby zdefiniowania istniejącej zmiennej globalnej
1
2
3
4
5
(def druga-nazwa 20)
(def druga-nazwa)

druga-nazwa
; => 20

Zobacz także:

Jednokrotne definiowanie zmiennych, defonce

Makro defonce służy do ustawiania powiązania głównego zmiennej globalnej pod warunkiem, że już tego nie uczyniono. Działa podobnie do def, lecz nie pozwala na ustawianie łańcuchów dokumentacyjnych wyrażanych jako argument. Przyjmuje dwa argumenty: niezacytowany symbol (z dookreśloną przestrzenią nazw lub nie) i wyrażenie, którego wartość po przeliczeniu stanie się powiązaniem głównym.

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

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

Użycie:

  • (defonce symbol wyrażenie)
Przykłady użycia funkcji defonce
1
2
3
(defonce zmienna 10)  ; => #'user/zmienna
(defonce zmienna 5)   ; => #'user/zmienna
zmienna               ; => 10

Zobacz także:

Odczytywanie obiektów Var

Do samego obiektu typu Var, stanowiącego rdzeń zmiennej globalnej, możemy uzyskać bezpośredni dostęp, bez automatycznych czynności ze strony ewaluatora. Dzięki kilku funkcjom i jednej formule specjalnej jesteśmy w stanie z przestrzeni nazw uzyskać obiekt, podając nazwę tej przestrzeni (lub korzystając z przestrzeni bieżącej) i symbolicznie wyrażoną nazwę zmiennej.

Pobieranie z przestrzeni nazw, var

Formuła specjalna var pozwala na odczyt obiektu typu Var umieszczonego pod nazwą określoną formułą symbolową (wyrażoną niezacytowanym symbolem), którą przekazano jako pierwszy argument. Przeszukana zostanie bieżąca przestrzeń nazw, chyba że podano symbol z dookreśloną przestrzenią.

Clojure wyposażono też w makro czytnika #', które jest skróconą formą wywołania var.

Jeśli symbol nie istnieje w bieżącej bądź podanej w symbolu przestrzeni nazw, lub jeśli nie wskazuje na obiekt typu Var, to wygenerowany zostanie wyjątek.

Przykład użycia formuły specjalnej var
1
2
3
4
5
6
7
8
9
10
11
12
13
(def nazwa 5)      ; tworzymy zmienną globalną

(var nazwa)        ; odczytujemy obiekt
; => #'user/nazwa

(var user/nazwa)   ; dookreślona przestrzeń nazw
; => #'user/nazwa

#'nazwa            ; lukier składniowy
; => #'user/nazwa

#'user/nazwa       ; lukier składniowy
; => #'user/nazwa

Widzimy tu przy okazji, że obiekty typu Var są reprezentowane w REPL z użyciem literałów, które odpowiadają wspomnianemu makru.

Wyszukiwanie w przestrzeniach nazw, find-var

Funkcja find-var umożliwia przeszukiwanie przestrzeni nazw w celu znalezienia obiektu typu Var identyfikowanego symbolem. Przyjmuje jeden argument, którym powinien być symbol o dookreślonej przestrzeni nazw w formie stałej.

Funkcja zwraca obiekt typu Var lub wartość nil, jeśli obiektu nie znaleziono. W przypadku pominięcia przestrzeni nazw w symbolu lub w przypadku, gdy określona w symbolu przestrzeń nie istnieje, generowany jest wyjątek.

Użycie:

  • (find-var symboliczna-nazwa).
Przykład użycia funkcji find-var
1
2
(find-var 'user/nie-istnieje)       ; => nil
(find-var 'clojure.string/replace)  ; => #'clojure.string/replace

Wyszukiwanie generyczne, ns-resolve

Funkcja ns-resolve również pozwala uzyskać obiekt typu Var, lecz jej działanie jest bardziej ogólne: wyszukuje podanej w przestrzeni nazw przypisane do symboli obiekty (zmienne globalne lub klasy Javy).

Drugą (lub trzecią w przypadku wariantu trójargumentowego) przekazywaną wartością powinien być symbol w formie stałej, który określa nazwę.

Funkcja zwraca identyfikowany symboliczną nazwą obiekt lub wartość nil, jeżeli nie znaleziono odwzorowania. Podana przestrzeń nazw musi istnieć, a jeśli nie istnieje, to wygenerowany będzie wyjątek.

Uwaga: Nie zaleca się używania ns-resolve bez odpowiednich testów, które sprawdzą czy zwrócony obiekt naprawdę jest typu Var. Lepiej skorzystać z formuły specjalnej var.

Użycie:

  • (ns-resolve przestrzeń            symboliczna-nazwa),
  • (ns-resolve przestrzeń środowisko symboliczna-nazwa).
Przykład odczytu zmiennej globalnej z użyciem ns-resolve
1
2
3
4
5
6
7
(def nazwa "nikow")

(if-let [ob (ns-resolve ; Pobierz obiekt umieszczony w:
             'user      ;  · przestrzeni nazw user,
             'nazwa)]   ;  · którego kluczem jest symbol nazwa.
  (if (var? ob) ob))    ; Zwróć nil, jeśli nie jest Varem.
; => #'user/nazwa

W powyższym przykładzie czynimy użytek z makra if-let, które ustawia powiązanie leksykalne ob, a następnie zwraca obiekt referencyjny pod warunkiem, że jest on typu Var (funkcja var?).

Dokładny opis funkcji ns-resolve znajduje się w rozdziale poświęconym przestrzeniom nazw.

Wyszukiwanie generyczne, resolve

Aby odnajdować zmienne globalne w przestrzeniach nazw, możemy też skorzystać z funkcji resolve, która działa tak samo jak ns-resolve, lecz korzysta z przestrzeni bieżącej.

Uwaga: Nie zaleca się używania resolve bez odpowiednich testów, które sprawdzą czy zwrócony obiekt naprawdę jest typu Var. Lepiej skorzystać z formuły specjalnej var.

Użycie:

  • (resolve            symboliczna-nazwa),
  • (resolve środowisko symboliczna-nazwa).
Przykład odczytu zmiennej globalnej z użyciem resolve
1
2
3
4
5
6
(def nazwa "nikow")

(if-let [ob (resolve    ; Pobierz obiekt umieszczony w bieżącej przestrzeni nazw,
             'nazwa)]   ;  którego kluczem jest symbol nazwa.
  (if (var? ob) ob))    ; Zwróć nil, jeśli nie jest Varem.
; => #'user/nazwa

Dokładny opis funkcji resolve znajduje się w rozdziale poświęconym przestrzeniom nazw.

Odczytywanie wartości bieżących

Odczytywanie wartości zmiennych globalnych polega na dereferencji (ang. dereference) wskazania do wartości bieżącej umieszczonego w obiekcie typu Var. Mamy tak naprawdę do czynienia z dwoma operacjami:

  1. Odszukanie w przestrzeni nazw obiektu Var identyfikowanego symbolem.
  2. Odczyt wartości bieżącej wskazywanej przez referencję.

Istnieją funkcje, które pozwalają nam samodzielnie dokonywać powyższych czynności (np. te przedstawione wcześniej służą do odczytywania obiektów typu Var), jednak w przypadku tak powszechnie użytkowanej struktury ciągłe ich wywoływanie czyniłoby kod mało czytelnym. Programista Clojure może więc korzystać z odpowiedniego wsparcia ze strony ewaluatora, który w specjalny sposób traktuje formuły symbolowe (wyrażane niezacytowanymi symbolami).

Na przykład z użyciem symbolu nazwa będziemy uzyskiwali dostęp do wartości wskazywanej przez powiązaną z nią zmienną globalną, jeśli została ona utworzona.

Przykład automatycznej dereferencji symbolicznie identyfikowanego obiektu
1
2
3
4
(def nazwa "nikow")

nazwa
; => "nikow"

Widzimy, że ewaluator wyręczył nas w dwójnasób. Po pierwsze poszukał odwzorowania symbolu nazwa w bieżącej przestrzeni nazw, a po drugie odwołał się do przyporządkowanego mu obiektu typu Var, aby pobrać wartość, na którą wskazuje umieszczona w tym ostatnim referencja.

Przypomnijmy, że gdybyśmy chcieli uzyskać dostęp do samego symbolu w formie stałej, musielibyśmy użyć cytowania.

Przykład cytowania pojedynczego symbolu
1
2
3
4
5
'nazwa
; => nazwa

(type 'nazwa)
; => clojure.lang.Symbol

Samodzielna dereferencja, var-get

Z użyciem ns-resolve, resolve czy var uzyskaliśmy dostęp do obiektu, lecz nie do wskazywanej przez niego wartości bieżącej. Aby ją odczytać, należy skorzystać z funkcji var-get. Przyjmuje ona jeden argument, który powinien być obiektem typu Var, a zwraca powiązaną wartość.

Użycie:

  • (var-get obiekt-var).
Przykłady użycia funkcji var-get
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
(def nazwa "nikow")

(var-get       ; Pobierz wartość obiektu wskazywanego przez Vara
 (ns-resolve   ;   identyfikowanego odwzorowaniem:
  'user        ;    · z przestrzeni nazw user,
  'nazwa))     ;    · którego kluczem jest symbol nazwa.

; => "nikow"

(var-get       ; Pobierz wartość obiektu typu Var
 (resolve      ;  identyfikowanego w bieżącej przestrzeni nazw odwzorowaniem,
  'nazwa))     ;    którego kluczem jest symbol nazwa.

; => "nikow"

(var-get       ; Pobierz wartość obiektu typu Var
 (var nazwa))  ;  identyfikowanego w bieżącej przestrzeni nazw symbolem nazwa.

; => "nikow"

Aby dokonać dereferencji można też skorzystać z bardziej generycznej funkcji deref, która działa nie tylko w odniesieniu do obiektów typu Var, ale również innych typów referencyjnych.

Funkcja w wariancie jednoargumentowym przyjmuje obiekt typu Var (lub inny obiekt referencyjny) i zwraca jego bieżącą wartość. Jeśli wywołanie odbywa się w obrębie transakcji STM, to zwracana jest wartość aktualna w tej transakcji. Jeżeli wywołanie odbywa się poza transakcją, to zwracana jest wartość ostatnio zatwierdzona.

Użycie:

  • (deref obiekt-var).
Przykłady użycia funkcji deref
1
2
3
4
5
6
7
8
9
10
11
12
(def nazwa "nikow")

(deref         ; Pobierz stan obiektu wskazywany referencją,
 (resolve      ;   identyfikowaną odwzorowaniem w bieżącej przestrzeni nazw,
  'nazwa))     ;     którego kluczem jest symbol nazwa.

; => "nikow"

(deref         ; Pobierz stan obiektu wskazywany referencją,
 (var nazwa))  ;   identyfikowaną w bieżącej przestrzeni nazw symbolem nazwa.

; => "nikow"

Dzięki istnieniu odpowiedniego makra czytnika powyższy przykład można zapisać krócej.

Przykład dereferencji z użyciem makra czytnika
1
2
3
4
5
6
7
(def nazwa "nikow")

@(resolve 'nazwa)  ; @ zamiast deref
; => "nikow"

@#'nazwa           ; @ zamiast deref i #' zamiast var
; => "nikow"

Aktualizowanie powiązań z wartościami

Ponowne definiowanie

Aktualizować powiązanie główne zmiennej globalnej możemy z użyciem formuły specjalnej def. Wariantem tej operacji może być też posłużenie się makrem defonce, które wytworzy powiązanie, ale pod warunkiem, że ono jeszcze nie istnieje. Kolejnym możliwym sposobem jest skorzystanie z intern.

Przykład użycia def i defonce do aktualizacji wartości bieżącej
1
2
3
4
5
6
7
8
9
10
11
(def a 5)           ; powiązanie główne
(def a 4)           ; nowe powiązanie główne
a                   ; => 4

(future (def a 6))  ; nowe powiązanie główne w innym wątku
a                   ; => 6

(defonce a 8)       ; nowe powiązanie pod warunkiem, że jeszcze nie ustawiono
(defonce b 8)       ; --''--
a                   ; => 6 
b                   ; => 8

Uwaga: Formuła specjalna def i funkcja intern mogą być użyte do zmiany powiązania głównego obiektu typu Var, które jest współdzielone między wątkami, nawet jeśli mamy do czynienia z konstrukcją izolującą zmienną w wątku bieżącym (np. w zasięgu leksykalnym).

Wszystkie wspomniane sposoby ustawiają powiązanie główne obiektu typu Var z podaną wartością, ale czynią w sposób niedbały, jeśli chodzi o współbieżność. Można ich używać tam, gdzie jesteśmy pewni, że program (jeszcze) nie korzysta z wątków i gdzie nowa wartość nie zależy od bieżącej.

W przypadku korzystania ze zmiennych globalnych do pamiętania odniesień do funkcji (w celu ich identyfikowania) czy do elementów konfiguracji – a więc zgodnie z przeznaczeniem – wspomniane kłopoty nie powinny się pojawiać.

Spójrzmy na przykładowy kod, który pokazuje negatywne skutki korzystania ze zmiennych globalnych niezgodnie z przeznaczeniem.

Przykład aktualizowania zmiennej globalnej w wielu wątkach
1
2
3
4
5
6
(def wektor (vector)) ; Tworzymy Vara powiązanego z pustym wektorem.
(dotimes [x 9]        ; Powtarzamy 9 razy:
  (future             ;  odłożoną w czasie (w innym wątku) operację
   (def wektor        ;   zmiany powiązania głównego
        (conj wektor  ;    na nowo stworzony wektor, będący kopią obecnego
              x))))   ;    z dodanym elementem: numerem aktualnego powtórzenia.

Mamy tu 9 różnych wątków, z których każdy do wektora identyfikowanego symbolem wektor dodaje liczbę, którą można nazwać kolejnym numerem wątku. Najpierw pobierana jest zawartość wektora wskazywanego przez zmienną identyfikowaną symbolem wektor, a następnie tworzony jest nowy wektor różniący się od poprzedniego dodanym elementem (w postaci liczby będącej wspomnianym numerem). Zmienna globalna wektor jest następnie w każdym z wątków ponownie wiązana z uzyskaną strukturą. Dzieje się tak dlatego, że powiązanie główne jest współdzielone między wątkami.

Po uruchomieniu spodziewamy się wartości bieżącej w postaci wektora zawierającego wszystkie liczby ze zbioru od 0 do 9, ale uzyskujemy coś innego:

1
2
wektor
; => [0 5 6 7 8]

Brakuje liczb 1, 2, 3 i 4. Dlaczego?

Ustawianie głównego powiązania na bazie poprzedniej wartości (linie 4–6) nie jest operacją atomową, tzn. między odczytem a aktualizacją mija pewien czas, w trakcie którego inne wątki mogą również zmieniać współdzieloną referencję.

Mamy tam następującą sekwencję operacji:

  1. Odczyt wartości bieżącej wskazywanej przez obiekt typu Var.
  2. Stworzenie nowego wektora na bazie uzyskanego.
  3. Aktualizacja referencji w zmiennej (wskazanie na nową wartość).

Niestety, niektóre wątki dokonały odczytu wartości bieżącej, przeliczenia jej i powiązania zmiennej wektor z nowo utworzoną wartością pochodną w tym samym czasie. Doszło więc do nadpisania dopiero co zaktualizowanego w innym wątku powiązania. Tego typu usterkę nazywamy sytuacją wyścigu (ang. race condition).

Zobacz także:

Zmiana powiązania głównego, alter-var-root

Zalecanym sposobem zmiany powiązania głównego jest użycie funkcji alter-var-root. Działa ona w ten sposób, że dla podanego obiektu typu Var w sposób atomowysynchroniczny dokonuje aktualizacji referencji.

Funkcja jako pierwszy argument przyjmuje obiekt typu Var, a jako drugi funkcję zwracającą nową wartość na bazie bieżącej. Przekazywana funkcja powinna przyjmować przynajmniej jeden argument – z jego użyciem będzie przekazana aktualna wartość powiązana ze zmienną. Jeśli przekazywana funkcja wymaga przekazania dodatkowych argumentów, to można je podać jako kolejne, opcjonalne argumenty wywołania alter-var-root.

Użycie:

  • (alter-var-root obiekt-var funkcja & argumenty).
Przykład użycia funkcji alter-var-root
1
2
3
4
5
(def x 5)
(alter-var-root #'x (constantly 10))

x
; => 10

Wiemy, że użycie def czy intern ustawia główne powiązanie zmiennej globalnej we wszystkich wątkach, ale wymagane jest podanie już przeliczonej wartości, co może powodować problemy w programach współbieżnych i wielowątkowych, gdy nowa wartość zależy od bieżącej.

Możemy więc spróbować raz jeszcze poprzedniego przykładu, ale zmienionego tak, aby aktualizacja powiązania głównego zmiennej globalnej korzystała z funkcji alter-var-root.

Przykład użycia funkcji alter-var-root w kodzie wielowątkowym
1
2
3
4
5
6
7
8
(def wektor (vector))      ; Tworzymy Vara powiązanego z pustym wektorem.
(dotimes [x 9]             ; Powtarzamy 9 razy:
 (future                   ;  odłożoną w czasie (w innym wątku) operację
  (alter-var-root          ;   aktualizacji powiązania głównego
   #'wektor                ;    zmiennej globalnej wektor,
   (fn [stary]             ;    aplikując do poprzedniej wartości funkcję,
    (conj stary            ;     która zwróci nowy wektor, będący kopią obecnego
          x)))))           ;     z dodanym elementem: numerem aktualnego powtórzenia.

Uzyskany wektor nie ma już braków:

1
2
wektor
; => [0 2 1 5 7 6 4 3 8]

Wynika to z użycia atomowej operacji aktualizowania wartości zmiennej globalnej, która polega na:

  1. Odczycie wartości bieżącej.
  2. Obliczeniu nowej wartości na bazie poprzedniej z użyciem przekazanej funkcji.
  3. Powiązaniu obiektu typu Var z nową wartością (zaktualizowaniu referencji).

Wszystkie te trzy kroki zostaną wykonane synchronicznie, tzn. z zapewnieniem, że w tym czasie nie będzie przez żaden z wątków wykonywana taka operacja.

Gdy przyjrzymy się dostępowi poszczególnych wątków do zmiennej globalnej, to zauważymy, że powstanie swoista kolejka modyfikacji, przy czym każda zmiana będzie jednym, szczelnym działaniem przypominającym transakcję bazy danych.

Zapamiętajmy:

Zmienne globalne służą przede wszystkim do identyfikowania innych obiektów, a nie do wyrażania i zarządzania częstymi zmianami stanów tożsamości.

Redefinicje

Czasami zachodzi potrzeba chwilowego przesłonięcia powiązań zmiennych globalnych. W takich sytuacjach – np. w celach testowych czy w przypadku odpluskwiania (a może nawet w programie z modularnymi funkcjami, które można włączać i wyłączać) – przydają się konstrukcje with-redefswith-redefs-fn.

Działają one w obrębie wszystkich wątków, dokonując tymczasowego powiązania zmiennych globalnych z podanymi wartościami. Modyfikowane są ich powiązania główne. Po zakończeniu wykonywania wyrażenia powiązania główne zmiennych globalnych są przywracane.

Tworzenie redefinicji

Funkcja with-redefs-fn

Funkcja with-redefs-fn przyjmuje dwa argumenty. Pierwszym powinna być mapa powiązań, której kluczami są obiekty typu Var, a wartościami nowe powiązania główne tych obiektów, a drugim obiekt funkcji, która zostanie wykonana po zmianie powiązań. Wartością zwracaną jest rezultat wykonania przekazanej funkcji. Po wykonaniu funkcji powiązania główne są przywracane.

Użycie:

  • (with-redefs-fn mapa-powiązań funkcja).
Przykład użycia makra with-redefs-fn
1
2
3
4
(def a 2)
(def b 4)
(with-redefs-fn {#'a 1 #'b 1} (fn [] (+ a b)))
; => 2

Makro with-redefs

Makro with-redefs działa podobnie do with-redefs-fn, a nawet wewnętrznie z niego korzysta. Jako pierwszy argument przyjmuje wektor powiązań wyrażonych formułami symbolowymi zmiennych globalnych z nowymi wartościami ich powiązań głównych, a jako drugi argument wyrażenie, które zostanie przeliczone, gdy powiązania będą zmienione. Wartością zwracaną jest rezultat wykonania podanego wyrażenia. Po wykonaniu makra powiązania główne są przywracane.

Użycie:

  • (with-redefs wektor-powiązaniowy wyrażenie).
Przykład użycia makra with-redefs
1
2
3
4
(def a 2)
(def b 4)
(with-redefs [a 1 b 1] (+ a b))
; => 2

Walidatory

Zmienne globalne można opcjonalnie wyposażyć w mechanizmy testujące poprawność wartości, z którymi dokonywane są ich powiązania. Służą do tego funkcje set-validator!get-validator. Można ich używać również w odniesieniu do innych typów referencyjnych języka (np. obiektów typu Atom, Ref czy Agent).

Walidator (ang. validator) to w tym przypadku czysta (wolna od efektów ubocznych), jednoargumentowa funkcja, która przyjmuje wartość poddawaną sprawdzaniu. Jeśli jest ona niedopuszczalna, funkcja powinna zwrócić wartość false lub wygenerować wyjątek.

Operacje na walidatorach

Ustawianie walidatora, set-validator!

Funkcja set-validator! służy do ustawiania walidatora dla obiektu typu Var. Jako pierwszy argument należy podać obiekt typu Var, a obiekt opisanej funkcji sprawdzającej przekazać jako drugi argument wywołania. Opcjonalnie można zamiast obiektu funkcji podać wartość nil – w takim przypadku walidator zostanie usunięty.

Zanim walidator zostanie ustawiony, sprawdzane jest czy wartość, która ma być powiązana ze zmienną globalną jest akceptowana przez podaną funkcję. Jeśli tak nie jest, to zgłaszany jest wyjątek, a walidator nie jest ustawiany ani zmieniany.

Użycie:

  • (set-validator! obiekt-var funkcja).
Przykład użycia funkcji set-validator!
1
2
3
4
5
6
7
8
9
10
11
12
(def mniej-niż-pięć 0)

(set-validator! #'mniej-niż-pięć
                (fn [wartość] (< wartość 5)))
; => nil

(def mniej-niż-pięć 4) ; ok
; => #'user/mniej-niż-pięć

(def mniej-niż-pięć 6)
; >> IllegalStateException Invalid reference state
; >> clojure.lang.ARef.validate (ARef.java:33)

Pobieranie walidatora, get-validator

Mając obiekt typu Var, możemy pobrać funkcyjny obiekt jego walidatora, posługując się funkcją get-validator. Przyjmuje ona jeden argument, który powinien być obiektem typu Var, a zwraca obiekt funkcji lub nil, jeśli walidatora nie ustawiono.

Użycie:

  • (get-validator obiekt-var).
Przykład użycia funkcji get-validator
1
2
3
4
5
6
7
8
(def mniej-niż-pięć 0)
(def nasz-walidator (fn [wartość] (< wartość 5)))

(set-validator! #'mniej-niż-pięć nasz-walidator)
; => nil

(get-validator #'mniej-niż-pięć) ; pobranie walidatora
; => #<user$nasz_walidator [email protected]>

Zmienne dynamiczne

Zmienne globalne nazywane są czasem zmiennymi dynamicznymi, ponieważ mają ciekawą właściwość – opcjonalnie mogą korzystać z zasięgu dynamicznego (ang. dynamic scope). Oznacza to, że możliwe jest utworzenie takiej zmiennej globalnej, która przez pewien czas, niezależnie od miejsca w kodzie źródłowym, będzie powiązana z nową wartością.

Zmiana wartości bieżącej będzie widoczna tylko w bieżącym wątku. Zasięg dynamiczny nie jest związany z współbieżnością z definicji – po prostu w Clojure, który akcentuje współbieżność, właściwość ta została dodana do konstrukcji stwarzającej dynamiczny zasięg globalnych zmiennych. Dzięki niej poszczególne wątki programu mogą dokonywać bezpiecznej zmiany powiązania zmiennej globalnej, bez zakłócania pracy innych wątków.

Zmiennych globalnych powiązanych dynamicznie możemy używać nie tylko ze względu na współbieżne wykonywanie, ale też po to, aby dla danej sekcji programu obowiązywały różne od pierwotnych warunki. Przykładem może być sytuacja, gdy program korzysta ze zmiennej globalnej *kolor-tła*, która jest wykorzystywana w różnych funkcjach odpowiedzialnych za rysowanie interfejsu, ale w pewnym procesie (np. wyświetlania okna z ostrzeżeniem) chcielibyśmy zmienić wartość koloru na inną.

Aby skorzystać z dynamicznego powiązania muszą być spełnione następujące warunki:

  • Zmienna globalna musi już istnieć i być zadeklarowana jako dynamiczna z użyciem metadanej :dynamic.

  • Sekcja, w której powiązanie główne zmiennej globalnej będzie przesłonięte powiązaniem dynamicznym musi być określona S-wyrażeniem podanym jako jeden z argumentów makra binding.

Operacje na zmiennych dynamicznych

Tworzenie zmiennej dynamicznej

Deklarowanie zmiennej jako dynamicznej polega na skorzystaniu z metadanych podawanych w trakcie definiowania powiązania (klucz :dynamic musi być skojarzony z wartością true).

Przykład definiowania zmiennej dynamicznej
1
2
(def ^{:dynamic true} dynamiczna 5)
(def ^:dynamic        dynamiczna 5)  ; uproszczona wersja zapisu

Przesłanianie zmiennej dynamicznej, binding

Aby przesłonić wartość bieżącą zmiennej dynamicznej w pewnym kontekście, należy użyć makra binding. Jako pierwszy argument przyjmuje ono wektor powiązaniowy, w którym nieparzyste elementy par to niezacytowane symbole identyfikujące zmienne dynamiczne, a parzyste to ich nowe wartości. Będą one obowiązywały we wszystkich wyrażeniach podanych jako ostatnie argumenty makra.

Makro zwraca wartość ostatnio podanego wyrażenia lub wartość nil, jeśli nie podano wyrażeń.

Użycie:

  • (binding wektor-powiązaniowy & wyrażenie…).
Przykład użycia makra binding
1
2
3
4
5
6
7
8
9
10
11
12
(def ^:dynamic dynamo 5)  ; tworzenie zmiennej dynamicznej
                          ; o wartości początkowej 5

(defn funkcja [] dynamo)  ; tworzenie funkcji, która zwraca wartość
                          ; zmiennej dynamicznej

(binding [dynamo 7]       ; powiązanie dynamiczne
 (funkcja))               ;   wywołanie funkcji odczytującej
; => 7                    ;   dynamo ma wartość 7

(funkcja)                 ; wywołanie funkcji odczytującej
; => 5                    ; dynamo ma wartość 5

Możemy zaobserwować, że zmienna globalna dynamo w ciele wywołania binding została przesłonięta przez dynamicznie wytworzone powiązanie tej zmiennej z inną wartością.

Powiązania dynamiczne zmiennych globalnych realizowane są na poziomie obiektów typu Var, a nie ich symbolicznych nazw.

Przykład bezpośredniego użycia obiektu typu Var w binding
1
2
3
4
5
6
7
8
9
10
(def ^:dynamic dynamo 5)  ; tworzenie zmiennej dynamicznej
                          ; o wartości początkowej 5

(defn pobierz-dynamo [v]  ; dla podanego obiektu zmiennej dynamicznej
  (binding [dynamo 8]     ; przesłoń wartość bieżącą wartością 8
    (var-get v)))         ; i pobierz wskazywaną obiektem wartość
                          ; w zasięgu powiązania dynamicznego

(pobierz-dynamo           ; wywołaj funkcję
  (var dynamo))           ; przekazując obiekt Var

Dynamiczne zmienne mogą być używane do wskazywania nie tylko stałych formuł, ale też funkcji, które potem mogą być redefiniowane w zależności od kontekstu. Otwiera to drogę do programowania aspektowego.

Przesłanianie zmiennych dynamicznych, with-bindings

Aby usprawnić korzystanie z wielu zmiennych globalnych powiązanych dynamicznie w jednym wyrażeniu, w Clojure istnieje makro with-bindings.

Użycie:

  • (with-bindings mapa-powiązań & wyrażenie…).

Pierwszym argumentem makra powinna być mapa powiązaniowa, a kolejnym wyrażenia przeznaczone do przeliczenia. Wartością zwracaną jest wartość ostatniego wyrażenia lub nil, jeżeli nie podano wyrażeń.

Przykład użycia makra with-bindings
1
2
3
4
5
(def ^:dynamic a 2)
(def ^:dynamic b 4)
(defn sumator [] (+ a b))
(with-bindings { #'a 1 #'b 2 } (sumator))
; => 3

Przesłanianie zmiennych dynamicznych, with-bindings*

Wariantem makra with-bindings jest funkcja with-bindings*, która przyjmuje funkcję zamiast zestawu wyrażeń. Podana funkcja będzie wywołana, a odwoływanie się w niej (lub wywoływanych przez nią funkcjach, markach bądź formułach specjalnych) do zmiennych dynamicznych będzie korzystało z dynamicznie przesłoniętych wartości określonych podaną mapą powiązaniową.

Użycie:

  • (with-bindings* mapa-powiązań funkcja & argument…).

Pierwszym argumentem funkcji powinna być mapa powiązaniowa, a kolejnym funkcja, która zostanie wywołana. Jako dodatkowe, opcjonalne argumenty można podać argumenty, których wartości zostaną przekazane jako argumenty wywoływanej funkcji. Wartością zwracaną jest rezultat wywołania funkcji.

Przykład użycia funkcji with-bindings*
1
2
3
4
5
(def ^:dynamic a 2)
(def ^:dynamic b 4)
(defn sumator [] (+ a b))
(with-bindings* { #'a 1 #'b 2 } sumator)
; => 3

Uwaga: wartości przekazywanych do funkcji argumentów nie będą dynamicznie przesłonięte wartościami podanymi w mapie.

Aktualizowanie zmiennej dynamicznej, set!

Dzięki formule specjalnej set! możemy zmieniać wartość zmiennej dynamicznej w bieżącym wątku wykonywania. Przyjmuje ona dwa argumenty: niezacytowany symbol i wyrażenie, które emituje nową wartość.

Użycie:

  • (set! symbol wyrażenie).
Przykład użycia funkcji set!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
;; definicja zmiennej dynamicznej
(def ^:dynamic *dynamo* 108)
; => #'user/dynamo

;; czytnik zmiennej dynamicznej
(def daj-dynamo (fn [] *dynamo*))

;; zasięg dynamiczny i przesłonięcie
(binding [*dynamo* 21]
  (set! *dynamo* 4)       ; aktualizacja powiązania
  (daj-dynamo))           ; wywołanie czytnika zmiennej
; => 4

;; odczyt zmiennej dynamicznej
*dynamo*
; => 108

Zmienne dynamiczne można też aktualizować z wykorzystaniem funkcji var-set, która została omówiona przy okazji opisywania zmiennych lokalnych.

Przykład użycia funkcji var-set w odniesieniu do zmiennej dynamicznej
1
2
3
4
5
6
7
8
9
10
11
;; definicja zmiennej dynamicznej
(def ^:dynamic *dynamo* 108)
; => #'user/dynamo

;; użycie var-set na obiekcie typu Var
(binding [*dynamo* 21] (var-set #'*dynamo* 4) *dynamo*)
; => 4

;; odczyt powiązania głównego
*dynamo*
; => 108

Zmienne specjalne

Przyjęło się, że zmienne globalne, które mogą mieć dynamiczny zasięg – nazywane zmiennymi specjalnymi (ang. special variables) – powinny być odpowiednio wyróżniane w kodzie źródłowym. W przypadku dialektów języka Lisp korzysta się z zapisu zwanego żargonowo nausznikami (ang. earnmuffs), dołączając z obu stron symbolicznej nazwy znaki asterysku (pot. gwiazdki).

Przykłady definiowania zmiennych specjalnych
1
2
3
4
(def *nazwa-pliku*  "/var/log/plik")
(def *rozmiar-okna*            1024)
(def *tryb-odpluskwiania*      true)
(def *alternatywne-wyjście*        )

Dlaczego “nauszniki”? Krążą o tym pewne legendy. Jedna z nich wspomina, że w programowaniu funkcyjnym bardzo pożądaną cechą jest przewidywalność rezultatów. Wywołując czystą funkcję dla podanych argumentów o ustalonych wartościach, zawsze otrzymamy ten sam wynik. Dla programistów Lispa oznacza to ciepłe uczucie bezpiecznego obcowania z programem.

Jednak modelowanie problemów tzw. świata rzeczywistego to procesy, a nie tylko kalkulacje, więc nie da się uniknąć zmiennych stanów czy funkcji, które nie są czyste, lecz wpływają na otoczenie. Oznaczanie dynamicznych źródeł danych “nausznikami” informuje programistę, że powinien uważać, bo rezultaty ich odczytów nie są przewidywalne i warto zadbać o jakąś ochronę przed ich chłodem.

Powiązania leksykalne obiektów typu Var

Istnieją mechanizmy języka, które pozwalają korzystać z obiektów typu Var w taki sposób, że ich widoczność będzie ograniczona zasięgiem leksykalnym.

Zmienne lokalne

Zmienne lokalne (ang. local variables) to zmienne o zasięgu leksykalnym, które korzystają z obiektów typu Var nieumieszczonych w przestrzeniach nazw. Obsługa tych zmiennych jest ukłonem w stronę programistów, którzy rozwiązując skomplikowane problemy muszą w drodze wyjątku (np. ze względów wydajnościowych) skorzystać z nieco bardziej imperatywnego podejścia. Niektóre algorytmy bardzo trudno jest realizować wyłącznie funkcyjnie i w takich przypadkach przydają się obiekty Var powiązane leksykalnie z symbolicznymi identyfikatorami.

Ponieważ Clojure akcentuje współbieżność, więc poza określonym syntaktycznie zasięgiem będziemy mieli również do czynienia z izolowaniem obiektów typu Var w wątkach wykonywania.

Zmienne lokalne są zmiennymi leksykalnymi, których zasięg ograniczony jest umiejscowieniem w kodzie źródłowym, a dokładniej w wyrażeniach podanych jako ostatnie argumenty konstrukcji with-local-vars.

Tworzenie zmiennych lokalnych, with-local-vars

Za tworzenie leksykalnego zasięgu powiązań obiektów typu Var z symbolicznymi identyfikatorami odpowiada makro with-local-vars. Przyjmuje ono pary powiązań umieszczone w wektorze podanym jako pierwszy argument i wyrażenia do przeliczenia podane jako kolejne argumenty. Skojarzone ze zmiennymi wartości umieszczone w parach staną się ich bieżącymi stanami, a wewnątrz wyrażeń będzie można korzystać ze zdefiniowanych w ten sposób zmiennych.

Makro with-local-vars zwraca wartość ostatniego przeliczanego wyrażenia lub wartość nil, jeśli nie podano wyrażeń.

Użycie:

  • with-local-vars powiązania & wyrażenie….
Przykład użycia makra with-local-vars
1
2
(with-local-vars [a 1 b 2] b)
; => #<Var: --unnamed-->

W powyższym przykładzie możemy zaobserwować, że nie dochodzi do automatycznej dereferencji zmiennych i operujemy na ich obiektach, a nie na wartościach bieżących.

Odczytywanie zmiennych lokalnych, var-get

W wyrażeniach makra with-local-vars widoczne są obiekty typu Var, których odczyt możliwy jest z użyciem funkcji var-get, deref lub literału skonstruowanego z użyciem przedrostka @. Odczytywane są wartości bieżące zmiennych (izolowane w lokalnym wątku).

Przykład odczytu zmiennej w ciele makra with-local-vars
1
2
3
4
5
6
7
(with-local-vars [a 1 b 2]
  (+ (var-get a) (var-get b)))
; => 3

(with-local-vars [a 1 b 2]
  (+ @a @b))
; => 3

Aktualizowanie zmiennych lokalnych, var-set

W wyrażeniach makra with-local-vars widoczne są obiekty typu Var, których powiązania z wartościami możemy aktualizować z użyciem funkcji var-set. Przyjmuje ona dwa argumenty: pierwszy powinien być obiektem typu Var, którego powiązanie chcemy zmienić, a drugi nową wartością tego powiązania.

Zmieniane jest powiązanie główne izolowane w obrębie lokalnego wątku.

Użycie:

  • (var-set obiekt-var wyrażenie).
Przykład użycia funkcji var-set
1
2
3
4
5
6
7
(with-local-vars [a 1 b 2]
  (var-set a 2)             ; pierwszy wątek, aktualizacja powiązania głównego
  (future (println @a))     ; >> 2 (drugi wątek, wartość z powiązania głównego)
  (future (var-set a 3))    ; aktualizacja powiązania w drugim wątku
  (Thread/sleep 2000)       ; opóźnienie 2s
  (+ @a @b))                ; pierwszy wątek, sumowanie wartości zmiennych
; => 4

Testowanie właściwości zmiennych

Testowanie typu

Czy Var?, var?

Predykat var? pozwala sprawdzić, czy podany argument jest obiektem typu Var. Zwraca wartość true, jeżeli tak jest, a false w przeciwnym razie.

Użycie:

  • (var? wartość).
Przykład użycia funkcji var?
1
2
3
(def a 100)
(var? #'a)
; => true

Testowanie powiązania

Czy powiązane?, thread-bound?

Predykat bound? pozwala sprawdzić, czy podane jako argumenty obiekty typu Var są powiązane z jakimiś wartościami. Zwraca wartość true, jeżeli tak jest (lub nie podano argumentów), a false w przeciwnym razie.

W praktyce funkcji bound? można użyć, aby zbadać czy na zmiennych zadziała operacja deref.

Użycie:

  • (bound? & obiekt-var…).
Przykład użycia funkcji bound?
1
2
3
4
5
6
7
8
9
10
11
12
13
(def ggg)
(bound? #'ggg)
; => false

(def ggg 5)
(bound? #'ggg)
; => true

(with-redefs [a 5] (bound? #'a))
; => true

(with-local-vars [a 5] (bound? a))
; => true

Testowanie cech wielowątkowych

Czy izolowane w wątku?, thread-bound?

Predykat thread-bound? pozwala sprawdzić, czy podane jako argumenty obiekty typu Var są izolowane w poszczególnych wątkach wykonywania. Zwraca wartość true, jeżeli tak jest (lub nie podano argumentów), a false w przeciwnym razie.

W praktyce funkcji thread-bound? można użyć, aby zbadać czy na zmiennych zadziała operacja set! lub var-set.

Użycie:

  • (thread-bound? & obiekt-var…).
Przykład użycia funkcji thread-bound?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(def a 5)
(def ^:dynamic dynamo 5)

(thread-bound? #'a)
; => false

(thread-bound? #'dynamo)
; => false

(with-redefs [a 5] (thread-bound? #'a))
; => false

(with-local-vars [a 5] (thread-bound? a))
; => true

(binding [dynamo 10] (thread-bound? #'dynamo))
; => true

Z powyższego przykładu możemy dowiedzieć się, że powiązania zmiennych będą izolowane w wątkach wykonywania, jeśli są to obiekty typu Var o zasięgu leksykalnym i zmienne dynamiczne objęte dynamicznym zasięgiem.

Zobacz także

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

Komentarze