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.
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).
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ęgu
i poziomem 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?)
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:
- Opis funkcji
intern
w rozdziale poświęconym przestrzeniom nazw.
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ść.
Z 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…)
.
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?)
.
Obliczenie wyrażeń z powyższego przykładu sprawi, że:
W bieżącej przestrzeni nazw pojawi się odwzorowanie symbolu
nazwa
na internalizowany obiekt typuVar
.Obiekt typu
Var
zostanie powiązany z łańcuchem znakowym"nikow"
(stanie się on wartością powiązania głównego tego obiektu referencyjnego).W bieżącej przestrzeni nazw pojawi się odwzorowanie kojarzące symbol
funka
z kolejnym utworzonym obiektem typuVar
.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:
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ść:
Zobacz także:
- Opis formy specjalnej
def
w rozdziale poświęconym przestrzeniom nazw.
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)
Zobacz także:
- Opis makra
defonce
w rozdziale poświęconym przestrzeniom nazw.
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.
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)
.
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)
.
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:
- Opis funkcji
ns-resolve
w rozdziale poświęconym przestrzeniom nazw.
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)
.
Zobacz także:
- Opis funkcji
resolve
w rozdziale poświęconym przestrzeniom nazw.
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.
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:
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)
.
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)
.
Dzięki istnieniu odpowiedniego makra czytnika powyższy przykład można zapisać krócej:
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.
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:
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:
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:
- Odczyt wartości bieżącej wskazywanej przez obiekt typu
Var
. - Stworzenie nowego wektora na bazie uzyskanego.
- 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:
- Opis formy specjalnej
def
w rozdziale poświęconym przestrzeniom nazw, - opis formy specjalnej
defonce
w rozdziale poświęconym przestrzeniom nazw, - opis funkcji
intern
w rozdziale poświęconym przestrzeniom nazw.
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 atomowy i synchroniczny
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)
.
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
:
Uzyskany wektor nie ma już braków:
Wynika to z zastosowania atomowej operacji aktualizowania wartości zmiennej globalnej, która polega na:
- Odczycie wartości bieżącej.
- Obliczeniu nowej wartości na bazie poprzedniej z użyciem przekazanej funkcji.
- 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-redefs
i with-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)
.
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)
.
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!
i 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)
.
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)
.
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
).
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…)
.
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:
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ń.
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.
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)
.
Zmienne dynamiczne można też aktualizować z wykorzystaniem funkcji
var-set
, która została omówiona przy okazji opisywania zmiennych
lokalnych:
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).
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…
.
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).
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)
.
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ść)
.
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…)
.
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…)
.
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
- http://clojure.org/vars
- http://clojure.org/namespaces
- http://pramode.net/clojure/2010/05/15/story-of-cloure-vars-part-2/
- https://blog.rjmetrics.com/2012/01/11/lexical-vs-dynamic-scope-in-clojure/
- https://stackoverflow.com/questions/1774417/scoping-rules-in-clojure
- https://stackoverflow.com/a/1987033/617851
- https://groups.google.com/d/msg/clojure/SKrLmJKnm8Q/bR0uG3q5tBgJ
- https://clojuredocs.org/clojure.core/intern
- http://clojure.org/metadata