Poczytaj mi Clojure – cz. 5: Przestrzenie nazw i powiązania

Powiązania pozwalają nazywać i wskazywać pamięciowe obiekty, z których korzystamy w programach (nadawać im stałe tożsamości), a przestrzenie nazw umożliwiają zarządzanie widocznością i kapsułkowanie fragmentów kodu. W tym odcinku dowiemy się, jak rozumieć te mechanizmy i jak ich używać.

Przestrzenie nazw i powiązania

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

Inżynieria oprogramowania wyróżnia dwie ważne właściwości, które decydują o tym jak i gdzie możemy korzystać z nazw identyfikujących: widoczność (ang. visibility) i zasięg (ang. scope).

W Clojure widocznością możemy sterować, korzystając z przestrzeni nazw, natomiast zasięg zależy od rodzaju użytej konstrukcji i kontekstu – może być leksykalny, dynamiczny lub nieograniczony. Widoczność identyfikatora może być ograniczona zarówno jego zasięgiem w danym kontekście, jak i bezpośrednio określona przestrzenią nazw.

Przestrzenie nazw

W Clojure funkcjonuje pojęcie przestrzeni nazw (ang. namespace). Dzięki temu mechanizmowi można organizować poszczególne elementy programów i separować symboliczne identyfikatory, które pochodzą z różnych źródeł, aby unikać konfliktów nazewnictwa funkcji, makr i innych pamięciowych obiektów.

Przestrzeń nazw jest w sensie abstrakcyjnym słownikiem terminów, z których każdy powinien być niepowtarzalny w jej obrębie. W przypadku Clojure przestrzenie nazw to w istocie mapy zawierające odwzorowania symboli na przypisane im:

  • zmienne globalne (obiekty typu Var),
  • odniesienia do klas Javy (obiektów typu Class),
  • odniesienia do obiektów typu Var z innych przestrzeni.

Dodatkowo dla każdej przestrzeni utrzymywana jest mapa aliasów, które pozwalają odwoływać się do innych przestrzeni nazw z użyciem przypisanych im alternatywnych identyfikatorów.

Zmienne globalne służą zazwyczaj do przechowywania informacji konfiguracyjnych lub podprogramów (np. funkcji czy makr). Klasy Javy z kolei pozwalają korzystać z ekosystemu JVM. Z kolei odniesienia do zmiennych globalnych z innych przestrzeni oszczędzają palce podczas wpisywania identyfikatorów (nie trzeba podawać nazw przestrzeni), jeśli tylko zostaną zaimportowane.

Dwie przestrzenie nazw mogą zawierać dwie takie same symboliczne nazwy, które odnoszą się do różnych obiektów, np. funkcji. Na przykład w Clojure istnieje funkcja clojure.core/replace, a także clojure.string/replace. Programista może wybrać, której funkcji chce użyć, podając jej nazwę uzupełnioną o nazwę przestrzeni. Dzięki temu można unikać konfliktów, szczególnie podczas korzystania z bibliotek zewnętrznego autorstwa.

Odniesienia do Varów z innych przestrzeni tworzy się z użyciem funkcji refer lub use, odniesienia do klas Javy z użyciem import, natomiast nowe zmienne globalne odwzorowywane są automatycznie w chwili ich stwarzania (np. z użyciem def czy intern).

Same przestrzenie nazw umieszczone są w jednej, globalnej przestrzeni nazw. Znaczy to, że nie mogą istnieć dwie przestrzenie o takiej samej nazwie.

Wartym odnotowania jest też fakt, że odwzorowania przestrzeni nazw mogą być publiczne (ang. public) lub prywatne (ang. private). Te ostatnie nie będą widoczne poza przestrzenią nazw, w której zostały umieszczone.

Gdy przygotowywane jest środowisko uruchomieniowe języka Clojure, tworzona jest przestrzeń nazw clojure, w której symbolowi *ns* przypisywana jest globalna zmienna dynamiczna identyfikująca bieżącą przestrzeń nazw. Następnie do przestrzeni clojure dodawane są pary odniesień, które identyfikują wbudowane funkcje, formuły specjalne i makra języka. Odniesienia są odwołaniami do przyporządkowań z innych przestrzeni (np. z clojure.core), które stają się widoczne w przestrzeni bieżącej.

Wśród automatycznie dodawanych odniesień znajdziemy między innymi takie, które identyfikują funkcje: in-ns (służy do ustawiania bieżącej przestrzeni nazw), import (przypisuje nazwy klas podanego pakietu Javy do symbolicznych nazw w bieżącej przestrzeni) i refer (przypisuje w bieżącej przestrzeni symbole do obiektów Var z innej przestrzeni). Dzięki temu programista może korzystać z przestrzeni nazw i sprawiać, by pojawiały się w nich wybrane przyporządkowania, które pozwolą na zarządzanie widocznością identyfikatorów w kodzie.

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

Tworzenie przestrzeni nazw

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

Tworzenie przestrzeni, create-ns

Funkcja create-ns przyjmuje nazwę przestrzeni nazw w formie stałej symbolu i tworzy podaną przestrzeń, jeśli jeszcze nie istnieje. W przypadku, gdy przestrzeń o podanej nazwie już została stworzona, nie jest podejmowana żadna czynność.

Użycie:

  • (create-ns symboliczna-nazwa).

Funkcja przyjmuje nazwę przestrzeni wyrażoną symbolem w formie stałej, a zwraca obiekt przestrzeni nazw.

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

Zobacz także:

Używanie przestrzeni nazw

Korzystanie z przestrzeni nazw polega na odwoływaniu się do rozmaitych obiektów z użyciem formuł symbolowych, czyli symboliniezacytowanej postaci, które identyfikują inne obiekty. Umieszczając taki symbol w kodzie źródłowym programu, sprawiamy, że przeszukana będzie (z użyciem funkcji resolve) odpowiednia przestrzeń nazw, a następnie pobrana będzie przypisana do symbolicznej nazwy wartość obiektu, który jest nią identyfikowany.

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

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

Po operacji przeszukiwania z przestrzeni nazw zostanie pobrany skojarzony z symboliczną nazwą obiekt. Będzie to albo zmienna globalna typu Var, albo obiekt Javy. Jeżeli mamy do czynienia z przypisanym symbolowi odesłaniem do obiektu Var w innej przestrzeni nazw, to zostanie ono użyte, aby go uzyskać.

Ustawianie przestrzeni bieżącej, in-ns

Przestrzeń bieżąca nie jest stała. Odwołanie do niej rezyduje w globalnej zmiennej dynamicznej o nazwie *ns*. Możemy w danym pliku źródłowym (lub nawet w wyrażeniu będącym argumentem makra binding) dokonać przełączenia przestrzeni przez zmianę wartości tej zmiennej.

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

Użycie:

  • (in-ns symboliczna-nazwa).

Funkcja przyjmuje jeden argument, który powinien być symbolem w formie stałej. Jeżeli określona nim przestrzeń nazw jeszcze nie istnieje, to będzie wywołana funkcja create-ns, aby ją utworzyć. Po wykonaniu funkcji powiązanie dynamicznej, globalnej zmiennej *ns* zostanie zmienione i zwrócony obiekt przestrzeni.

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

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

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

Ustalanie nazwy przestrzeni, namespace

Przestrzenie nazw mają swoje nazwy. Dzięki funkcji namespace możemy na podstawie podanego symbolu zawierającego w sobie nazwę przestrzeni, uzyskać właśnie tę informację.

Użycie:

  • (namespace symboliczna-nazwa).

Funkcja nie dokonuje przeszukania przestrzeni nazw, po prostu pozyskuje z podanego jako argument symbolu odpowiedni składnik.

Zwracaną wartością jest łańcuch tekstowy lub nil, jeśli mamy do czynienia z symbolem bez dookreślonej przestrzeni nazw.

Przykład użycia funkcji namespace
1
2
(namespace 'przestrzeń/jakaś-nazwa)
; => "przestrzeń"

Wyszukiwanie obiektów, ns-resolve

Dzięki funkcji ns-resolve można uzyskać obiekt identyfikowany symbolem, jeśli symbol o takiej nazwie został przypisany do niego w przestrzeni nazw podanej jako pierwszy argument (jako symbol lub obiekt przestrzeni). Ostatnim argumentem jest symbol w formie stałej, którego poszukujemy.

Funkcja zwraca obiekt o podanej, wyrażonej symbolem nazwie lub wartość nil, jeżeli w danej przestrzeni nie znaleziono odwzorowania.

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

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

Użycie:

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

Wyszukiwanie w przestrzeni bieżącej, resolve

Funkcja resolve jest mniej wymagającą wersją ns-resolve. Nie trzeba jej podawać nazwy przestrzeni nazw, ponieważ domyślnie korzysta ona z bieżącej (określonej zmienną globalną *ns*). Można jednak podać symbol z dookreśloną przestrzenią nazw, a funkcja skorzysta z tej informacji. Wartością zwracaną jest obiekt typu Var.

Użycie:

  • (resolve            symboliczna-nazwa),
  • (resolve środowisko symboliczna-nazwa).
Przykłady użycia funkcji resolve
1
2
3
4
5
6
7
8
9
10
(resolve 'replace)
; => #'clojure.core/replace

;; podajemy środowisko zawierające symbol
(resolve #{'replace} 'replace)
; => nil

;; podajemy symbol z dookreśloną przestrzenią nazw
(resolve 'clojure.string/replace)
; => #'clojure.core/replace

Zarządzanie przestrzeniami

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

Pobieranie listy przestrzeni, all-ns

Funkcja all-ns nie przyjmuje żadnych argumentów, a zwraca sekwencję (obiekt typu IteratorSeq) zawierającą wszystkie zdefiniowane przestrzenie nazw w postaci ich obiektów.

Użycie:

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

Wyszukiwanie przestrzeni, find-ns

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

Użycie:

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

Usuwanie przestrzeni, remove-ns

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

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

Użycie:

  • (remove-ns nazwa-przestrzeni).
Przykłady użycia funkcji remove-ns
1
2
(remove-ns 'clojure.string)
; => nil

Zarządzanie odwzorowaniami

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

Dodawanie odniesień do obiektów Var, refer

Funkcja refer umożliwia dodawanie nowych odniesień do Varów umieszczonych w innych przestrzeniach nazw. Przyjmuje ona symbolicznie wyrażoną nazwę źródłowej przestrzeni nazw jako pierwszy argument i opcjonalnie dodatkowe argumenty wyrażające filtry, które zostaną użyte, aby uszczegółowić przeprowadzane operacje.

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

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

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

Możliwe do zastosowania filtry to:

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

Filtry te powinny składać się z nazwy w formie słowa kluczowego, po której następuje sekwencja symboli, określająca wartości filtra, którymi będą symboliczne nazwy do uwzględnienia (:only) lub pominięcia (:exclude). Sekwencjami mogą być dowolne kolekcje (np. wektory) wyposażone w sekwencyjny interfejs dostępu. Jeżeli zażądano przemianowania (:rename), to zamiast sekwencji należy podać mapę zawierającą pary przekształceń. W ten sposób do danej zmiennej globalnej będzie można odwoływać się w bieżącej przestrzeni pod inną symboliczną nazwą.

Użycie:

  • (refer nazwa-przestrzeni & filtry).
Przykłady użycia funkcji refer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
;; stwórz odniesienia do wszystkich Varów z clojure.string
(refer 'clojure.string)
; => nil

;; stwórz odniesienie tylko do identyfikatora 'replace'
(refer 'clojure.string :only '[replace])
; => nil

;; stwórz odniesienia do wszystkich Varów z clojure.string
;; za wyjątkiem 'replace' i 'reverse'
(refer 'clojure.string :exclude '[replace reverse])
; => nil

;; stwórz odniesienia do wszystkich Varów z clojure.string
;; za wyjątkiem 'replace', a 'reverse' zamień na 'nazad'
(refer 'clojure.string
       :exclude '[replace]
       :rename  '{reverse nazad})
; => nil

Zobacz także:

Dodawanie odniesień do klas Javy, import

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

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

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

Funkcja zwraca wartość obiekt ostatnio zaimportowanej klasy, a w przypadku nieistniejącego pakietu lub nazwy klasy generowany jest wyjątek.

Użycie:

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

Gdzie lista-symboli to:

  • (symbol-pakietu symbole-nazw-klas).
Przykłady użycia funkcji import
1
2
3
4
5
6
7
8
9
;; importowanie pojedynczych klas
(import java.util.Date)

;; importowanie wybranych klas z pakietu
(import '(java.util Date Dictionary))

;; użycie obiektu
(Date.)
; => #inst "2015-04-02T11:35:43.980-00:00"

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

Zobacz także:

Internalizowanie obiektów Var, intern

Funkcja intern służy do internalizowania zmiennych globalnych. Przyjmuje ona dwa argumenty: symbolicznie wyrażoną nazwę lub obiekt przestrzeni i nazwę zmiennej w postaci symbolu w formie stałej.

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

W wersji trójargumentowej funkcja inicjuje obiekt wartością, to znaczy ustawia wewnątrz zmiennej globalnej referencję, która odnosi się do innego obiektu pamięciowego. Jeżeli wartość już istniała, to jest aktualizowana, a Var pozostaje ten sam.

Funkcja zwraca obiekt typu Var identyfikowany symbolem.

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

Użycie:

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

Możemy sprawić, aby internalizowana zmienna globalna została potraktowana jako prywatna, tzn. była widoczna tylko w przestrzeni nazw, do której ją dodano. Można w tym celu użyć tzw. metadanych symboli, z których intern skorzysta. Chodzi tu konkretnie o metainformację oznaczoną kluczem :private.

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

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

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

Przykład zmiany powiązania głównego
1
2
3
4
5
6
7
8
(intern 'user 'xx 5)      ; utworzenie i powiązanie z wartością 5

(pprint #'xx)             ; wyświetlenie obiektu
; >> #<[email protected]: 5>

(intern 'user 'xx 7)      ; powiązanie z wartością 7
(pprint #'xx)             ; wyświetlenie obiektu
; >> #<[email protected]: 7>

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

Dodawanie obiektów Var, def

Formuła specjalna def działa bardzo podobnie do intern, ale operuje na bieżącej przestrzeni nazw i wymaga podania niezacytowanego symbolu jako pierwszego argumentu.

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

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

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

Użycie:

  • (def symbol łańcuch-dokumentujący? wartość-początkowa?).
Przykłady użycia funkcji def
1
2
3
4
5
6
7
8
9
10
11
(def zmienna)                   ; => #'user/zmienna
(def zmienna 5)                 ; => #'user/zmienna
(def zmienna "dokumentacja" 5)  ; => #'user/zmienna
user/zmienna                    ; => 5
zmienna                         ; => 5

;; dostęp do dokumentacji – funkcja doc
(doc zmienna)
; >> user/zmienna
; >>  dokumentacja zmiennej
; => nil

W przypadku def również można dodać do symbolu metadaną, która wskaże, że w przestrzeni nazw przyporządkowanie ma być prywatne.

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

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

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

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

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

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

Zmienne globalne mogą być aktualizowane przez ponowne wywołanie funkcji def. Aktualizowanie polega na powiązaniu ich z nowymi wartościami. Jeżeli zmienna już istnieje, to jej obiekt w przestrzeni nazw nie zostanie zastąpiony innym, zostanie po prostu zmienione jego odniesienie do konkretnej wartości.

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

(def xx 7)                ; powiązanie z wartością 7
(pprint #'xx)             ; wyświetlenie obiektu
; >> #<[email protected]: 7>

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

Jednokrotne dodawanie obiektów Var, defonce

Makro defonce pozwala stworzyć obiekt typu Var i umieścić go w bieżącej lub określonej symbolem przestrzeni nazw. Działa podobnie do def, jednak nie dokonuje aktualizowania powiązania, jeśli obiekt typu Var już je ma.

Makro nie pozwala na ustawianie łańcuchów dokumentacyjnych, bowiem przyjmuje tylko dwa argumenty: niezacytowany symbol (mogący zawierać informację o przestrzeni nazw) i wyrażenie, którego stała wartość po przeliczeniu stanie się powiązaniem głównym.

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

Makro defonce przydaje się przy nazywaniu obiektów funkcyjnych i wszędzie tam, gdzie trzeba odwoływać się do stałej wartości, która powinna być wynikiem pierwszego wartościowania jakiegoś wyrażenia lub pierwszego pobrania danych z zewnątrz.

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

Użycie:

  • (defonce symbol wyrażenie).
Przykłady użycia funkcji defonce
1
2
3
4
5
6
7
8
9
10
(defonce zmienna)                 ; => #'user/zmienna
(defonce zmienna 5)               ; => #'user/zmienna
(defonce ^:flaszka zmienna 1000)  ; => #'user/zmienna
user/zmienna                      ; => 5
zmienna                           ; => 5

(meta #'zmienna)
; => { :ns #<Namespace user>, :name zmienna,
; =>   :flaszka true, :file "NO_SOURCE_PATH",
; =>   :column 1, :line 1 }

Usuwanie odwzorowań, ns-unmap

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

Funkcja zwraca wartość nil, a jeśli podana przestrzeń nie istnieje, to generowany jest wyjątek.

Użycie:

  • (ns-unmap przestrzeń-nazw symboliczna-nazwa).
Przykład użycia funkcji ns-unmap
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
;; tworzymy zmienną globalną x
(def x 5)
; => #'user/x

;; sięgamy po jej wartość
x
; => 5

;; dodatkowo tworzymy odwołanie do obiektu Var tej zmiennej
(def y (var x))

;; usuwamy odwzorowanie
(ns-unmap 'user 'x)
; => nil

;; sprawdzamy, czy identyfikator x jest wciąż widoczny
(resolve 'x)
; => nil

;; sprawdzamy, czy sam obiekt Var istnieje,
;; choć nie jest już powiązany z symbolem x
(deref y)
; => 5

W powyższym przykładzie pokazaliśmy przy okazji, że gdy usuwane jest odwzorowanie w przestrzeni nazw, to sam obiekt klasy Var (lub inny) nie jest niszczony – po prostu traci nazwę.

Dodawanie aliasów, alias

Mechanizm aliasów pozwala odwoływać się do różnych przestrzeni nazw z użyciem łatwiejszych do zapisania i odczytu identyfikatorów.

Dzięki funkcji alias można do bieżącej przestrzeni nazw dodawać alternatywne nazwy innych przestrzeni nazw. Funkcja przyjmuje dwa argumenty. Pierwszy powinien być symbolem w formie stałej, a drugi obiektem przestrzeni nazw lub jej nazwą wyrażoną symbolem w stałej formie. Pierwszy argument to nazwa aliasu, a drugi przestrzeń nazw, do której odniesienie ma być wytworzone.

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

Użycie:

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

Zobacz także:

Usuwanie aliasów, unalias

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

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

Użycie:

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

Odczytywanie zawartości

Pobieranie nazwy, ns-name

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

Użycie:

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

Pobieranie aliasów, ns-aliases

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

Użycie:

  • (ns-aliases przestrzeń-nazw).
Przykłady użycia funkcji ns-aliases
1
2
3
(ns-aliases 'clojure.core)     ; => {jio #<Namespace clojure.java.io>}
(alias 'stri 'clojure.string)  ; => nil
(ns-aliases 'user)             ; => {stri #<Namespace clojure.string>}

Pobieranie odwzorowań obiektów Var, ns-interns

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

Użycie:

  • (ns-interns przestrzeń-nazw).
Przykład użycia funkcji ns-interns
1
2
3
4
(ns-interns 'user)
; => {apropos-better #'user/apropos-better, cdoc #'user/cdoc,
; =>  find-name #'user/find-name, help #'user/help,
; =>  clojuredocs #'user/clojuredocs}

Pobieranie odniesień do obiektów Var, ns-refers

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

Użycie:

  • (ns-refers przestrzeń-nazw).
Przykład użycia funkcji ns-refers
1
2
3
(ns-refers 'user)
; => {primitives-classnames #'clojure.core/primitives-classnames,
; =>  +' #'clojure.core/+', …}

Pobieranie odniesień do klas Javy, ns-imports

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

Użycie:

  • (ns-imports przestrzeń-nazw).
Przykład użycia funkcji ns-imports
1
2
(ns-imports 'user)
; => {Enum java.lang.Enum, InternalError java.lang.InternalError, …}

Pobieranie wszystkich odwzorowań, ns-map

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

Użycie:

  • (ns-map przestrzeń-nazw).
Przykład użycia funkcji ns-map
1
2
(ns-map 'user)
; => {primitives-classnames #'clojure.core/primitives-classnames, … }

Pobieranie odwzorowań publicznych, ns-publics

Funkcja ns-publics przyjmuje jeden argument, którym powinien być symbol w formie stałej określający nazwę przestrzeni nazw lub obiekt tej przestrzeni, a zwraca mapę zawierającą publiczne przyporządkowania symbolicznych identyfikatorów do obiektów. Jeśli przestrzeń nie istnieje, wygenerowany zostanie wyjątek.

Użycie:

  • (ns-publics przestrzeń-nazw).
Przykład użycia funkcji ns-publics
1
2
3
(ns-publics 'user)
; => {apropos-better #'user/apropos-better, cdoc #'user/cdoc,
; =>  find-name #'user/find-name, help #'user/help, clojuredocs #'user/clojuredocs}

Obsługa bibliotek

Biblioteka (ang. library) to zbiór umieszczonych w plikach zasobów zawierających kod źródłowy, który może być używany przez aplikację lub inną bibliotekę. W bibliotece mogą znajdować się dane, podprogramy (np. makra czy funkcje), a nawet nowe typy danych. Dzięki bibliotekom możliwe jest ponowne korzystanie z już zaprogramowanych metod rozwiązywania problemów.

Wczytywanie bibliotek

Ładowanie bibliotek programistycznych, a następnie umieszczanie potrzebnych odwołań w bieżącej przestrzeni nazw wymaga skorzystania z kilku przedstawionych wcześniej funkcji i makr (np. refer czy import). Na szczęście programiści nie muszą zbytnio się trudzić, ponieważ istnieją odpowiednie makra, które pozwalają w zwięzły sposób wyrazić co ma być załadowane i jakie dodatkowe czynności należy przeprowadzić na przestrzeniach nazw.

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

Wczytywanie samodzielne, load

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

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

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

Ostatni element podanej ścieżki będzie potraktowany jak nazwa pliku do wczytania i zostanie do niego dołączony łańcuch tekstowy z rozszerzeniem .clj.

Użycie:

  • (load & ścieżka…)
Przykład użycia funkcji load
1
2
3
4
5
6
7
8
9
;; ładowanie pliku projektu
;; src/projekt/core.clj
(load "projekt/core")
; => nil

;; ładowanie pliku głównego
;; biblioteki clojure.string 
(load "/clojure/string")
; => nil

Makro require

Makro require ładuje zewnętrzne biblioteki programistyczne. Każdy podawany argument powinien być jedną z kilku klauzul:

  • specyfikacja biblioteki (ang. library spec),
  • lista przedrostkowa (ang. prefix list),
  • flaga modyfikatora (ang. modifier flag).

Specyfikacja biblioteki to albo symbolicznie wyrażona nazwa biblioteki, albo wektor zawierający jej nazwę oraz dodatkowe parametry, których nazwy są słowami kluczowymi, a zawierającą je strukturą sekwencyjna kolekcja. Dzięki parametrom specyfikacji biblioteki możemy zdecydować co stanie się zaraz po załadowaniu jej do pamięci.

Możliwe parametry to:

  • :as symboliczna-nazwa – korzysta z alias i wytwarza odniesienie do ładowanej biblioteki pod podaną nazwą w bieżącej przestrzeni nazw;
  • :refer (symboliczne-nazwy) – korzysta z refer i dla sekwencji symbolicznie wyrażonych nazw wytwarza odniesienia w bieżącej przestrzeni nazw (podanie klucza :all oznacza żądanie wytworzenia odniesień do wszystkich publicznych zmiennych globalnych);

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

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

  • :reload – wymusza ponowne wczytanie bibliotek do pamięci, nawet jeśli już zostały wczytane;
  • :reload-all – działa jak :reload, ale wpływa na wszystkie biblioteki zależne, ładowane przez właśnie wczytywaną (jeśli jest w nich czyniony użytek z use lub require);
  • :verbose – sprawia, że wypisane zostaną informacje dotyczące ładowania i tworzenia odniesień.

Makro działa w ten sposób, że dla każdej wczytywanej biblioteki stwarzana jest przestrzeń nazw i pakiet Javy – nazwy tych dwóch ostatnich są takie same, jak podana nazwa symboliczna. Załadowanie biblioteki jest w istocie wczytaniem jej pliku głównego (ang. root file), zlokalizowanego w katalogu głównym biblioteki. Plik główny jest poszukiwany według następującego schematu:

  • każda kropka z podanej nazwy biblioteki zostaje zmieniona w separator nazwy ścieżkowej (np. a.b staje się a/b);
  • ostatnia część nazwy uznawana jest za nazwę pliku (np. b.clj);
  • pozostała część nazwy uznawana jest za nazwę katalogu głównego (np. a);
  • względna ścieżka wraz z nazwą pliku jest dołączana do kolejnych ścieżek przeszukiwania klas, aż zostanie odnaleziony główny plik biblioteki.

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

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

Użycie:

  • (require & specyfikacja… & lista-przedrostkowa… & flaga…).
Przykłady użycia makra require
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
;; wczyta bibliotekę i stworzy przestrzeń nazw clojure.string
(require 'clojure.string)
; => nil

;; możemy podać kilka bibliotek
(require 'clojure.string 'clojure.test 'clojure.set)

;; możemy podać kilka bibliotek o wspólnym przedrostku
(require '[clojure string test set])

;; wczyta bibliotekę i stworzy przestrzeń nazw clojure.string
;; nawet, jeśli już była wczytana
(require 'clojure.string :reload :verbose)
; => (clojure.core/load "/clojure/string")
; => nil

;; wczyta bibliotekę, jeśli jeszcze nie była wczytana
;; i stworzy alias w bieżącej przestrzeni nazw, aby
;; przestrzeń clojure.string była widoczna jako st
(require '[clojure.string :as st] :verbose)
; => (clojure.core/in-ns 'user)
; => (clojure.core/alias 'st 'clojure.string)
; => nil

Makro use

Makro use działa tak samo jak require i w ten sam sposób się je wywołuje, ale automatycznie dodawane są odniesienia do każdej zmiennej globalnej z ładowanej biblioteki, z wykorzystaniem funkcji refer.

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

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

Użycie:

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

Od wydania 1.4 języka Clojure zaleca się korzystać z makra require lub ns z odpowiednimi parametrami.

Makro ns

Makro ns zostało stworzone, aby nie trzeba było wywoływać innych makr i funkcji związanych z obsługą przestrzeni nazw, lecz zgrupować wszystkie ważne czynności w jednym miejscu (np. w nagłówkowej części pliku z kodem źródłowym).

Pozwala ono określić bieżącą przestrzeń nazw (utworzyć, jeśli jeszcze nie istnieje i przełączyć się na nią), a następnie dokonywać wczytywania potrzebnych plików z kodem źródłowym, importowania wszystkich lub wybranych odwzorowań i generowania pseudokodu dla podanych klas.

Makro przyjmuje nazwę przestrzeni, na którą należy się przełączyć i opcjonalny zestaw tzw. klauzul referencyjnych, które mogą zawierać polecenia wykonania dodatkowych operacji. Klauzule powinny być ujęte w nawiasy okrągłe i rozpoczynać się słowami kluczowymi, będącymi ich nazwami. Argumenty po kluczach będą przekazane do wywoływanych funkcji oraz makr i nie muszą być cytowane.

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

Użycie:

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

Klauzule referencyjne:

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

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

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

Jeżeli nie zachodzi proces kompilacji, to klauzula :gen-class jest ignorowana. Jeśli z kolei nie podano jej, a kompilacja się odbywa, to wytworzony będzie wyłącznie kod dla nazwa-przestrzeni__init.class.

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

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

Następnie konstruowane są aliasy dla przestrzeni nazw clojure.set, clojure.string, randomseed.pl.zasoby.plikowe w celu ich łatwiejszego dookreślania. W tej samej sekcji tworzone są odwołania do zmiennych globalnych z przestrzeni clojure.repl (dla docdir), aby można było je wywoływać bez podawania przestrzeni.

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

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

Powiązania

drugim rozdziale podręcznika zdefiniowano powiązanie (ang. binding) jako element języka, który służy do utrzymywania stałej tożsamości. Jest to ogólna definicja, dlatego spróbuję wyjaśnić termin od strony praktycznej.

Powiązania przypominają zmienne z języków imperatywnych. Dzięki nim można identyfikować obiekty umieszczone w pamięci. Identyfikacja ta polegała będzie na:

W przypadku formuł powiązaniowych mamy do czynienia z niezmiennymi powiązaniami symbolicznych identyfikatorów z wartościami. Ich wartości nie można aktualizować, lecz możliwe jest przesłanianie przez powiązanie symbolu o takiej samej nazwie z inną wartością.

W przypadku typów referencyjnych możemy dokonywać aktualizacji wartości bieżących, do których się odnoszą, korzystając z odpowiednich funkcji.

Rodzaje powiązań

Technicznie rzecz ujmując, możemy mieć do czynienia z trzema głównymi rodzajami powiązań:

  • symboli z wartościami,
  • obiektów referencyjnych z wartościami,
  • zmiennych dynamicznych z wartościami.

Zmienne dynamiczne są obsługiwane przez jeden z typów referencyjnych, jednak wyróżniamy je z osobna, ponieważ cechuje je tzw. zasięg dynamiczny.

Powiązania symboli

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

  • przestrzeniach nazw (ang. namespaces):

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

    • ze zmiennymi leksykalnymi (konstrukcje let, loop i podobne);
    • lokalnymi obiektami Var (konstrukcja with-local-vars);
    • argumentami funkcji w ich definicjach (tzw. powiązania parametryczne – konstrukcje fn, defn);

a dodatkowo:

  • w abstrakcyjnych powiązaniach strukturalnych (ang. structural bindings):
    • powiązaniami leksykalnymi (konstrukcja let i podobne),
    • argumentami funkcji (konstrukcja fn i podobne),
    • argumentami makr, które przekładają się na wyżej wymienione.

Powiązania obiektów referencyjnych

Powiązania obiektów typu referencyjnego służą do śledzenia zmiennych stanów tożsamości wyrażanych różnymi wartościami na przestrzeni czasu, a miejscem, w którym przechowywana jest informacja o powiązaniach z wartościami są obiekty referencyjne, np.:

Powiązania dynamiczne

Powiązania dynamiczne (ang. dynamic bindings) służą do przesłaniania powiązań zmiennych globalnych. Zmienne takie nazywamy wtedy zmiennymi dynamicznymi. Od zwykłych zmiennych globalnych różnią się sposobem inicjowania obiektu typu Var, a skorzystanie z dynamicznego powiązania realizowane jest z użyciem makra binding.

Powiązania strukturalne

Powiązania strukturalne (ang. structural bindings) to powiązania leksykalne lub parametryczne, w których dochodzi do dekompozycji struktury asocjacyjnej (np. mapy) lub sekwencyjnej (np. wektora).

Zasięgi powiązań

Zasięg (ang. scope) to obszar programu, w którym dane powiązanie może być użyte:

Poza zasięgiem powiązania możemy też mówić o jego zakresie widoczności (ang. visibility), czyli o obszarze korzystania z niego bez konieczności odwoływania się do pełnej nazwy. Widoczność zależy od zasięgu, ale można też dodatkowo nią sterować, korzystając z przestrzeni nazw.

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

W Clojure możemy mieć do czynienia z kilkoma rodzajami zasięgów: nieograniczonym, leksykalnymdynamicznym.

Zasięg nieograniczony

W przypadku symbolicznie identyfikowanych zmiennych globalnych bądź klas Javy, możemy mówić o zasięgu nieograniczonym (ang. indefinite scope), to znaczy o potencjalnej możliwości odwołania się do wskazywanych obiektów z dowolnego miejsca w programie.

Sterowanie widocznością w tym zasięgu możliwe jest dzięki przestrzeniom nazw, w których wspomniane obiekty są identyfikowane symbolami.

Przykład definiowania i używania zmiennej globalnej
1
2
3
(def x 5)          ; zmienna globalna x (powiązanie główne z wartością 5)
(defn funk [] x)   ; funkcja, która zwraca wartość zmiennej globalnej x
(funk)             ; wywołanie funkcji

Zasięg leksykalny

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

Zasięg leksykalny jest często wykorzystywany w wielu językach programowania, w których jednym z podstawowych elementów programu są zmienne. Można mówić wtedy np. o lokalnym zasięgu leksykalnym (w obrębie ciała funkcji czy pewnego bloku kodu).

W Clojure z zasięgiem tym mamy do czynienia:

  • gdy wprost to wyrazimy (korzystając z odpowiednich makr czy formuł specjalnych);
  • w definicjach funkcji i makr (przy obsłudze ich parametrów).

Formuła specjalna let i wektor powiązaniowy

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

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

Użycie:

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

gdzie wektor-powiązaniowy to:

  • [formuła-powiązaniowa wyrażenie-inicjujące …].

Pierwszym argumentem, jaki należy przekazać konstrukcji let, jest wektor powiązaniowy (ang. binding vector). Jest to wektorowe S-wyrażenie, które powinno składać się z tzw. par powiązaniowych. Pierwsze elementy tych par powinny być formułami powiązaniowymi, a drugie (przypisane do nich) wartościami, czyli tzw. wyrażeniami inicjującymi.

Formuły powiązaniowe możemy wyrażać z użyciem:

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

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

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

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

Warto zaznaczyć, że w przypadku let nie mamy do czynienia z obiektami typu Var, lecz ze stałymi powiązaniami służącymi do identyfikacji podanych wartości. Powiązania symboli z wartościami stworzone w wektorze powiązaniowym przechowywane są na stosie (ang. stack), natomiast nowe wartości powstające w rezultacie obliczania wyrażeń inicjujących zajmują przestrzeń sterty (ang. heap).

Możemy przesłaniać powiązania leksykalne, tworząc nowe, które bazują na tych samych symbolicznych nazwach:

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

Formuła let zwraca wartość ostatnio obliczonego wyrażenia lub wartość nil, jeśli żadnego wyrażenia nie podano.

Powiązania z warunkiem, if-let

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

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

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

Użycie:

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

Powiązania z funkcjami, letfn

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

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

1
2
3
4
(let [x (fn [a] (+ 2 a))]
  (x 2))

; => 4

Funkcje możemy też wywoływać w wektorze powiązaniowym, czyli zwracane wartości traktować jak wyrażenia inicjujące lub ich składniki:

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

; => 7

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

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

; >> java.lang.RuntimeException: Unable to resolve symbol: x in this context

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

Użycie:

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

gdzie wektor-specyfikacji-funkcji to:

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

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

Przykład użycia makra letfn
1
2
3
4
5
(letfn [(y [a] (+ 3 (x a)))
        (x [a] (+ 2 a))]
  (y 2))

; => 7

Zobacz także:

Powiązania z warunkiem, when-let

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

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

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

Użycie:

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

Powiązania pierwszego niepustego, when-first

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

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

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

Użycie:

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

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

Powiązania wartościowych, when-some

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

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

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

Użycie:

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

Powiązania wartościowych z warunkiem, if-some

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

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

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

Użycie:

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

Powiązania w pętli, loop i recur

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

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

Użycie:

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

Zobacz także:

Parametry funkcji

Z leksykalnym zasięgiem spotkamy się również, gdy zdefiniujemy funkcję, która przyjmuje jakieś argumenty. Mówimy wtedy o powiązaniach parametrycznych, czyli tworzeniu powiązań parametrów (wyrażonych symbolami) z wartościami argumentów przekazanych podczas wywołania.

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

(fn [a b]              ; definicja funkcji anonimowej; parametry a i b
  (+ a b))             ; widoczne tylko w ciele funkcji (w granicach S-wyrażenia)

Zmienne lokalne, with-local-vars

Istnieje również lokalny zasięg leksykalny dla obiektów typu Var. Można z nich korzystać, gdy jakiś problem trzeba wyrazić imperatywnie i zachodzi konieczność użycia odpowiednika lokalnych zmiennych. Służy do tego makro with-local-vars, które dokładniej omówiono w rozdziale poświęconym obiektom typu Var.

Użycie:

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

Zobacz także:

Zasięg dynamiczny

Zasięg dynamiczny (ang. dynamic scope) to zasięg, w którym mamy do czynienia z przesłanianiem (ang. shadowing) istniejących powiązań zmiennych globalnych (o zasięgu nieograniczonym) przez utrzymywanie dla każdej z nich globalnego stosu powiązań. Dzieje się to niezależnie od kontekstu leksykalnego i wymaga użycia specjalnej formuły.

Stos powiązań to struktura, której zadaniem jest obsługa przesłaniania wartości bieżącej globalnej zmiennej, ale tylko w pewnym czasowym kontekście wykonywania.

Jeśli istnieje zmienna globalna, dla której w jakimś momencie tworzony jest przez programistę zasięg dynamiczny, to na stosie przypisanym do tej zmiennej zostanie umieszczone jej nowe powiązanie z wartością. Będzie ono ze stosu zdjęte dopiero wtedy, gdy zakończone zostanie obliczanie wyrażenia, w którym ustanowiono dynamiczne powiązanie (w przypadku Clojure ciało makra binding, które omówione jest niżej).

Jeśli podczas wykonywania się programu, w którym mamy zmienną globalną o dynamicznym zasięgu, pojawia się kolejne jej przesłonięcie (spowodowane wprowadzeniem nowego dynamicznego zasięgu), to powiązanie znów wędruje na skojarzony z daną zmienną stos.

Każde odwołanie do zmiennej globalnej, dla której istnieje niepusty stos powiązań dynamicznych, skutkuje zwróceniem wartości, do której odnosi się ostatnie (najbardziej aktualne) powiązanie na tym stosie. Dzieje się to niezależnie od kontekstu leksykalnego. Powiązanie dynamiczne możemy więc nazwać powiązaniem, które trwa pewien czas, w przeciwieństwie do powiązań leksykalnych, które obejmują pewne obszary (fragmenty) kodu źródłowego.

Gdy na stosie nie ma żadnego powiązania dynamicznego, to używane jest tzw. powiązanie główne zmiennej globalnej.

Powiązania dynamiczne zmiennych globalnych realizowane są przez przesłanianie powiązań obiektów referencyjnych reprezentujących te zmienne, a nie przez przesłanianie odwzorowań symboli w przestrzeniach nazw. Poza tym dynamiczne przesłonięcia zmiennych globalnych dokonywane są zawsze w bieżącym wątku wykonywania. Jeśli w pozostałych wątkach nie utworzono dynamicznego powiązania (z użyciem konstrukcji binding), zmienna zachowa w nich aktualne powiązanie główne.

Tworzenie powiązań dynamicznych

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

Przykład użycia powiązań dynamicznych
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
(def ^:dynamic *x* 5)                   ; zmienna dynamiczna *x*

(def obiekt-x                           ; zmienna globalna obiekt-x
  (var *x*))                            ; wskazuje na obiekt Var zmiennej *x*

(defn chwal-się []                      ; funkcja wyświetlająca wartość *x*
  (println " *x* po nazwie:"
           *x* "\n"                     ; odczyt powiązanej wartości
           "*x* po obiekcie:"
           @obiekt-x "\n"))             ; dereferencja obiektu

(defn testuj []                         ; funkcja testująca
  (binding [*x* 10]                     ; zasięg dynamiczny *x*
    (println "* zasięg dynamiczny")
    (chwal-się))                        ; wywołanie funkcji w zasięgu
  (println "* zasięg nieograniczony:")  ; dynamicznym
  (chwal-się))                          ; wywołanie funkcji poza
                                        ; dynamicznym zasięgiem
(testuj)

Zobacz także:

Dekompozycja

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

Z dekompozycji możemy korzystać w wektorze powiązaniowym formuły specjalnej let, wektorze parametrycznym formuły fn oraz makra defn, a także w konstrukcjach, które korzystają z wymienionych (np. for czy doall).

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

Przykład samodzielnego powiązywania wartości z symbolami i dekompozycji
1
2
3
4
5
6
7
8
9
10
11
12
(def dane [1 2 3])   ; wektor z trzema wartościami

;; ręczne powiązywanie wybranych elementów z symbolami 
(let [a (first dane)
      b (first (rest dane))
      c (first (rest (rest dane)))]
  (list a b c))
; => (1 2 3)

;; dekompozycja
(let [[a b c] dane] (list a b c))
; => (1 2 3)

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

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

  • w miejscu pojedynczego symbolu pojawia się (zawierające różne symbole) wyrażenie powiązaniowe (ang. binding expression), na przykład wektorowe wyrażenie powiązaniowe lub mapowe wyrażenie powiązaniowe;
  • wartością przypisanego mu wyrażenia inicjującego będzie wieloelementowa struktura (np. wektor, mapa, lista, rekord itp.).
Nazwy wyrażeń i formuł powiązaniowych na przykładzie wywołania let
1
2
3
4
5
6
7
8
(let   [           ; wektor powiązaniowy:
                   ; · pierwsza para:
        dane       ;   · symbol (formuła powiązaniowa)
        [1 2 3]    ;   · wyrażenie inicjujące (formuła wektorowa)
                   ; · druga para:
        [a b c]    ;   · wektorowe wyrażenie powiązaniowe (formuła dekompozycyjna)
        dane]      ;   · wyrażenie inicjujące (formuła symbolowa)
  (list a b c))

Dekompozycja pozycyjna

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

Dekompozycji pozycyjnej możemy używać zarówno w wektorach powiązaniowych formuł specjalnych (takich jak np. let czy binding), jak również w wektorach parametrycznych definicji funkcji.

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

Wektorowa formuła dekompozycyjna

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

Użycie:

  • [[symbol…] wyrażenie-inicjujące].
Przykłady dekompozycji pozycyjnej w konstrukcji let
1
2
3
4
5
6
7
8
9
10
11
(let [[a b c] [1 2 3]]        ; a -> 1, b -> 2, c -> 3
  (list a b c))
; => (1 2 3)

(let [wektor    [4 5 6]       ; powiązanie z wektorem 
      sekwencja (seq wektor)  ; powiązanie z sekwencją na bazie wektora
      [a b c]   [1 2 3]       ; powiązania z dekompozycji wyrażenia wektorowego
      [d e f]   wektor        ; powiązania z dekompozycji wektora
      [g    ]   sekwencja]    ; powiązania z dekompozycji sekwencji
  (list a b c d e f g))       ; stworzenie listy z wartościami powiązań
; => (1 2 3 4 5 6 4)

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

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

(funkcja 1 2 3)
; => (1 2 3)

Ignorowanie elementów

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

Użycie:

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

Warto wiedzieć, że symbol _ ma specjalne znaczenie tylko na zasadzie konwencji. W jego miejsce można podać dowolny inny symbol, który nie będzie używany i może zostać wielokrotnie przesłonięty.

Grupowanie elementów

Inną opcją może być zgrupowanie wszystkich pozostałych, niepowiązanych elementów w jednym, wariadycznym powiązaniu. Służy do tego symbol ampersandu umieszczony przed nazwą symbolu.

Użycie:

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

Dostęp do oryginalnej sekwencji

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

Użycie:

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

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

Dekompozycja asocjacyjna

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

Dekompozycji asocjacyjnej możemy używać zarówno w wektorach powiązaniowych (np. formuły specjalnej let czy makra binding), jak również w wektorach parametrycznych definicji funkcji.

Mapowa formuła dekompozycyjna

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

Do wyrażania tej operacji służy mapowe wyrażenie powiązaniowe (ang. map binding expression), które skrótowo można nazywać mapą powiązaniową (ang. binding map). Należy umieścić ją jako pierwszy element każdej pary powiązaniowej w wektorze powiązaniowym lub zamiast pojedynczego parametru w wektorze parametrycznym funkcji. Kluczami tej mapy powinny być niezacytowane symbole, a wartościami klucze źródłowej struktury (niekoniecznie wyrażone słowami kluczowymi).

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

Użycie:

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

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

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

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

W przypadku wektorów parametrycznych funkcji wyrażenie inicjujące pochodzi z przekazywanych do funkcji argumentów.

Przykład użycia mapy powiązaniowej w wektorze parametrycznym
1
2
3
4
5
6
7
8
;;              WEKTOR  PARAMETRYCZNY
;;                mapa  powiązaniowa
(defn funkcja [ & {a :a, b :b, c :c} ]
  (list a b c))

;;        argumenty (wyrażenie inicjujące)
(funkcja         :a 1, :b 2, :c 3)
; => (1 2 3)

Klucze mapy powiązaniowej

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

W mapie powiązaniowej należy podać parę, której kluczem jest słowo kluczowe :keys, a przypisaną wartością wektor zawierający niezacytowane symbole lub słowa kluczowe określające nazwy słów kluczowych odpowiadających kluczom destrukturyzowanej struktury, których wartości chcemy powiązać z symbolami o takich samych nazwach.

Użycie:

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

(let [{:keys [a b c]} {:a 1, :b 2, :c 3}]
  (list a b c))
; => (1 2 3)

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

Użycie:

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

(let [{:syms [a b c]} '{a 1, b 2, c 3}]
  (list a b c))
; => (1 2 3)

Dostęp do oryginalnej asocjacji

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

Użycie:

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

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

Wartości domyślne

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

Po słowie kluczowym :or należy podać mapę określającą domyślne wartości dla kluczy.

Użycie:

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

Dekompozycja asocjacyjna wektorów

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

Użycie:

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

Struktury zagnieżdżone

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

Przykład dekompozycji zagnieżdżonej struktury
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(def dane-osobowe
  {:imię     "Paweł"
   :nazwisko "Wilk"
   :płeć     :m
   :kontakty {:telefony [123456, 543210]
              :e-maile  ["pw-at-gnu.org"]}})

(let [{:keys [imię nazwisko płeć], {[telefon] :telefony
                                    [e-mail]  :e-maile} :kontakty}
      dane-osobowe
      nazwa-płci (if (= płeć :m) "mężczyzna" "kobieta")]
  (println "Imię i nazwisko: " imię nazwisko)
  (println "Płeć:            "    nazwa-płci)
  (println "Telefon:         "       telefon)
  (println "E-mail:          "        e-mail))

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

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

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

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

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

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

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

Klucze w pełni kwalifikowane

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

Przykłady dekompozycji z kluczami o dookreślonych przestrzeniach nazw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(def dane-osobowe
  {:imię "Paweł"
   :kontakty/telefony [123456, 543210]
   :kontakty/e-maile  ["pw-at-gnu.org"]})

(let [{:keys [imię],
       [telefon] :kontakty/telefony,
       [e-mail]  :kontakty/e-maile} dane-osobowe]
  (println "Imię:   "    imię)
  (println "Telefon:" telefon)
  (println "E-mail: "  e-mail))

(let [{:keys [imię kontakty/telefony kontakty/e-maile]} dane-osobowe]
  (println "Imię:    "     imię)
  (println "Telefony:" telefony)
  (println "E-maile: "  e-maile))
Przykład dekompozycji z kluczami z bieżącej przestrzeni nazw
1
2
3
4
5
6
7
8
9
10
(ns user)
(def dane-osobowe
  {:imię "Paweł"
   ::telefony [123456, 543210]
   ::e-maile  ["pw-at-gnu.org"]})

(let [{:keys [imię ::telefony user/e-maile]} dane-osobowe]
  (println "Imię:    "     imię)
  (println "Telefony:" telefony)
  (println "E-maile: "  e-maile))
Przykład dekompozycji z symbolami o dookreślonych przestrzeniach nazw
1
2
3
4
5
6
7
8
9
10
11
(def dane-osobowe
  {'imię "Paweł"
   'kontakty/telefony [123456, 543210]
   'kontakty/e-maile  ["pw-at-gnu.org"]})

(let [{:syms [imię],
       [telefon] 'kontakty/telefony,
       [e-mail]  'kontakty/e-maile} dane-osobowe]
  (println "Imię:   "    imię)
  (println "Telefon:" telefon)
  (println "E-mail: "  e-mail))

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

Diagnozowanie dekompozycji

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

Dekompozycja do tekstu, destructure

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

Użycie:

  • (destructure powiązania).

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

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

Przykład użycia funkcji destructure
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
(def dane-osobowe
  {'imię "Paweł"
   'kontakty/telefony [123456, 543210]
   'kontakty/e-maile  ["pw-at-gnu.org"]})

(destructure '[{:syms
                [imię],
                [telefon] 'kontakty/telefony,
                [e-mail]  'kontakty/e-maile} dane-osobowe])

; => [map__10728
; =>  dane-osobowe
; =>  map__10728
; =>  (if
; =>   (clojure.core/seq? map__10728)
; =>   (clojure.lang.PersistentHashMap/create (clojure.core/seq map__10728))
; =>   map__10728)
; =>  vec__10729
; =>  (clojure.core/get map__10728 (quote kontakty/telefony))
; =>  telefon
; =>  (clojure.core/nth vec__10729 0 nil)
; =>  vec__10730
; =>  (clojure.core/get map__10728 (quote kontakty/e-maile))
; =>  e-mail
; =>  (clojure.core/nth vec__10730 0 nil)
; =>  imię
; =>  (clojure.core/get map__10728 (quote imię))]        

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(let [mapa-danych       dane-osobowe
      mapa-danych       (if (seq? mapa-danych)
                          (apply hash-map (seq mapa-danych))
                          mapa-danych)
      wektor-telefonów  (get mapa-danych 'kontakty/telefony)
      wektor-e-maili    (get mapa-danych 'kontakty/e-maile)
      imię              (get mapa-danych 'imię)
      telefon           (nth wektor-telefonów 0 nil)
      e-mail            (nth wektor-e-maili   0 nil)]
  {:imię    imię
   :telefon telefon
   :e-mail  e-mail})

; => {:e-mail "pw-at-gnu.org" :imię "Paweł" :telefon 123456}

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

Komentarze