stats

Poczytaj mi Clojure, cz. 7

Zmienne globalne: typ Var

Grafika

Zmienna globalna to jedna z najpowszechniej wykorzystywanych konstrukcji języka Clojure. Dzięki niej można identyfikować funkcje, inne obiekty referencyjne i wartości wyrażające konfigurację programu.

Zmienne globalne

W Clojure mamy wiele sposobów tworzenia powiązań nazw z wartościami. Różnią się one celami zastosowania (głównie z powodu różnych sposobów obsługi współbieżności) i możliwymi rodzajami zasięgów. Poza powiązaniami leksykalnymi, które służą do nazywania wartości w pewnych obszarach kodu źródłowego, możemy też skorzystać z globalnych powiązań, działających w całym programie, a ograniczonych nie miejscem, lecz aktualnie ustawioną przestrzenią nazw.

Globalne zmienne nie są bezpośrednimi przyporządkowaniami nazw do wartości. Składają się z identyfikowanych nazwami obiektów referencyjnych (typu clojure.lang.Var), a dopiero te zawierają odwołania do konkretnych obiektów umieszczonych w pamięci.

Zmienna globalna (ang. global variable) to obiekt typu Var przypisany do symbolicznego identyfikatora w przestrzeni nazw. Jak zdążyliśmy dowiedzieć się we wcześniejszych częściach, przestrzenie nazw to wewnętrznie mapy, których kluczami są symbole, a wartościami m.in. obiekty Var. Dzięki temu można nazywać globalne zmienne i na podstawie symbolicznych identyfikatorów odwoływać się do reprezentujących je obiektów z użyciem czytelnych form symbolowych.

Gdy mechanizmy języka Clojure wykryją w kodzie źródłowym niezacytowany symbol, przeprowadzany jest proces rozpoznawania jego nazwy (ang. name resolution), a jednym z przeszukiwanych miejsc jest właśnie przestrzeń nazw. Gdy okaże się, że to właśnie tam do symbolu przypisano obiekt typu Var, wartość bieżąca pobierana jest automatycznie. Programista nie musi używać jakichś specjalnych konstrukcji (jak w przypadku innych typów referencyjnych), aby ją uzyskać; zostaje niejako podstawiona w miejscu symbolicznej nazwy.

Ogólnie rzecz ujmując, 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 w Clojure nie zawierają właściwych danych, lecz odniesienia do nich. Takie podejście jest konsekwencją założenia, że wartości są niezmienne. Odwołanie do umieszczonej w pamięci wartości nazywamy referencją, a typ danych, który pozwala tworzyć obiekty przechowujące takie odwołania, typem referencyjnym.

Zmienna globalna w Clojure

Można powiedzieć, że globalne zmienne wytwarzane są z użyciem dwóch rodzajów powiązań. Pierwsze jest odwzorowaniem symbolicznej nazwy na obiekt typu Var (w przestrzeni nazw), a drugie umieszczonym w tym obiekcie odniesieniem (referencją) do konkretnej wartości bieżącej.

Obsługa zmiennych globalnych

Globalnie nazwane obiekty typu Var są najczęściej używanym typem referencyjnym w programach pisanych w Clojure, ponieważ zazwyczaj przechowują odwołania do:

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

Utrzymywanie tożsamości z użyciem globalnych zmiennych polega na:

  • identyfikowaniu obiektów typu Var z użyciem symboli (w przestrzeni nazw),
  • utrzymywaniu w obiektach typu Var odniesień do tzw. wartości bieżących.

Czym są wymienione 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 niej odniesienia (ang. reference).

Przykłady użycia zmiennych globalnych
(def  długość   5)         ; zmienna odwołująca się do wartości stałej
(def  szerokość 2)         ; zmienna odwołująca się do wartości stałej
(defn pole [x y] (* x y))  ; zmienna odwołująca się do funkcji

(pole długość szerokość)   ; automatyczne pobieranie wartości bieżących
; => 10
(def długość 5) ; zmienna odwołująca się do wartości stałej (def szerokość 2) ; zmienna odwołująca się do wartości stałej (defn pole [x y] (* x y)) ; zmienna odwołująca się do funkcji (pole długość szerokość) ; automatyczne pobieranie wartości bieżących ; => 10

Pierwszą wartość nadaną zmiennej globalnej nazywamy jej powiązaniem głównym (ang. root binding). Jest ona współdzielona między wszystkimi wątkami wykonywania i zwracana wtedy, gdy nie istnieją powiązania specyficzne dla oddzielnie realizowanych wątków. Powiązanie główne można zmieniać.

Od chwili powiązania zmiennej z wartością początkową jesteśmy w stanie przeprowadzać na niej różne operacje, które będą polegały na odczycie aktualnie wskazywanej wartości lub jej aktualizacji – podmiany referencji tak, aby zmienna odnosiła się do innej wartości.

Charakter zmiennych globalnych z Clojure można zobrazować kontrastem, 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 przydzielonym jej obszarze pamięci. Aktualizacja polega na modyfikacji obecnych tam danych, ewentualnie na ręcznym przydzieleniu nowego miejsca, jeżeli nowa struktura byłaby za duża. W przypadku Clojure zmienna globalna nie przechowuje żadnej wartości, a aktualizacja polega na podmianie odwołania do wartości, która już gdzieś w pamięci istnieje. Dzięki temu wartości mogą pozostawać niezmienne (niemutowalne), a mimo to da się reprezentować różne, zmieniające się w czasie stany.

Sama referencja znajdująca się w obiekcie typu Var jest elementem mutowalnym, czyli takim, którego zawartość zajmuje stałe miejsce poddawane 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 i w razie potrzeby odpowiednio izolowane.

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 będzie więc 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 aplikacji). Oczywiście nie wyklucza to stosowania globalnych zmiennych do identyfikowania innych obiektów referencyjnych, które pozwalają wyrażać zmienne stany.

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, czyli wspomniane wcześniej powiązanie główne, jest 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 realizowane jest z użyciem wspomnianych wcześniej przestrzeni nazw.

Opcjonalnie zmienne globalne mogą być tzw. zmiennymi dynamicznymi i mieć zasięg dynamiczny (ang. dynamic scope), jeżeli podczas definiowania ich ustawimy odpowiednią opcję metadanych symbolu i w konsekwencji kojarzonego z nim zaraz potem obiektu Var, a później w programie skorzystamy z makra binding, aby dynamicznie przesłaniać wskazywaną wartość inną.

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. Nie są wtedy identyfikowane w przestrzeniach nazw, lecz na lokalnym stosie utrzymywanym dla odpowiedniej formy powiązaniowej. Przypominają wtedy zmienne znane z imperatywnych języków programowania.

Zastosowania typu Var

Zmienne globalne to najpopularniejszy, choć nie jedyny, sposób korzystania z obiektów typu clojure.lang.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 między wątkami wykonywania.

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

Rodzaj zmiennej Przeznaczenie Współdzielenie Zasięg
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. nieograniczony
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. dynamiczny
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. 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 wytwarzać niepowiązane (ang. unbound) obiekty typu Var bez wartości początkowej, tzn. pozbawione powiązania głównego.

W większości zastosowań tworzenie zmiennej globalnej będzie polegało na użyciu formy specjalnej def, jednak poniżej omówione zostaną również inne konstrukcje, dzięki którym programista ma większą kontrolę nad procesem tworzenia tego rodzaju powiązań.

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. Jeżeli pod podaną nazwą już zarejestrowano obiekt typu Var, nie będzie stworzony nowy, a ewentualnie będzie zaktualizowane jego powiązanie główne (jeżeli podano dodatkową wartość przy wywoływaniu intern).

Metadane zawarte w symbolu zostaną skopiowane do nowo powstałego obiektu typu Var, a niektóre z nich będą miały wpływ na zachowanie się zmiennej. Opis metadanych, które są używane przez zmienne globalne znajduje się w rozdziale opisującym przestrzenie nazw, w sekcji poświęconej formie specjalnej def, a poniżej przestawiono 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 znakowy dokumentujący tożsamość zmiennej.
:tag określenie-typu Class lub Symbol Symbol stanowiący nazwę klasy lub obiekt typu Class, który określa typ obiektu Javy znajdującego się w zmiennej (chyba, że jest to funkcja – wtedy będzie typ zwracanej przez nią wartości).
: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 podana przestrzeń nazw nie istnieje, zgłaszany jest wyjątek.

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

Rzadko pojawi się potrzeba bezpośredniego internalizowania obiektów typu Var z użyciem funkcji intern. Znacznie wygodniejszym sposobem tworzenia zmiennych globalnych jest definiowanie ich z wykorzystaniem formy specjalnej def.

Zobacz także:

Deklarowanie zmiennych, declare

Makro declare przyjmuje zero lub więcej argumentów podanych jako symbole w formach powiązaniowych 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ść.

declare skorzystamy wtedy, gdy chcemy odwoływać się do zmiennej globalnej w obszarze programu, który wczytywany jest wcześniej, niż dochodzi do inicjowania wartości bieżącej tej zmiennej, ale wartościowany będzie później, więc nie ma ryzyka odwołania się do zmiennej niepowiązanej z żadną wartością. Praktycznym przykładem może być tu wystąpienie formy wywołania funkcji zanim w kodzie źródłowym pojawi się jej definicja.

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

Użycie:

  • (declare & symbol…).

Przykład użycia funkcji declare
1
2
(declare)        ; => nil
(declare a b c)  ; => #'user/c 
(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ą, możemy skorzystać z formy specjalnej def. Przyjmuje ona jeden obowiązkowy argument, którym powinien być symbol w formie powiązaniowej określający nazwę zmiennej globalnej. Symbol ten zostanie umieszczony w przestrzeni nazw i będzie identyfikował utworzony obiekt typu clojure.lang.Var.

Dodatkowe argumenty przyjmowane przez def to:

  • opcjonalny łańcuch dokumentujący, który powinien być łańcuchem znakowym 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 przyjętej logiki działania konkretnego programu.

Forma 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 formy specjalnej def
1
2
(def nazwa "nikow")          ; powiązanie z łańcuchem znakowym
(def funka (fn [] "nikow"))  ; powiązanie z obiektem funkcji
(def nazwa "nikow") ; powiązanie z łańcuchem znakowym (def funka (fn [] "nikow")) ; powiązanie z obiektem funkcji

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

  1. W bieżącej przestrzeni nazw pojawi się odwzorowanie symbolu nazwa na internalizowany obiekt typu Var.

  2. Obiekt typu Var zostanie powiązany z łańcuchem znakowym "nikow" (stanie się on wartością powiązania głównego tego obiektu referencyjnego).

  3. W bieżącej przestrzeni nazw pojawi się odwzorowanie kojarzące symbol funka z kolejnym utworzonym obiektem typu Var.

  4. Kolejny obiekt typu Var zostanie powiązany z anonimową funkcją, która jako wartość zwraca łańcuch znakowy "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 ustawić. W takim przypadku nie będzie instancjonowany kolejny obiekt typu Var, a zaktualizowane zostanie powiązanie główne istniejącego:

Przykład ustawienia powiązania głównego istniejącej zmiennej globalnej
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
;; zmienna niepowiązana
(def druga)

;; próba pobrania wartości
druga
; => #object[clojure.lang.Var$Unbound 0x3eba57a7 "Unbound: #'user/druga"]

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

;; próba pobrania wartości
druga
; => 10
;; zmienna niepowiązana (def druga) ;; próba pobrania wartości druga ; => #object[clojure.lang.Var$Unbound 0x3eba57a7 "Unbound: #'user/druga"] ;; powiązanie z wartością 10 (def druga 10) ;; próba pobrania wartości druga ; => 10

Gdy spróbujemy utworzyć globalną zmienną bez powiązania głównego, a identyfikowany podanym symbolem obiekt typu 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 20)
(def druga)

druga
; => 20
(def druga 20) (def druga) druga ; => 20

Zobacz także:

Jednokrotne definiowanie, defonce

Makro defonce służy do ustawiania powiązania głównego zmiennej globalnej pod warunkiem, że jeszcze tego nie uczyniono. Działa podobnie do def, ale nie pozwala na ustawianie łańcuchów dokumentujących wyrażanych wartością argumentu.

Makro przyjmuje dwa argumenty: symbol w formie powiązaniowej (z dookreśloną przestrzenią nazw lub nie) i następujące po nim wyrażenie, którego wartość po przeliczeniu stanie się powiązaniem głównym zmiennej globalnej identyfikowanej w przestrzeni nazw podaną symboliczną nazwą.

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, ustawione będą metadane pochodzące z przekazanego symbolu i zastąpią istniejące.

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
(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 automatycznej dereferencji ze strony ewaluatora. Dzięki kilku funkcjom i jednej forme specjalnej jesteśmy w stanie z przestrzeni nazw pobrać obiekt referencyjny, podając nazwę tej przestrzeni (lub korzystając z przestrzeni bieżącej) i symbolicznie wyrażoną nazwę zmiennej.

Pobieranie z przestrzeni nazw, var

Forma specjalna var pozwala na odczyt obiektu typu Var identyfikowanego w przestrzeni nazw symbolem podanym jako jej pierwszy argument. Przeszukana zostanie bieżąca przestrzeń nazw, chyba że podano symbol z dookreśloną przestrzenią.

Clojure wyposażono też w makro czytnika #' (symbol kratki i apostrofu), które jest skróconą postacią wywołania var.

Jeżeli w bieżącej lub określonej przestrzeni nazw nie istnieje przyporządkowanie identyfikowane podanym symbolem, lub gdy nie odnosi się do obiektu typu Var, zgłoszony zostanie wyjątek.

Przykład użycia formy 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
(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 przy okazji, że obiekty typu Var są reprezentowane w REPL z użyciem literałów, które odpowiadają składni wspomnianego makra czytnika.

Wyszukiwanie po nazwach, find-var

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

Funkcja zwraca obiekt typu Var lub wartość nieustaloną nil, gdy obiektu nie znaleziono. W przypadku niepodania przestrzeni nazw lub wtedy, gdy określona nazwą 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
(find-var 'user/nie-istnieje) ; => nil (find-var 'clojure.string/replace) ; => #'clojure.string/replace

Rozpoznawanie po nazwach, ns-resolve

Funkcja ns-resolve podobnie jak find-var pozwala uzyskać obiekt typu Var, lecz jej działanie jest bardziej ogólne: wyszukuje w podanej przestrzeni nazw przypisane do symboli obiekty – nie tylko referencje typu Var, ale też klasy Javy.

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

Funkcja zwraca identyfikowany symboliczną nazwą obiekt lub wartość nieustaloną nil, jeżeli nie znaleziono odwzorowania. Podana przestrzeń nazw musi istnieć, a jeżeli nie istnieje, zgłoszony zostanie wyjątek.

Uwaga: Nie zaleca się używania ns-resolve bez odpowiednich testów, które sprawdzą czy zwrócony obiekt naprawdę jest typu clojure.lang.Var. Do operowania na zmiennych globalnych zaleca się korzystanie z mniej podatnej na występowanie błędów formy 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
8
(def nazwa "nikow")

(if-let [ob (ns-resolve ; Pobierz obiekt z:
             'user      ;  · przestrzeni nazw user,
             'nazwa)]   ;  · którego kluczem jest symbol nazwa.
  (if (var? ob) ob))    ; Zwróć nil, jeżeli nie jest Varem.

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

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

Zobacz także:

Rozpoznawanie w bieżącej, resolve

Aby odnajdywać 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. Do uzyskiwania obiektów typu Var zmiennych globalnych lepiej skorzystać z formy 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
7
(def nazwa "nikow")

(if-let [ob (resolve    ; Pobierz obiekt z bieżącej przestrzeni nazw,
             'nazwa)]   ;  którego kluczem jest symbol nazwa.
  (if (var? ob) ob))    ; Zwróć nil, jeżeli nie jest Varem.

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

Zobacz także:

Odczytywanie wartości bieżących

Odczytywanie wartości zmiennych globalnych polega na odczycie z przestrzeni nazw przyporządkowanego do symbolicznej nazwy obiektu typu Var i uzyskaniu jego wartości bieżącej w procesie dereferencji (ang. dereference).

Istnieją funkcje, które pozwalają nam samodzielnie dokonywać każdej z dwóch wspomnianych wyżej czynności (np. jedne służące do odnajdywania obiektów typu Var w przestrzeniach nazw, a inne do odczytywania ich wartości bieżących), jednak w przypadku tak powszechnie użytkowanego konstruktu 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 formy 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 nim zmienną globalną, jeżeli została ona wcześniej utworzona.

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

nazwa
; => "nikow"
(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 obiektu typu Var, aby pobrać wartość bieżącą, na którą wskazuje umieszczona w nim referencja.

Przypomnijmy, że gdybyśmy chcieli uzyskać dostęp do samego symbolu w jego 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
'nazwa ; => nazwa (type 'nazwa) ; => clojure.lang.Symbol

Pobieranie wartości, var-get

Z użyciem ns-resolve, resolve czy var możemy uzyskać 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órym powinien być obiektem typu Var, a zwraca wartość bieżącą.

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
  'user        ;    w przestrzeni nazw user
  'nazwa))     ;    symbolem nazwa.

; => "nikow"

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

; => "nikow"

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

; => "nikow"
(def nazwa "nikow") (var-get ; Pobierz wartość obiektu wskazywanego przez Vara (ns-resolve ; identyfikowanego 'user ; w przestrzeni nazw user 'nazwa)) ; symbolem nazwa. ; => "nikow" (var-get ; Pobierz wartość obiektu typu Var (resolve ; identyfikowanego w bieżącej przestrzeni 'nazwa)) ; symbolem nazwa. ; => "nikow" (var-get ; Pobierz wartość obiektu typu Var (var nazwa)) ; identyfikowanego w bieżącej przestrzeni symbolem nazwa. ; => "nikow"

Dereferencja, deref

Aby dokonać dereferencji, czyli odczytać wartość bieżącą obiektu typu Var, można skorzystać z bardziej generycznej funkcji deref, która działa również w odniesieniu do innych typów referencyjnych.

Funkcja w wariancie jednoargumentowym przyjmuje obiekt typu Var (lub inny obiekt referencyjny) i zwraca jego bieżącą wartość. Gdy wywołanie odbywa się w obrębie transakcji STM, zwracana jest wartość aktualna w tej transakcji. Jeżeli wywołanie odbywa się poza transakcją, 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 bieżącą wartość obiektu referencyjnego
 (resolve      ;  identyfikowanego w bieżącej przestrzeni nazw
  'nazwa))     ;     symbolem nazwa.

; => "nikow"

(deref         ; Pobierz bieżącą wartość obiektu referencyjnego,
 (var nazwa))  ;  identyfikowanego w bieżącej przestrzeni symbolem nazwa.

; => "nikow"
(def nazwa "nikow") (deref ; Pobierz bieżącą wartość obiektu referencyjnego (resolve ; identyfikowanego w bieżącej przestrzeni nazw 'nazwa)) ; symbolem nazwa. ; => "nikow" (deref ; Pobierz bieżącą wartość obiektu referencyjnego, (var nazwa)) ; identyfikowanego w bieżącej przestrzeni 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"
(def nazwa "nikow") @(resolve 'nazwa) ; @ zamiast deref ; => "nikow" @#'nazwa ; @ zamiast deref i #' zamiast var ; => "nikow"

Aktualizowanie referencji

Ponowne definiowanie

Aktualizować powiązanie główne zmiennej globalnej możemy z użyciem formy 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.

Wszystkie wyżej wymienione formy pozwolą nam uaktualnić bieżącą wartość istniejącej zmiennej globalnej, a także dodatkowo wytworzyć w przestrzeni nazw powiązanie symbolu z obiektem typu Var, jeżeli nie istnieje jeszcze tak nazwane przyporządkowanie.

Przykład użycia defdefonce 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 nie ustawiono
(defonce b 8)       ; --''--
a                   ; => 6 
b                   ; => 8
(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 nie ustawiono (defonce b 8) ; --''-- a ; => 6 b ; => 8

Uwaga: Forma 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żeli 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ą to w sposób niedbały, gdy uwzględnimy współbieżność. Można ich używać wtedy, gdy 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 w celu zapamiętywania odniesień do funkcji bądź do elementów konfiguracji programu – a więc zgodnie z przeznaczeniem – wspomniane problemy nie powinny się pojawiać. Spójrzmy jednak 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 utworzony wektor, będący kopią obecnego
              x))))   ;    z dodanym elementem: numerem powtórzenia.
(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 utworzony wektor, będący kopią obecnego x)))) ; z dodanym elementem: numerem powtórzenia.

Mamy tu 9 różnych wątków, z których każdy do wektora identyfikowanego symbolem wektor dodaje liczbę. Najpierw pobierana jest zawartość wektora wskazywanego przez zmienną identyfikowaną symbolem wektor, a następnie tworzony jest nowy wektor różniący się od poprzedniego dodawanym elementem. Zmienna globalna wektor jest następnie w każdym z wątków aktualizowana tak, aby odnosić się z uzyskaną strukturą (nowym wektorem). 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]
wektor ; => [0 5 6 7 8]

Brakuje wartości 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ę.

W każdym wątku mamy do czynienia z następującą sekwencją oddzielnych 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 aktualizacji powiązania głównego zmiennej wektor z nową wartością w tym samym czasie. Doszło więc do nadpisania dopiero co zaktualizowanej w innym wątku referencji. Tego typu usterkę nazywamy sytuacją wyścigu (ang. race condition).

Zobacz także:

Zmiana powiązania, alter-var-root

Zalecanym sposobem zmiany powiązania głównego w przypadku często zmieniających się wartości zmiennych globalnych 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 wartość bieżąca zmiennej. Jeżeli przekazywana funkcja przyjmuje dodatkowe argumenty, ich wartości można przekazać 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
(def x 5) (alter-var-root #'x (constantly 10)) x ; => 10

Wiemy, że użycie def czy intern ustawia główne powiązanie obiektu typu Var 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. Użyjmy więc 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            ;     zwracającą nowy wektor (kopię obecnego)
          x)))))           ;     z dodanym elementem: numerem powtórzenia.
(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 ; zwracającą nowy wektor (kopię obecnego) x))))) ; z dodanym elementem: numerem powtórzenia.

Uzyskany wektor nie ma już braków:

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

Wynika to z zastosowania 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 bazodanową transakcję.

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.

Redefinicje

Czasami zachodzi potrzeba chwilowego przesłonięcia wartości zmiennych globalnych. W takich sytuacjach – np. w celach testowych czy w przypadku odpluskwiania – 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ą powiązania główne. Po zakończeniu wykonywania podanego wyrażenia powiązania główne są przywracane.

Tworzenie redefinicji

Funkcja with-redefs-fn

Funkcja with-redefs-fn przyjmuje dwa argumenty. Pierwszym powinna być mapa, której kluczami są obiekty typu Var, a wartościami nowe powiązania główne tych obiektów. Wartością drugiego argumentu powinien być obiekt funkcji, która zostanie wykonana po zmianie powiązań.

Zwrócona zostanie wartość będąca rezultatem wykonania przekazanej funkcji.

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
(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 go używa. Jako pierwszy argument przyjmuje wektor powiązań złożony z form powiązaniowych symboli (identyfikujących zmienne globalne) i wyrażeń inicjujących, które posłużą do aktualizowania powiązań głównych wskazanych zmiennych. Kolejnymi argumentami powinny być wyrażenia, które zostaną przeliczone, gdy powiązania będą tymczasowo zmienione.

Wartością zwracaną jest rezultat wykonania ostatniego z podanych wyrażeń, a po wykonaniu makra powiązania główne są przywracane.

Użycie:

  • (with-redefs wektor-powiązań 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
(def a 2) (def b 4) (with-redefs [a 1 b 1] (+ a b)) ; => 2

Uwaga: Na czas obliczania wartości makra zmiana powiązań głównych widoczna jest we wszystkich wątkach wykonywania programu.

Walidatory

Zmienne globalne można opcjonalnie wyposażyć w mechanizmy sprawdzają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żeli jest ona niedopuszczalna, funkcja powinna zwrócić wartość false lub zgłosić 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 funkcję sprawdzającą przekazać jako drugi argument wywołania. Opcjonalnie można zamiast obiektu funkcji podać wartość nieustaloną nil – w takim przypadku walidator zostanie usunięty.

Zanim walidator zostanie ustawiony dokonywane jest sprawdzenie, czy wartość, która ma być powiązana ze zmienną globalną, jest akceptowana przez podaną funkcję. Jeżeli tak nie jest, zgłoszony będzie wyjątek, a walidator nie zostanie ustawiony.

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)
(def mniej-niż-pięć 0) (set-validator! #&#39;mniej-niż-pięć (fn [wartość] (&lt; wartość 5))) ; =&gt; nil (def mniej-niż-pięć 4) ; ok ; =&gt; #&#39;user/mniej-niż-pięć (def mniej-niż-pięć 6) ; &gt;&gt; IllegalStateException Invalid reference state ; &gt;&gt; clojure.lang.ARef.validate (ARef.java:33)

Pobieranie walidatora, get-validator

Mając obiekt typu Var, możemy pobrać funkcyjny obiekt skojarzonego z nim walidatora z użyciem funkcji get-validator. Przyjmuje ona jeden argument, który powinien być obiektem typu Var, a zwraca obiekt funkcji lub wartości nieustaloną nil, jeżeli 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 user$nasz_walidator@3182127d>
(def mniej-niż-pięć 0) (def nasz-walidator (fn [wartość] (&lt; wartość 5))) (set-validator! #&#39;mniej-niż-pięć nasz-walidator) ; =&gt; nil (get-validator #&#39;mniej-niż-pięć) ; pobranie walidatora ; =&gt; #&lt;user$nasz_walidator user$nasz_walidator@3182127d&gt;

Zmienne dynamiczne

Zmienne globalne mogą być zmiennymi dynamicznymi i korzystać z zasięgu dynamicznego (ang. dynamic scope). Oznacza to, że możliwe jest utworzenie takiej zmiennej globalnej, która w pewnych kontekstach wykonywania, niezależnie od miejsca w kodzie źródłowym, będzie powiązana z inną wartością.

Zmiana wartości bieżącej zmiennej dynamicznej z użyciem odpowiedniej formy 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 z wartością.

Zmiennych globalnych powiązanych dynamicznie możemy używać nie tylko ze względu na współbieżne wykonywanie, ale również po to, aby dla danej sekcji programu obowiązywały inne 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 podmienić 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.

  • Obszar, w którym powiązanie główne zmiennej globalnej będzie przesłonięte powiązaniem dynamicznym musi być określony S-wyrażeniem podanym jako jeden z argumentów makra binding lub makr, które na nim bazują.

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
(def ^{:dynamic true} dynamiczna 5) (def ^:dynamic dynamiczna 5) ; uproszczona wersja zapisu

Przesłanianie wartości, 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ązań, w którym nieparzyste elementy par to symbole w formach powiązaniowych identyfikujące zmienne dynamiczne, a parzyste to (po przeliczeniu) 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ść nieustaloną nil, jeżeli nie podano wyrażeń.

Użycie:

  • (binding wektor-powiązań & 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
(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 ; =&gt; 7 ; dynamo ma wartość 7 (funkcja) ; wywołanie funkcji odczytującej ; =&gt; 5 ; dynamo ma wartość 5

Możemy zaobserwować, że wartość zmiennej globalnej dynamo w ciele wywołania binding została przesłonięta przez dynamicznie 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
(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 form, ale też funkcji, które potem mogą być redefiniowane w zależności od kontekstu. Otwiera to drogę do programowania aspektowego.

Przesłanianie wielu, 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 powinno być mapowe wyrażenie inicjujące, czyli mapa, której kluczami są obiekty typu Var, a wartościami nowe wartości bieżące tych obiektów. Kolejne argumenty makra zostaną przeliczone, a wszelkie wyrażenia w kontekście ich ewaluacji, w których korzysta się ze zmiennych globalnych podanych w mapowym S-wyrażeniu, będą korzystały z przesłoniętych wartości bieżących.

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
(def ^:dynamic a 2) (def ^:dynamic b 4) (defn sumator [] (+ a b)) (with-bindings { #&#39;a 1 #&#39;b 2 } (sumator)) ; =&gt; 3

Przesłanianie w funkcji, 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ź formach specjalnych) do zmiennych dynamicznych będzie korzystało z przesłoniętych wartości określonych kluczami podanej mapy.

Użycie:

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

Pierwszym argumentem funkcji powinno być mapowe wyrażenie inicjujące, czyli mapa, a kolejnym funkcja, która zostanie wywołana. Jako dodatkowe, opcjonalne argumenty można podać wartości, które zostaną przekazane jako argumenty wywoływanej funkcji. Wartością zwracaną będzie rezultat jej wywołania.

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
(def ^:dynamic a 2) (def ^:dynamic b 4) (defn sumator [] (+ a b)) (with-bindings* { #&#39;a 1 #&#39;b 2 } sumator) ; =&gt; 3

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

Aktualizowanie wartości bieżącej, set!

Dzięki forme specjalnej set! możemy zmieniać wartość zmiennej dynamicznej w bieżącym wątku wykonywania i w kontekście form with-bindings, with-bindings* bądź binding. Przyjmuje ona dwa argumenty, które są formą powiązaniową: niezacytowany symbol i wyrażenie, inicjujące nową wartość bieżącą.

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
;; definicja zmiennej dynamicznej (def ^:dynamic *dynamo* 108) ; =&gt; #&#39;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 ; =&gt; 4 ;; odczyt zmiennej dynamicznej *dynamo* ; =&gt; 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
;; definicja zmiennej dynamicznej (def ^:dynamic *dynamo* 108) ; =&gt; #&#39;user/dynamo ;; użycie var-set na obiekcie typu Var (binding [*dynamo* 21] (var-set #&#39;*dynamo* 4) *dynamo*) ; =&gt; 4 ;; odczyt powiązania głównego *dynamo* ; =&gt; 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 ^:dynamic *nazwa-pliku*  "/var/log/plik")
(def ^:dynamic *rozmiar-okna*            1024)
(def ^:dynamic *tryb-odpluskwiania*      true)
(def ^:dynamic *alternatywne-wyjście*        )
(def ^:dynamic *nazwa-pliku* &#34;/var/log/plik&#34;) (def ^:dynamic *rozmiar-okna* 1024) (def ^:dynamic *tryb-odpluskwiania* true) (def ^:dynamic *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 Lispu oznacza to ciepłe uczucie bezpiecznego obcowania z programem.

W praktyce oznaczanie dynamicznych źródeł danych „nausznikami” informuje programistę, że powinien uważać, ponieważ rezultaty ich odczytów nie są przewidywalne i zależą od otoczenia, a nie tylko od kalkulacji.

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 zasięg będzie ograniczony do obszaru leksykalnego.

Zmienne lokalne

Zmienne lokalne (ang. local variables) w Clojure 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 leksykalnie powiązane 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, 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 powiązań podanym jako pierwszy argument i wyrażenia do przeliczenia podane jako kolejne argumenty. Do skojarzonych z lokalnymi zmiennymi wartości będzie można odwoływać się w tych wyrażeniach z użyciem ich symbolicznych nazw.

Makro with-local-vars zwraca wartość ostatniego przeliczanego wyrażenia lub wartość nieustaloną nil, jeżeli 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-->
(with-local-vars [a 1 b 2] b) ; =&gt; #&lt;Var: --unnamed--&gt;

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. W związku z tym konieczne jest zastosowanie odpowiednich funkcji bądź wyrażenia dereferencyjnego w postaci makra czytnika @.

Odczytywanie wartości bieżących, var-get

W wyrażeniach przekazanych do makra with-local-vars widoczne są obiekty typu Var, których odczyt możliwy jest z użyciem funkcji var-get, deref lub wyrażenia dereferencyjnego skonstruowanego z użyciem przedrostka @ (znak małpki). Odczytywane są wartości bieżące zmiennych (izolowanych 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
(with-local-vars [a 1 b 2] (+ (var-get a) (var-get b))) ; =&gt; 3 (with-local-vars [a 1 b 2] (+ @a @b)) ; =&gt; 3

Aktualizowanie wartości bieżących, 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 izolowane w obrębie lokalnego wątku powiązanie główne.

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)           ; wątek I, aktualizacja powiązania głównego
  (future (println @a))   ; >> 2 (wątek II, wartość z powiązania głównego)
  (future (var-set a 3))  ; aktualizacja powiązania w wątku II
  (Thread/sleep 2000)     ; opóźnienie 2s
  (+ @a @b))              ; wątek I, sumowanie wartości
; => 4
(with-local-vars [a 1 b 2] (var-set a 2) ; wątek I, aktualizacja powiązania głównego (future (println @a)) ; &gt;&gt; 2 (wątek II, wartość z powiązania głównego) (future (var-set a 3)) ; aktualizacja powiązania w wątku II (Thread/sleep 2000) ; opóźnienie 2s (+ @a @b)) ; wątek I, sumowanie wartości ; =&gt; 4

Testowanie właściwości zmiennych

Testowanie typu, 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
(def a 100) (var? #&#39;a) ; =&gt; true

Testowanie powiązania, thread-bound?

Predykat bound? pozwala sprawdzić, czy podane jako argumenty obiekty typu Var są referencyjnie powiązane z 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
(def ggg) (bound? #&#39;ggg) ; =&gt; false (def ggg 5) (bound? #&#39;ggg) ; =&gt; true (with-redefs [a 5] (bound? #&#39;a)) ; =&gt; true (with-local-vars [a 5] (bound? a)) ; =&gt; true

Test izolowania 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
(def a 5) (def ^:dynamic dynamo 5) (thread-bound? #&#39;a) ; =&gt; false (thread-bound? #&#39;dynamo) ; =&gt; false (with-redefs [a 5] (thread-bound? #&#39;a)) ; =&gt; false (with-local-vars [a 5] (thread-bound? a)) ; =&gt; true (binding [dynamo 10] (thread-bound? #&#39;dynamo)) ; =&gt; true

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

Zobacz także

Jesteś w sekcji .
Tematyka:

Taksonomie: