stats

Poczytaj mi Clojure, cz. 3

Podstawowe konstrukcje

Grafika

Specyficzna składnia dialektów języka Lisp pozwala precyzyjnie określać i rozróżniać podstawowe konstrukcje, dodawać nowe elementy syntaktyczne, a także przekształcać formy programu jako dane podczas rozwijania makr. Wynika to z zastosowania prostych, jednak przemyślanych sposobów organizowania i reprezentowania kodu źródłowego.

Podstawowe konstrukcje

Programy pisane w odmianach języka Lisp charakteryzują się prostymi regułami składniowymi. Zamiast dyskutować o nich teoretycznie, rozpoczniemy praktycznym przykładem, na który będziemy się powoływać w celu poznania podstawowych mechanizmów rządzących tłumaczeniem kodu źródłowego na postać zrozumiałą dla komputera. Śledząc, co robi kompilator, lepiej zrozumiemy konstrukcje języka.

Oto nasz bazowy przykład:

(print "Witaj, Lispie!")
(print "Witaj, Lispie!")

Nie jest specjalnie trudny. Prawda?

Składnia

Lisp z wyglądu przypomina owsiankę
z wmieszanymi obciętymi paznokciami.
 
 
— Larry Wall

Zacznijmy od składni (ang. syntax). Pierwszym, co rzuca się w oczy, gdy widzimy programy napisane w dialektach Lispu, jest umieszczanie niemal każdej złożonej konstrukcji w nawiasach. W innych językach programowania nawiasy służą do grupowania wybranych elementów składniowych, np. argumentów podczas wywoływania bądź definiowania funkcji, zestawu warunków czy działań na wartościach. W Lispach nawiasy są podstawowym elementem leksykalnym, używanym do nadawania kształtu całemu programowi i każdemu z wyrażeń.

W językach typu Lisp konstruujemy wyrażenia z zastosowaniem tzw. notacji polskiej (ang. Polish notation, skr. PN), zwanej też zapisem przedrostkowym (ang. prefix notation). Polega ona na tym, że najpierw umieszczamy operator (nazwę funkcji), a następnie operandy (argumenty wywołania). Nawiasów używamy, aby oznaczać początki i końce wyrażeń.

Notacja przedrostkowa jest różna od popularnego w wielu językach programowania zapisu wrostkowego (ang. infix), lecz nie na tyle, żeby wielce utrudniało to rozpoznawanie poszczególnych części wyrażeń. Nasz przykładowy program możemy w Rubym przedstawić tak:

print "Witaj, Lispie!"
print "Witaj, Lispie!"

A w języku C w następujący sposób:

#include <stdio.h>

int main(int argc, char *argv[]) {
    printf("Witaj, Lispie!");
    return 0;
}
#include &lt;stdio.h&gt; int main(int argc, char *argv[]) { printf(&#34;Witaj, Lispie!&#34;); return 0; }

Różnice między omawianymi rodzajami zapisu daje się dobrze zilustrować operacjami matematycznymi. Popatrzmy na dwa proste działania:

2 + 2 * 3
2 + 2 * 3

I zapis zgodny ze składnią języka Clojure:

(+ 2 (* 2 3))
(+ 2 (* 2 3))

Możemy zauważyć, że w drugim przykładzie operator dodawania i jego operandy są ujęte w nawiasy, a operacja znajduje się na pierwszej pozycji każdej wartościowanej listy. Zaletą tego zapisu jest brak konieczności pamiętania o pierwszeństwie operatorów (ang. operator precedence).

Poza tym zapis przedrostkowy nadaje zagnieżdżonym wywołaniom jeden regularny kształt. Clojure nadal używa innych ograniczników dla literałów wektorów, map i zbiorów, ale nie potrzebuje osobnej tabeli pierwszeństwa operatorów wrostkowych. Korzyścią jest regularność gramatyki, a nie obietnica, że każdy parser zapisu przedrostkowego będzie szybszy.

Czytnik

Wróćmy do naszego programu:

(print "Witaj, Lispie!")
(print &#34;Witaj, Lispie!&#34;)

Pierwszym krokiem jest wczytanie tekstu do pamięci. Czytnik (ang. reader) pobiera następną formę ze strumienia znaków i zwraca reprezentowaną przez nią wartość Clojure. Otwarcie pliku lub strumienia terminalu należy do wywołującego; sam czytnik jest użyteczny niezależnie od kompilacji i wartościowania. Na potrzeby objaśnienia możemy wyróżnić dwa zadania pojęciowe:

  1. związaną z wykrywaniem znanych konstrukcji leksykalnych w tekście;

  2. związaną z wyodrębnianiem spośród znalezionych konstrukcji gramatycznie poprawnych wyrażeń i reprezentowaniem ich w postaci wewnętrznych, pamięciowych struktur.

Analiza leksykalna

Pierwsza faza wczytywania źródeł programu do pamięci to analiza leksykalna (ang. lexical analysis). Polega ona na:

  • oczyszczeniu wejścia ze zbędnych symboli;

  • rozpoznaniu w strumieniu znaków sekwencji pasujących do zdefiniowanych w leksykonie języka jednostek leksykalnych (ang. lexical units), w tym kontekście zwanych tokenami (ang. tokens);

  • wydzieleniu z tekstu fragmentów, które mają znaczenie składniowe (tzw. leksemów, ang. lexemes), będących swego rodzaju instancjami wykrytych tokenów.

Patrząc leksykalnie, możemy wskazać w przykładzie następujące fragmenty znaczące:

Leksem Nazwa tokenu
( literał listowy (otwierający)
print symbol
"Witaj, Lispie" literał łańcucha znakowego
) literał listowy (zamykający)

Słownictwo to jest użytecznym modelem opisowym; nie oznacza, że Clojure udostępnia osobny strumień tokenów lub publiczny tokenizer. Publiczna operacja read pobiera jedną pełną formę i zwraca jej wartość.

Analiza składniowa

Drugim zadaniem pojęciowym jest parsowanie (ang. parsing). Obejmuje ono:

  • rozpoznanie w strumieniu leksemów konstrukcji składniowych (ang. syntactic constructs) przez porównanie ich rodzajów i umiejscowienia z regułami gramatycznymi języka;

  • wczytywanie zagnieżdżonych form według reguł ograniczników i znaków makrowych;

  • wytworzenie pamięciowych reprezentacji znalezionych wyrażeń z użyciem odpowiednich struktur danych;

Rezultatem są dane Clojure reprezentujące następną formę. Kompilator może później przeanalizować te dane do własnej reprezentacji wewnętrznej, lecz wynik czytnika nie jest tym AST kompilatora.

Formy czytnika

Formy czytnika (ang. reader forms) to zapisy tekstowe, które czytnik Clojure rozpoznaje i zmienia w wartości.

Poniższa tabela zawiera podstawowe formy czytnika. W pierwszej kolumnie umieszczono nazwę tokenu, w drugiej przykłady leksemów, a w ostatniej typ wartości zwracanej przez czytnik, jeżeli wejście jest poprawne.

Nazwa tokenu Przykłady leksemów Typ danych
symbol raz
przestrzeń/dwa
Symbol
literał pusty nil nil
literał kluczowy :raz
::dwa
:przestrzeń/x
::przestrzeń/y
Keyword
literał łańcuchowy "raz dwa" java.lang.String
literał listowy (1 2 3) PersistentList
literał wektorowy [1 2 3] PersistentVector
literał mapowy {:a 1 :b 2}
#::{:a 1 :b 2}
#:przestrzeń{:a 1 :b 2}
PersistentArrayMap
PersistentHashMap
literał logiczny true, false java.lang.Boolean
literał liczby całkowitej 1
0xff
017
2r1101
java.lang.Long
literał liczby wymiernej 1/2 Ratio
literał liczby dużej 1.2M
1N
java.math.BigDecimal
Bigint
literał liczby zmiennoprzecinkowej -2.7e-4 java.lang.Double

Makra czytnika

Niektóre znaki uruchamiają specjalne zachowanie podczas wczytywania i tradycyjnie są nazywane makrami czytnika (ang. reader macros) lub znakami makrowymi. Zachowanie Clojure łączy wbudowane reguły z wewnętrzną tabelą odczytu, której programy nie mogą modyfikować. Literały oznaczone z kwalifikowanymi znacznikami są kontrolowanym punktem rozszerzeń, który odwzorowuje znacznik na funkcję czytającą dane.

Poniższa tabela przedstawia zestawienie makr czytnika:

Nazwa tokena Przykłady leksemów Wynik czytnika
cytowanie (ang. quote) 'raz
'(raz dwa)
lista rozpoczynająca się od quote
cytowanie składniowe
(ang. syntax-quote)
`raz
`(dwa trzy)
różne
cofanie
cytowania składniowego

(ang. syntax unquote)
~cytat różne
cofanie
cytowania składniowego z rozplataniem

(ang. syntax unquote-splicing)
~@(list 1 2) różne
mapa metadanowa
(ang. metadata map)
^{:doc "Opis"} x następna forma z metadanymi
klucz metadanowy
(ang. metadata key)
^:dynamic x następna forma z metadanymi
znacznik metadanowy
(ang. metadata tag)
^Integer x następna forma z metadanymi
komentarz
(ang. comment)
; komentarz brak
literał znakowy
(ang. character literal)
\a, \b, \c, \newline java.lang.Character
wyrażenie dereferencyjne
(ang. dereference expression)
@x lista rozpoczynająca się od deref
makro dyspozycyjne (ang. dispatch macro) # różne

Ostatnia pozycja w tabeli to tzw. makro dyspozycyjne. Jest to ogólna nazwa określająca podgrupę makr czytnika, których wszystkie tokeny rozpoczynają się symbolem kratki (#). Gdy czytnik zauważa ten znak, przekazuje kontrolę nad dalszą analizą konstrukcji do oddzielnej tablicy makr. Poniżej znajduje się lista tokenów, których obsługa odbywa się z użyciem makra dyspozycyjnego:

Nazwa tokena Przykłady leksemów Wynik czytnika
cytowanie Varów
(ang. var-quote)
#'x lista rozpoczynająca się od var
literał zbiorowy
(ang. set literal)
#{1 2 3} PersistentHashSet
wyrażenie regularne
(ang. regular expression)
#"raz.dw[aA]" java.util.regex.Pattern
literał funkcji anonimowej
(ang. anonymous function literal)
#(pr %)
#(pr %1 %2)
lista rozpoczynająca się od fn*
ignorowanie następnej formy
(ang. ignore next form)
#_ raz dwa brak
literał rekordu/typu
(ang. record/type literal)
#nazwa.typ[:x]
#nazwa.rekord{:x 1}
instancja nazwanego typu Clojure
wyrażenie warunkowe czytnika
(ang. reader conditional)
#?(:clj "Clojure"
     :cljs "ClojureScript")
wybrana forma lub brak
literał oznaczony
(ang. tagged literal)
#symbol argument różne
literał oznaczony inst
(ang. inst tagged literal)
#inst "2018-11-12" java.util.Date
literał oznaczony UUID
(ang. UUID tagged literal)
#uuid "88b05082-392d-4e0a-89c9-cf62ec375c43" java.util.UUID

Kilka pozycji rozwija się do zwykłych list, które dopiero później uzyskują znaczenie wykonywalne. Przykładowo @x jest wczytywane jako (clojure.core/deref x), a #'x jako (var x). Z kolei #_ powoduje całkowite pominięcie następnej formy. Literały oznaczone wywołują podczas wczytywania zarejestrowane funkcje czytające dane, dlatego wczytywanie zaufanego kodu Clojure i parsowanie niezaufanego EDN mają odmienne kontrakty bezpieczeństwa.

S-wyrażenia

Na poziomie gramatycznym każdy element kodu źródłowego w Lispie jest symbolicznie zapisanym wyrażeniem. Tekstowe reprezentacje wyrażeń – te, które widzimy w edytorze – to tzw. wyrażenia symboliczne (ang. symbolic expressions), nazywane skrótowo S-wyrażeniami (skr. S-expressions, sexprs, sexps).

Struktura S-wyrażeń przypomina trochę XML-a bądź JSON-a, tzn. mamy do czynienia z zapisem, który pozwala wyrażać zagnieżdżone i uporządkowane zestawy wartości, chociaż w nieco prostszy sposób niż wymienione formaty.

S-wyrażenie w Lispie możemy zdefiniować jako rodzaj notacji, w której:

  • każdy element jest wyrażeniem:
    • niezłożonym (zwanym atomem) lub
    • składającym się z S-wyrażeń ujętych w nawiasy
      i oddzielonych separatorem.
Przykłady S-wyrażeń w Lispie
print                     ; atomowe S-wyrażenie (nie lista S-wyrażeń)
"Witaj, Lispie!"          ; atomowe S-wyrażenie (nie lista S-wyrażeń)
(print "Witaj, Lispie!")  ; listowe S-wyrażenie
()                        ; listowe i atomowe S-wyrażenie jednocześnie
(print (+ 2 2))           ; S-wyrażenie złożone z zagnieżdżonych S-wyrażeń
print ; atomowe S-wyrażenie (nie lista S-wyrażeń) &#34;Witaj, Lispie!&#34; ; atomowe S-wyrażenie (nie lista S-wyrażeń) (print &#34;Witaj, Lispie!&#34;) ; listowe S-wyrażenie () ; listowe i atomowe S-wyrażenie jednocześnie (print (+ 2 2)) ; S-wyrażenie złożone z zagnieżdżonych S-wyrażeń

S-wyrażenie w Clojure będziemy definiowali w nieco bogatszy sposób, ponieważ mamy tam do czynienia z dodatkowymi literałami kolekcji. Będzie nim rodzaj notacji, w której:

  • każdy element jest wyrażeniem:
    • niezłożonym (zwanym atomem) lub
    • składającym się z odseparowanych S-wyrażeń:
      • w parach, ujętych w nawiasy klamrowe – {…};
      • w pojedynkę, ujętych w:
        • nawiasy okrągłe – (…);
        • klamry z symbolem kratki – #{…};
        • nawiasy kwadratowe – […].

W przypadku wyrażeń w nawiasach klamrowych pierwsze elementy par muszą być unikatowymi wartościami w całym wyrażeniu, a w przypadku wyrażeń w nawiasach klamrowych z symbolem kratki każda wartość musi być unikatowa. Sprawdzanie wykonywane jest już podczas analizy składniowej, a gdy podany element wymaga wcześniejszego wyliczenia, podczas ewaluacji.

Przykłady S-wyrażeń w Clojure
:raz                      ; atomowe S-wyrażenie
1                         ; atomowe S-wyrażenie
print                     ; atomowe S-wyrażenie
"Witaj, Lispie!"          ; atomowe S-wyrażenie
(print "Witaj, Lispie!")  ; listowe S-wyrażenie
[1 2 3]                   ; wektorowe S-wyrażenie
#{1 2 3}                  ; zbiorowe S-wyrażenie
{:raz 1 :dwa 2 :trzy 3}   ; mapowe S-wyrażenie
(str [1 2 3])             ; listowe i wektorowe S-wyrażenia
:raz ; atomowe S-wyrażenie 1 ; atomowe S-wyrażenie print ; atomowe S-wyrażenie &#34;Witaj, Lispie!&#34; ; atomowe S-wyrażenie (print &#34;Witaj, Lispie!&#34;) ; listowe S-wyrażenie [1 2 3] ; wektorowe S-wyrażenie #{1 2 3} ; zbiorowe S-wyrażenie {:raz 1 :dwa 2 :trzy 3} ; mapowe S-wyrażenie (str [1 2 3]) ; listowe i wektorowe S-wyrażenia

Rekurencyjna definicja S-wyrażenia może wydawać się mało zrozumiała, więc wspomożemy się naszym jednolinijkowym programem i dokonamy ręcznej kategoryzacji obecnych w nim elementów.

W zapisie (print "Witaj, Lispie!"):

  • (…) jest S-wyrażeniem, bo jest symbolicznie zapisaną listą S-wyrażeń;
  • print jest S-wyrażeniem, bo jest atomem;
  • "Witaj, Lispie!" jest S-wyrażeniem, bo jest atomem.

Graficznie można ten zestaw przedstawić w następujący sposób:

S-wyrażenie

W odróżnieniu od innych Lispów złożone S-wyrażenia w Clojure budowane są nie tylko w oparciu o listy oznaczone nawiasami okrągłymi, ale również na bazie dodatkowych znaczników, które odpowiadają pewnym rodzajom kolekcji. Zależnie od użytego zapisu w pamięci powstanie struktura danych reprezentująca odpowiedni rodzaj S-wyrażenia:

Notacja Literał S-wyrażenie Struktura Typ danych
(a…z) listowy listowe lista PersistentList
[a…z] wektorowy wektorowe wektor PersistentVector
{a b … x y} mapowy mapowe mapa PersistentArrayMap
PersistentHashMap
#{a…z} zbiorowy zbiorowe zbiór PersistentHashSet

Atomy

Omawiając symboliczne wyrażenia, wspomnieliśmy o ich specyficznej klasie zwanej atomami (ang. atoms). Atomem będzie taki element lispowej składni, który nie jest złożony (nie jest: listą, zbiorem, wektorem ani mapą). Wyjątki to pusta lista, pusty zbiór, pusty wektor i pusta mapa, które są zarówno wyrażeniami złożonymi, jak i atomami.

Do powyższej definicji należy jednak dodać jeszcze jeden istotny warunek, który decyduje o tym, że symboliczny zapis możemy uznać za lispowy atom. Przypomnijmy sobie nasz program:

(print "Witaj, Lispie!")
(print &#34;Witaj, Lispie!&#34;)

W poprzednich przykładach mogliśmy zauważyć, że printWitaj, Lispie! są atomami, ale czy byłyby nimi dowolne zestawy znaków, które nie są parą nawiasów z zawartością? Nie. Atom musi być poprawną konstrukcją składniową, na podstawie której czytnik będzie w stanie zdecydować, jaką wartość zwrócić. Istnieje tu więc pewien rygor. W tym konkretnym przypadku napis print zostanie zapamiętany w postaci symbolu, a Witaj, Lispie! w formie łańcucha znakowego, ponieważ spełniają składniowe warunki reprezentacji konkretnych struktur danych.

Owszem, reguły syntaktyczne są na tyle liberalne, że większość przypadkowo wpisanych słów czy nawet pojedynczych znaków zostanie uznanych za symbole (a więc atomy), jednak umieszczenie w symbolicznej etykiecie nawiasu czy rozpoczęcie jej cyfrą będą poważnymi nadużyciami i czytnik przestanie z nami współpracować.

Warto zaznaczyć, że pojęcie atomu jako klasy wyrażeń składniowych nie jest spopularyzowane w środowisku programistów języka Clojure. Wynika to prawdopodobnie z faktu, że w języku tym funkcjonuje referencyjny typ danych o nazwie Atom, który pomaga w przeprowadzaniu współbieżnych operacji na danych.

Listowe S-wyrażenia

Najczęściej spotykaną klasą S-wyrażeń są listowe S-wyrażenia (ang. list S-expressions). To właśnie dzięki nim programy pisane w dialektach języka Lisp składają się z dużej liczby nawiasów.

Listowe S-wyrażenie w Clojure powinno być listą elementów (innych S-wyrażeń), które oddzielone są znakami spacji, przecinka lub oboma tymi znakami. Początek i koniec listowego S-wyrażenia powinien być oznaczony otwierającym i zamykającym nawiasem okrągłym.

Podczas wartościowania niepustej listy jej pierwszy element określa sposób obsługi formy. Symbol na tej pozycji może nazywać formę specjalną, makro lub Var, którego wartość jest wywoływalna; każde inne wyrażenie operatora jest wartościowane i także musi dać IFn. W zwykłym wywołaniu funkcji pozostałe elementy są wartościowane od lewej do prawej i przekazywane jako argumenty.

Jeżeli nie podamy żadnych elementów, listowe S-wyrażenie spowoduje wygenerowanie pustej listy.

Przykłady listowych S-wyrażeń
 1;; wywołanie funkcji +
 2
 3(+ 1 2 3)
 4;=> 6
 5
 6;; wywołanie makra defn
 7;; służącego do definiowania funkcji nazwanej
 8
 9(defn powitaj [] "Witaj!")
10;=> #'user/powitaj
11
12;; wywołanie zdefiniowanej funkcji powitaj
13
14(powitaj)
15;=> "Witaj!"
16
17;; forma specjalna def
18;; służąca do definiowania zmiennej globalnej
19
20(def x 2)
21;=> #'user/x
22
23;; podwojenie wartości zmiennej globalnej
24;; identyfikowanej symbolem x
25
26(+ x x)
27;=> 4
28
29;; lista pusta
30
31()
32;=> ()
;; wywołanie funkcji + (+ 1 2 3) ;=&gt; 6 ;; wywołanie makra defn ;; służącego do definiowania funkcji nazwanej (defn powitaj [] &#34;Witaj!&#34;) ;=&gt; #&#39;user/powitaj ;; wywołanie zdefiniowanej funkcji powitaj (powitaj) ;=&gt; &#34;Witaj!&#34; ;; forma specjalna def ;; służąca do definiowania zmiennej globalnej (def x 2) ;=&gt; #&#39;user/x ;; podwojenie wartości zmiennej globalnej ;; identyfikowanej symbolem x (+ x x) ;=&gt; 4 ;; lista pusta () ;=&gt; ()

Starsze dialekty Lispu obsługiwały nieco odmienną postać listowych S-wyrażeń. W parze nawiasów nie umieszczano listy wszystkich elementów, ale tylko jedną jej komórkę z podziałem na lewą i prawą wartość, które oddzielano znakiem kropki, a koniec listy oznaczano symbolem nil, np.:

(+ . (1 . (2 . nil)))
(+ . (1 . (2 . nil)))

W Clojure ten rodzaj zapisu nie jest obsługiwany, chociaż istnieją struktury danych, które umożliwiają przeprowadzanie operacji na pojedynczych komórkach i tworzenie tzw. sekwencji.

Wektorowe S-wyrażenia

Literały wektorowe tworzą tzw. wektorowe S-wyrażenia, jednak w przeciwieństwie do wyrażeń listowych elementy umieszczane na ich pierwszych pozycjach nie mają specjalnego znaczenia. Efektem użycia wektorowego S-wyrażenia w podstawowej formie będzie struktura danych zwana wektorem, a każdy z elementów po uprzednim obliczeniu jego wartości stanie się jej składnikiem.

Z użyciem wektorowych S-wyrażeń możemy:

  • tworzyć wspomniane wyżej wektory i używać ich dla potrzeb logiki aplikacji,

  • określać listy argumentów definiowanych funkcji i makr,

  • wyrażać powiązania symboli z wartościami w odpowiednich konstrukcjach specjalnych (m.in. let czy binding),

  • dokonywać tzw. dekompozycji złożonych struktur o sekwencyjnym interfejsie dostępu.

Przykłady wektorowych S-wyrażeń
;; wektor literalny

[1 2 3 4]
;=> [1 2 3 4]

;; wektor literalny

[(+ 1 1) 2 3 4]
;=> [1 2 3 4]

;; wektor powiązań w formie let
;; wytwarza leksykalne powiązanie symbolu a z wartością 1

(let [a 1] a)
;=> 1

;; wektorowa forma powiązaniowa w formie let (dekompozycja)
;; wewnątrz wektora powiązań formy let
;; dokonuje powiązania każdego symbolu z S-wyrażenia [a b]
;; z wartością inicjującą z S-wyrażenia [1 2]
;; zgodnie z pozycją występowania

(let [[a b] [1 2]]
  (+ a b))
;=> 3

;; lista argumentów w definicji funkcji nazwanej

(defn dodaj
  [a b]
  (+ a b))
(dodaj 2 2)
;=> 4

;; wektor pusty

[]
;=> []
;; wektor literalny [1 2 3 4] ;=&gt; [1 2 3 4] ;; wektor literalny [(+ 1 1) 2 3 4] ;=&gt; [1 2 3 4] ;; wektor powiązań w formie let ;; wytwarza leksykalne powiązanie symbolu a z wartością 1 (let [a 1] a) ;=&gt; 1 ;; wektorowa forma powiązaniowa w formie let (dekompozycja) ;; wewnątrz wektora powiązań formy let ;; dokonuje powiązania każdego symbolu z S-wyrażenia [a b] ;; z wartością inicjującą z S-wyrażenia [1 2] ;; zgodnie z pozycją występowania (let [[a b] [1 2]] (+ a b)) ;=&gt; 3 ;; lista argumentów w definicji funkcji nazwanej (defn dodaj [a b] (+ a b)) (dodaj 2 2) ;=&gt; 4 ;; wektor pusty [] ;=&gt; []

Mapowe S-wyrażenia

Dzięki literałowi mapowemu możemy konstruować mapowe S-wyrażenia (ang. map S-expressions). W podstawowej formie pozwalają one wyrażać asocjacyjną strukturę danych zwaną mapą, która składa się ze zindeksowanych par typu klucz–wartość. Wartości powinny być oddzielone znakami spacji, znakami przecinka, albo obydwoma tymi znakami. Każdy pierwszy element w parze nazywamy kluczem (ang. key), a drugi wartością (ang. value).

Z użyciem mapowych S-wyrażeń możemy:

  • tworzyć wspomniane wyżej mapy i używać ich dla potrzeb logiki aplikacji,

  • określać listy argumentów nazwanych definiowanych funkcji i makr,

  • konstruować tzw. mapy metadanowe pozwalające wzbogacać niektóre konstrukcje w metadane, które mogą je opisywać bądź sterować ich właściwościami;

  • dokonywać dekompozycji złożonych struktur o asocjacyjnym interfejsie dostępu.

Przykłady mapowych S-wyrażeń
;; mapa literalna

{"a" 1 "b" 2}
;=> {"a" 1 "b" 2}

;; mapa wyrażająca argumenty nazwane
;; definiowanej funkcji
;; klucze a i b muszą być łańcuchami znakowymi

(defn dodaj [& {:strs [a b]}]
  (+ a b))

;; wywołanie funkcji z argumentami nazwanymi

(dodaj "b" 1, "a" 3)
;=> 4

;; mapa metadanowa definiowanej funkcji
;; z łańcuchem dokumentującym (klucz :doc)

(defn dodaj
  {:doc "Ta funkcja dodaje dwie liczby."}
  [a b]
  (+ a b))
;=> #'user/dodaj

;; wywołanie łańcucha dokumentującego dla dodaj
(doc dodaj)
;=>> -------------------------
;=>> user/dodaj
;=>> ([a b])
;=>>   Ta funkcja dodaje dwie liczby.
;=> nil

;; mapowa forma powiązaniowa w formie let (dekompozycja)
;; wewnątrz wektora powiązań formy let
;; dokonuje powiązania symboli a i b
;; z wartościami pochodzącymi z mapy o indeksach "a" i "b"

(let [{:strs [a b]} {"b" 1 "a" 3}]
  (+ a b))
;=> 4

;; mapa pusta

{}
;=> {}
;; mapa literalna {&#34;a&#34; 1 &#34;b&#34; 2} ;=&gt; {&#34;a&#34; 1 &#34;b&#34; 2} ;; mapa wyrażająca argumenty nazwane ;; definiowanej funkcji ;; klucze a i b muszą być łańcuchami znakowymi (defn dodaj [&amp; {:strs [a b]}] (+ a b)) ;; wywołanie funkcji z argumentami nazwanymi (dodaj &#34;b&#34; 1, &#34;a&#34; 3) ;=&gt; 4 ;; mapa metadanowa definiowanej funkcji ;; z łańcuchem dokumentującym (klucz :doc) (defn dodaj {:doc &#34;Ta funkcja dodaje dwie liczby.&#34;} [a b] (+ a b)) ;=&gt; #&#39;user/dodaj ;; wywołanie łańcucha dokumentującego dla dodaj (doc dodaj) ;=&gt;&gt; ------------------------- ;=&gt;&gt; user/dodaj ;=&gt;&gt; ([a b]) ;=&gt;&gt; Ta funkcja dodaje dwie liczby. ;=&gt; nil ;; mapowa forma powiązaniowa w formie let (dekompozycja) ;; wewnątrz wektora powiązań formy let ;; dokonuje powiązania symboli a i b ;; z wartościami pochodzącymi z mapy o indeksach &#34;a&#34; i &#34;b&#34; (let [{:strs [a b]} {&#34;b&#34; 1 &#34;a&#34; 3}] (+ a b)) ;=&gt; 4 ;; mapa pusta {} ;=&gt; {}

Zbiorowe S-wyrażenia

Literał zbiorowy umożliwia zapisywanie zbiorowych S-wyrażeń (ang. set S-expressions). Dzięki nim można w łatwy i przejrzysty sposób wyrażać zbiory, czyli struktury, w których każdy element występuje tylko raz.

Literał zbiorowy składa się z nawiasów klamrowych poprzedzonych znakiem kratki, wewnątrz których umieszczono niepowtarzalne w obrębie zbioru wartości. Elementy zbiorowego S-wyrażenia powinny być oddzielone znakiem spacji, przecinkiem lub obydwoma tymi znakami.

Jeżeli element wyrażenia zbiorowego nie jest wartością stałą, zostanie przeliczony zanim dojdzie do utworzenia obiektu reprezentującego zbiór.

Przykłady zbiorowych S-wyrażeń
#{1 2 3 4}
;=> #{1 2 3 4}

#{1 (+ 1 1) 3 4}
;=> #{1 2 3 4}

;; zbiór pusty

#{}
;=> #{}
#{1 2 3 4} ;=&gt; #{1 2 3 4} #{1 (+ 1 1) 3 4} ;=&gt; #{1 2 3 4} ;; zbiór pusty #{} ;=&gt; #{}

Formy wytwarzane przez czytnik

Spójrzmy, co czytnik zwróci po wczytaniu naszego programu:

(print "Witaj, Lispie!")
(print &#34;Witaj, Lispie!&#34;)

Otwierający nawias wprowadza listę. Czytnik umieszcza na jej pierwszej pozycji wartość Symbol o nazwie print, a na drugiej łańcuch Javy "Witaj, Lispie!". Wynikiem są zatem zwykłe dane Clojure: lista zawierająca symbol i łańcuch znakowy.

Konwersja tekstu do listy Clojure

Listy wytworzonej przez czytnik nie należy mylić z wewnętrznym abstrakcyjnym drzewem składniowym kompilatora. Kompilator analizuje listę w kontekście leksykalnym i przestrzeni nazw, po czym może reprezentować rozpoznane wywołanie za pomocą prywatnych danych implementacyjnych. Ta przeanalizowana reprezentacja nie jest tym, co zwraca read, ani wartością przekazywaną do makra.

Homoikoniczność

Clojure nazywamy homoikonicznym, ponieważ programy są reprezentowane przez te same rodzaje niemutowalnych struktur danych, na których rutynowo operują programy. Odpowiedniość jest strukturalna, a nie tekstowa: komentarze, białe znaki, przecinki i wiele zapisów makr czytnika nie jest zachowywanych. Przykładowo 'x(quote x) są wczytywane do równoważnych danych, choć mają odmienne sekwencje znaków.

Własność ta pozwala makrom składniowym przyjmować niewartościowane formy jako dane i zwracać formy zastępcze do dalszej analizy kompilatora. Nie wymaga udostępniania AST kompilatora ani modyfikowania już wykonywanego kodu maszynowego.

Semantyka

Kolejną warstwą jest wartościowanie (ang. evaluation): nadanie formie znaczenia w określonym kontekście leksykalnym i przestrzeni nazw oraz uzyskanie jej wartości. Clojure dla JVM nie ma osobnego interpretera tych form. Kompilator je analizuje, w razie potrzeby rozwija makra, emituje kod bajtowy i wykonuje ten kod; REPL jedynie sprawia, że cykl ten wydaje się natychmiastowy.

Przywołajmy raz jeszcze nasz przykład:

(print "Witaj, Lispie!")
(print &#34;Witaj, Lispie!&#34;)

Po wczytaniu kompilator otrzymuje jedną listę zawierającą symbol i łańcuch. Analizuje ją jako pojedynczą formę, aby uzyskać wartość najbardziej zewnętrznego wyrażenia.

W podanym przykładzie pierwszym elementem listy jest symbol print, który identyfikuje wbudowaną funkcję języka Clojure służącą do wyświetlania tekstu na ekranie. Badając powiązania leksykalne i odwzorowania bieżącej przestrzeni nazw, kompilator rozpozna symbol jako Var clojure.core/print i ustali, że ma do czynienia z wywołaniem funkcji. Podczas działania programu zostanie wywołana wartość funkcyjna tego Vara.

Pozostałym elementem jest argument funkcji. Operandy zwykłego wywołania funkcji są wartościowane od lewej do prawej. Łańcuch wartościuje się do siebie samego, więc do print zostanie przekazana wynikowa wartość java.lang.String.

Uruchomienie podprogramu funkcji print sprawi, że powstanie zamierzony efekt uboczny w postaci wyświetlenia na ekranie napisu:

Witaj, Lispie!

Dodatkowo funkcja zwróci wartość, która zależnie od sposobu uruchomienia programu będzie wyświetlona (w przypadku interaktywnej konsoli) lub pozostanie nieobsłużona (w innych przypadkach).

Formy

Forma (ang. form) jest wartością Clojure przedstawioną kompilatorowi do wartościowania. Listy są ważnymi formami, ale nie wszystkie formy są zagnieżdżonymi listami: wartościować można również symbole, literały, wektory, mapy i zbiory.

Znaczenie zależy zarówno od wartości, jak i kontekstu. Wartościowanie symbolu x(x 1) może dać funkcję, mapę, wektor, zbiór, klucz lub inny obiekt implementujący clojure.lang.IFn. Lista jest wtedy zwykłym wywołaniem, a kontrakt obiektu wywoływalnego określa znaczenie argumentu 1.

Zanim przejdziemy do omawiania głównych rodzajów form, nazwijmy te, które zostaną rozpoznane w naszym programie:

Formy w przykładowym programie
(print "Witaj, Lispie!")
(print &#34;Witaj, Lispie!&#34;)
  • (…) – forma listowa,
  • print – forma symbolowa,
  • "Witaj, Lispie!" – forma samowartościująca,
  • (print "Witaj, Lispie!") – forma wywołania funkcji:
    • po rozpoznaniu print jako Vara, którego wartość jest wywoływalna;
    • po zwartościowaniu argumentu łańcuchowego.

Formy samowartościujące

Łańcuchy, liczby, znaki, klucze, true, falsenil wartościują się do siebie samych. Tak samo zachowują się zwykle inne obiekty, którym nie przypisano specjalnych reguł wartościowania. Puste kolekcje są wartościami; niepuste formy wektorowe, mapowe i zbiorowe wartościują najpierw swoje elementy.

Przykłady form samowartościujących
"Witaj, Lispie!"  ; łańcuch tekstowy
\a                ; znak
()                ; lista pusta
123               ; liczba całkowita
:lalala           ; klucz
&#34;Witaj, Lispie!&#34; ; łańcuch tekstowy \a ; znak () ; lista pusta 123 ; liczba całkowita :lalala ; klucz

Forma specjalna quote podlega innej regule: zwraca swój argument jako dane bez wartościowania tego argumentu. Mechanizm cytowania zostanie dokładniej omówiony w dalszej części.

Formy symbolowe

Symbol będzie przez ewaluator potraktowany jak forma symbolowa (ang. symbol form), który spróbuje odnaleźć powiązaną z nim wartość. Zależnie od kontekstu nazwa symbolu może być rozpoznawana w różnych miejscach, np. w powiązaniach leksykalnych albo przestrzeniach nazw i ich zmiennych globalnych. Rozpoznanie symbolu daje skojarzoną wartość; nie przepisuje w miejscu struktury danych wytworzonej przez czytnik.

Przykłady form symbolowych
print
+
print +

Zobacz także:

Formy złożone

Wektory, mapy i zbiory są formami kolekcji. Wyrażenia ich elementów zostaną zwartościowane, a uzyskane wartości zebrane w wektorze, mapie bądź zbiorze. Lista podlega opisanym niżej regułom operatora, zamiast wartościować się jak zwykła kolekcja.

Przykłady form popularnych kolekcji
 [1 2 3]        ; forma wektorowa
#{1 2 3}        ; forma zbiorowa
 {"a" 1 "b" 2}  ; forma mapowa
'(1 2 3)        ; forma listowa (lista literalna)
[1 2 3] ; forma wektorowa #{1 2 3} ; forma zbiorowa {&#34;a&#34; 1 &#34;b&#34; 2} ; forma mapowa &#39;(1 2 3) ; forma listowa (lista literalna)

W przypadku niepustej listy lub sekwencji kompilator najpierw sprawdzi, czy operator nazywa formę specjalną albo makro. W przeciwnym razie operator i argumenty zostaną zwartościowane od lewej do prawej, a wartość operatora wywołana jako IFn.

Formy przeszukiwania

Wektory, zbiorymapy implementują IFn, mogą więc zajmować pozycję operatora. Wektor wyszukuje indeks, zbiór — element, a mapa — klucz. Wywołania mapy przyjmują również drugi argument: wartość zwracaną, gdy klucza nie znaleziono. Dopuszczalne liczby argumentów należą do kontraktu wywołania danego typu.

Przykłady form przeszukiwania kolekcji
([1 2 3]         0)  ;=> 1
(#{1 2 3}        2)  ;=> 2
({"a" 1 "b" 2} "b")  ;=> 2
([1 2 3] 0) ;=&gt; 1 (#{1 2 3} 2) ;=&gt; 2 ({&#34;a&#34; 1 &#34;b&#34; 2} &#34;b&#34;) ;=&gt; 2

Słowa kluczowe i symbole literalne również implementują IFn i mogą wyszukiwać siebie w kolekcji asocjacyjnej. Przyjmują kolekcję albo kolekcję wraz z wartością zwracaną, gdy elementu nie znaleziono.

Przykłady kluczowych i symbolowych form przeszukiwania
(:a {:a 1 :b 2})       ;=> 1
(:a #{:a :b :c})       ;=> :a
('a {'a 1 'b 2 :c 3})  ;=> 1
(:a {:a 1 :b 2}) ;=&gt; 1 (:a #{:a :b :c}) ;=&gt; :a (&#39;a {&#39;a 1 &#39;b 2 :c 3}) ;=&gt; 1
Formy wywołania funkcji

Jeżeli zwartościowany operator będzie obiektem funkcyjnym, obliczone wartości argumentów zostaną przekazane do jego metody invoke. Mechanizm współpracy z Javą korzysta z form wyrażeń hosta rozpoznawanych przez kompilator; same metody Javy nie są obiektami funkcyjnymi Clojure.

Przykłady form wywołania funkcji
(print "Witaj, Lispie!")  ;=> nil =>> Witaj, Lispie!
(+ 2 2)                   ;=> 4
(.toLowerCase "A")        ;=> a
(print &#34;Witaj, Lispie!&#34;) ;=&gt; nil =&gt;&gt; Witaj, Lispie! (+ 2 2) ;=&gt; 4 (.toLowerCase &#34;A&#34;) ;=&gt; a
Formy specjalne

Jeżeli pierwszy symbol nazywa formę specjalną (ang. special form), kompilator zastosuje właściwą jej gramatykę i reguły wartościowania. Formy specjalne są rozpoznawane bezpośrednio przez kompilator; nie są Varami zawierającymi funkcje.

Przykłady użycia form specjalnych
(def   x 1)                           ; definiowanie zmiennej globalnej
(fn    [x] (inc x))                   ; tworzenie funkcji
(def   f (fn [x] (inc x)))            ; definiowanie funkcji nazwanej
(let   [a 1] a)                       ; tworzenie powiązań leksykalnych
(quote (1 2 3))                       ; cytowanie
(. System (getProperty "user.home"))  ; dostęp do klas Javy
(def x 1) ; definiowanie zmiennej globalnej (fn [x] (inc x)) ; tworzenie funkcji (def f (fn [x] (inc x))) ; definiowanie funkcji nazwanej (let [a 1] a) ; tworzenie powiązań leksykalnych (quote (1 2 3)) ; cytowanie (. System (getProperty &#34;user.home&#34;)) ; dostęp do klas Javy

Formy specjalne są – jak sama nazwa mówi – formami, których reguły różnią się od reguł zwykłych wywołań funkcji. Jedne wprowadzają powiązania, inne sterują wartościowaniem, jeszcze inne współpracują z platformą hosta. Ich argumenty są przetwarzane dokładnie tak, jak określa to dana forma specjalna.

Dzięki formom specjalnym możemy na przykład definiować zmienne globalne czy funkcje, a także tworzyć powiązania symboli z wartościami w pewnych obszarach programu.

Zagnieżdżone wartościowanie

Wartościowanie podlega regule właściwej danej formie. Zwykłe wywołania wartościują operator i argumenty, formy kolekcji — swoje elementy, a formy specjalne i makra określają własne zachowanie. Wartość zwrócona przez jedną z tych operacji jest wynikiem, a nie automatycznie kolejną formą do wartościowania. W szczególności rozpoznanie symbolu powiązanego z listą zwraca tę listę jako wartość, zamiast ją wykonywać.

Niepoprawne formy

Jeżeli forma narusza właściwą jej regułę — na przykład operator nie implementuje IFn, symbolu nie można rozpoznać albo argumenty mają niezgodne typy — wartościowanie rzuci wyjątek. To, czy otaczający proces zakończy pracę, zależy od sposobu obsługi błędów; REPL zwykle zgłasza błąd i działa dalej.

Przykłady S-wyrażeń, które nie są formami
1(1 2 3)   ; liczba 1 nie jest wywoływalna
2(())      ; lista pusta nie jest wywoływalna
3(+ /)     ; + nie może dodać wartości funkcyjnej przechowywanej przez /
4(nie_ma)  ; brak konstrukcji identyfikowanej symbolem nie_ma
5nie_ma    ; brak konstrukcji identyfikowanej symbolem nie_ma
(1 2 3) ; liczba 1 nie jest wywoływalna (()) ; lista pusta nie jest wywoływalna (+ /) ; + nie może dodać wartości funkcyjnej przechowywanej przez / (nie_ma) ; brak konstrukcji identyfikowanej symbolem nie_ma nie_ma ; brak konstrukcji identyfikowanej symbolem nie_ma

Nazwy niektórych form są umowne, tzn. bazują na założeniu, że pewne S-wyrażenia dadzą się przeliczyć do danych konkretnych typów. Na przykład zapis:

(dodaj 1 2 3)
(dodaj 1 2 3)

nazwiemy umownie formą wywołania funkcji, chociaż w istocie nie mamy pewności, czy forma symbolowa dodaj będzie rozpoznana w przestrzeni nazw jako zmienna globalna, a znajdujący się tam obiekt referencyjny będzie zawierał odniesienie do funkcji.

Formy powiązaniowe

Forma powiązaniowa (ang. binding form) to konstrukcja, w której do symbolicznego identyfikatora przypisywana jest wartość, aby wytworzyć powiązanie. Na poziomie składniowym będziemy w tych formach mieli do czynienia z niezacytowanymi symbolami, jednak nie będą one traktowane jak formy symbolowe, których wartości należy poznać, lecz właśnie jako wyrażenia przypisujące pamięciowe obiekty do symbolicznych nazw.

Formę powiązaniową rozpoznamy w wyrażeniach reprezentujących pewne formy specjalne, gdzie będziemy mieli do czynienia z:

  • parą złożoną z niezacytowanego symbolu i innej formy (reprezentującej wartość, która po obliczeniu ma być przypisana);

  • niezacytowanego symbolu występującego samodzielnie, np. w wektorze parametrycznym funkcji, gdzie wartość będzie dynamicznie przypisana podczas przekazywania argumentów przy jej wywołaniu.

Formy powiązaniowe symboli są składnikiem niektórych form złożonychspecjalnych. Znajdziemy je na przykład:

Dodatkowo formy powiązaniowe mogą pojawić się również w tzw. powiązaniach strukturalnych, czyli abstrakcyjnych powiązaniach, w których dochodzi do dekompozycji złożonych struktur danych i przypisania symbolom wartości na wskazanych pozycjach lub identyfikowanych podanymi kluczami. Będziemy mieli wtedy do czynienia z powiązaniową formą wektora (ang. vector binding form) lub powiązaniową formą mapy (ang. map binding form), w której zamiast pojedynczego, niezacytowanego symbolu po lewej stronie pojawia się wektorowe bądź mapowe S-wyrażenie.

Niektóre inne konstrukcje korzystające z form powiązaniowych możemy również intuicyjnie określać mianem form powiązaniowych. Będą to takie wyrażenia, których celem jest przede wszystkim wykształcanie powiązań. Na przykład formę specjalną let nazwiemy też formą powiązaniową let (ang. let binding form), mimo że w istocie korzysta ona z potencjalnie wielu podstawowych form powiązaniowych zgrupowanych w wektorze powiązań.

Przykłady form powiązaniowych
(def x 5)     ; symbol x w formie powiązaniowej
(fn [x] nil)  ; symbol x formie powiązaniowej
(let [x 2])   ; symbol x w formie powiązaniowej

(defn nazwa   ; symbol nazwa w formie powiązaniowej (nazwa funkcji)
  [x]         ; symbol x w formie powiązaniowej (nazwa parametru)
  nil)
(def x 5) ; symbol x w formie powiązaniowej (fn [x] nil) ; symbol x formie powiązaniowej (let [x 2]) ; symbol x w formie powiązaniowej (defn nazwa ; symbol nazwa w formie powiązaniowej (nazwa funkcji) [x] ; symbol x w formie powiązaniowej (nazwa parametru) nil)

Parametry funkcji ustanawiają powiązania leksykalne podczas jej wywołania. Są czymś innym niż dynamiczne powiązania Varów tworzone przez binding.

Zobacz także:

Cytowanie

Ważną konstrukcją specjalną w Clojure i innych Lispach jest forma specjalna quote, która zwraca swój argument bez wartościowania tego argumentu. Czytnik nadal wytwarza kompletną wartość argumentu. Podczas analizy kompilatora quote nakazuje zwrócić tę wartość jako dane, zamiast rozpoznawać jej symbole lub wywoływać listy.

W dialektach Lispu użycie formy quote pozwala stwarzać literalne warianty struktur, które w postaciach niezacytowanych byłyby użyte do reprezentowania kodu źródłowego programu i/lub wartościowane.

Przykłady użycia formy quote
1(quote raz)        ; literalny symbol
2(quote (1 2 3))    ; literalna lista
3(quote [a b])      ; literalny wektor
4(quote {a 1 b 2})  ; literalna mapa
5(quote #{1 2 3})   ; literalny zbiór
(quote raz) ; literalny symbol (quote (1 2 3)) ; literalna lista (quote [a b]) ; literalny wektor (quote {a 1 b 2}) ; literalna mapa (quote #{1 2 3}) ; literalny zbiór

Powyższe można zapisać także posługując się lukrem składniowym:

1'raz
2'(1 2 3)
3'[a b]
4'{a 1 b 2}
5'#{a b}
&#39;raz &#39;(1 2 3) &#39;[a b] &#39;{a 1 b 2} &#39;#{a b}

Te same struktury danych moglibyśmy wytworzyć nie stosując cytowania, ale korzystając z odpowiednich, wbudowanych funkcji języka:

1(symbol "raz")                            ; symbol
2(list 1 2 3)                              ; lista
3(vector (symbol "a") (symbol "b"))        ; wektor
4(hash-map (symbol "a") 1 (symbol "b") 2)  ; mapa
5(hash-set 1 2 3)                          ; zbiór
(symbol &#34;raz&#34;) ; symbol (list 1 2 3) ; lista (vector (symbol &#34;a&#34;) (symbol &#34;b&#34;)) ; wektor (hash-map (symbol &#34;a&#34;) 1 (symbol &#34;b&#34;) 2) ; mapa (hash-set 1 2 3) ; zbiór

Działanie obejmuje całą cytowaną wartość: zagnieżdżone symbole i listy również pozostają danymi.

Przykłady rekurencyjnego cytowania
1(quote (a b c))        ; lista z literalnymi symbolami
2(quote (+ 2 (* 2 3)))  ; lista z literalnymi symbolami i liczbami
3(quote [raz 2 3])      ; wektor z literalnym symbolem i liczbami
4(quote {a 1 b 2})      ; mapa z literalnymi symbolami i liczbami
5(quote #{a b c})       ; zbiór z literalnymi symbolami
(quote (a b c)) ; lista z literalnymi symbolami (quote (+ 2 (* 2 3))) ; lista z literalnymi symbolami i liczbami (quote [raz 2 3]) ; wektor z literalnym symbolem i liczbami (quote {a 1 b 2}) ; mapa z literalnymi symbolami i liczbami (quote #{a b c}) ; zbiór z literalnymi symbolami

Terminy literalny wektor, literalna mapaliteralny zbiór opisują zapis czytelnika, a nie gwarantują sposobu wartościowania każdej zagnieżdżonej formy. Gdy niezacytowana forma kolekcji jest wartościowana, wartościowane są jej elementy. Cytowanie kolekcji zewnętrznej zapobiega temu, ponieważ cały argument quote zostaje zwrócony jako dane.

Wektory literalne i formy wektorowe
[1 2 3]         ; literalny wektor; jego elementy wartościują się do siebie
[1 2 (inc 2)]   ; literalny wektor wartościowany do [1 2 3]
'[1 2 (inc 2)]  ; quote zwraca wektor zawierający listę (inc 2)
'[1 2]          ; cytowanie nie daje tutaj obserwowalnej różnicy
[1 2 3] ; literalny wektor; jego elementy wartościują się do siebie [1 2 (inc 2)] ; literalny wektor wartościowany do [1 2 3] &#39;[1 2 (inc 2)] ; quote zwraca wektor zawierający listę (inc 2) &#39;[1 2] ; cytowanie nie daje tutaj obserwowalnej różnicy

Spróbujmy jeszcze zastosować cytowanie w odniesieniu do naszego szablonowego programu:

'(print "Witaj, Lispie!")
&#39;(print &#34;Witaj, Lispie!&#34;)

Efekt wartościowania powyższego wyrażenia to wartość, którą jest lista zawierająca symbol i łańcuch znakowy, czyli nasz pierwotny program, dający wyrazić się tekstem:

(print "Witaj, Lispie!")

Cytowanie wartości samowartościującej, takiej jak łańcuch, daje tę samą wartość. Skutek cytowania jest najlepiej widoczny dla symboli i list, których zwykłe reguły wartościowania są inne.

Identyfikatory

Identyfikatory (ang. identifiers) to konstrukcje, które pozwalają nazywać tożsamości (np. pojedyncze wartości czy złożone struktury danych umieszczane w pamięci), aby można się było do nich potem odwoływać.

W Clojure istnieją konstrukcje identyfikacyjne (omawiane niżej symbole), które mają specjalne znaczenie składniowe i nazywane przez nie obiekty są rozpoznawane automatycznie. Znajdziemy też takie (np. klucze), które posłużą nam do identyfikacji danych użytkowych, ale nie ma obowiązku ich używać.

Symbole

Symbol (ang. symbol) w Clojure to typ danych (ang. data type) używany w formach jako nazwa, a w aplikacjach — jako dane symboliczne. Czytnik wytwarza jedynie wartości typu Symbol; kompilator i konstrukcje powiązaniowe nadają im zależne od kontekstu role nazw lokalnych, Varów, klas lub form specjalnych.

W Clojure formy symbolowe można wyrażać w tekście programu bez stosowania żadnych dodatkowych oznaczeń. Gdy symbol jest wartościowany jako forma, zostaje rozpoznany w bieżącym kontekście leksykalnym, przestrzeni nazw i hosta; nie jest samowartościujący.

Zapis symboli dopuszcza litery, cyfry i kilka znaków interpunkcyjnych, ze specjalnymi regułami dotyczącymi początkowych cyfr, /, ., : oraz zastrzeżonego symbolu &. Ponieważ ścisła gramatyka jest bogatsza niż krótka lista znaków, przy nietypowych nazwach bezpieczniej oprzeć się na oficjalnej dokumentacji czytnika.

Przykłady zastosowania symboli
1funk                ; funk to forma symbolowa
2(funk 1 2 3)        ; funk to forma symbolowa w formie wywołania funkcji
3(fn [x y] x)        ; symbole x oraz y są parametrami anonimowej funkcji
4                    ; [x y] zawiera dwie formy powiązaniowe symboli
5(.toLowerCase "A")  ; toLowerCase jest metodą klasy Javy
6
7; ChunkedSeq to klasa wewnętrzna klasy PersistentVector
8(new clojure.lang.PersistentVector$ChunkedSeq [1 2 3 4 5] 0 3)
funk ; funk to forma symbolowa (funk 1 2 3) ; funk to forma symbolowa w formie wywołania funkcji (fn [x y] x) ; symbole x oraz y są parametrami anonimowej funkcji ; [x y] zawiera dwie formy powiązaniowe symboli (.toLowerCase &#34;A&#34;) ; toLowerCase jest metodą klasy Javy ; ChunkedSeq to klasa wewnętrzna klasy PersistentVector (new clojure.lang.PersistentVector$ChunkedSeq [1 2 3 4 5] 0 3)

W wielu Lispach symbol jest typem referencyjnym, to znaczy samodzielnie identyfikuje inny obiekt, przechowując odwołanie do jego pamięciowej struktury. W przypadku Clojure jest inaczej – symbol nie zawiera żadnego odwołania, a to, że używając symbolowych form można wywoływać funkcje bądź odnosić się do stałych wartości zawdzięczamy odpowiedniemu traktowaniu przez ewaluator i przeszukiwaniu dodatkowych struktur (np. przestrzeni nazw czy obszaru powiązań leksykalnych).

Kropki mogą występować w nazwach przestrzeni i kwalifikowanych nazwach klas Javy. Kropka na początku albo końcu symbolu jest zastrzeżona dla składni współpracy Clojure z Javą.

Symbole z przestrzeniami

Symbol może opcjonalnie zawierać dodatkową nazwę określającą tzw. przestrzeń nazw, czyli specjalny zbiór, który służy do grupowania identyfikatorów w celu eliminowania konfliktów. Więcej szczegółów o korzystaniu z tego mechanizmu sterowania widocznością można znaleźć w dalszej części tego odcinka.

Nazwa przestrzeni jest łańcuchem znakowym podlegającym takiemu samemu rygorowi syntaktycznemu, jak nazwa symbolu, a wyrażamy ją przez umieszczenie przed nazwą symbolu i oddzielenie znakiem ukośnika (/), na przykład:

1my/funk        ; symbol funk z przestrzenią my
2(my/funk 1 2)  ; symbol funk z przestrzenią my w formie wywołania funkcji
my/funk ; symbol funk z przestrzenią my (my/funk 1 2) ; symbol funk z przestrzenią my w formie wywołania funkcji

W obiekcie symbolu nie znajdziemy innego niż tekstowe odwołania do przestrzeni. Tu również nie dochodzi do przechowywania referencji. Możemy podać nieistniejącą przestrzeń i nie będzie to błąd, dopóki ewaluator nie zacznie wartościować zapisu.

Symbole literalne

Symbole znajdziemy w formach wytworzonych przez czytnik, gdzie analiza kompilatora może traktować je jako identyfikatory. Możemy też korzystać z cytowanych symboli jak ze zwykłych wartości w logice aplikacji. Będą wtedy na przykład pełniły funkcję prostych typów wyliczeniowych (ang. enumerated types), reprezentując stałe wartości wchodzące w skład ustalonych zbiorów:

1(list (symbol "trochę") (symbol "bardzo") (symbol "najbardziej"))
2(list 'trochę 'bardzo 'najbardziej)
3'(trochę bardzo najbardziej)
(list (symbol &#34;trochę&#34;) (symbol &#34;bardzo&#34;) (symbol &#34;najbardziej&#34;)) (list &#39;trochę &#39;bardzo &#39;najbardziej) &#39;(trochę bardzo najbardziej)

Powyższe trzy zapisy są równoważne. Pierwszy konstruuje literalną listę, której elementami są symbole tworzone na bazie łańcuchów znakowych z użyciem wbudowanej funkcji symbol; drugi również spowoduje powstanie listy, ale do wyrażania literalnych symboli korzystamy z cytowania; trzeci natomiast czyni użytek z rekurencyjnego cytowania całego listowego S-wyrażenia.

Cytowane symbole mogą też zawierać określenie przestrzeni nazw:

1(symbol "nazwa" "przestrzeń")
2'nazwa/przestrzeń
(symbol &#34;nazwa&#34; &#34;przestrzeń&#34;) &#39;nazwa/przestrzeń

Czasami w Lispach używa się literalnych symboli jako kluczy indeksujących w strukturach asocjacyjnych (np. mapach):

{ 'trochę 21, 'bardzo 108, 'najbardziej 11 }
{ &#39;trochę 21, &#39;bardzo 108, &#39;najbardziej 11 }

Symbole nie muszą być internalizowane, ale równe symbole mają spójną semantykę równości i haszowania, są więc pełnoprawnymi kluczami map. Słowa kluczowe są bardziej idiomatyczne w wielu schematach danych aplikacyjnych, ponieważ wartościują się do siebie i zwyczajowo pełnią rolę kluczy asocjacyjnych — nie dlatego, że wyszukiwanie symboli byłoby semantycznie niebezpieczne.

Zobacz także:

Klucze

Słowo kluczowe (ang. keyword), zwane potocznie kluczem (ang. key) to typ danych, który – podobnie jak symbole – służy do identyfikowania innych obiektów, jednak w Clojure nie ma specjalnego znaczenia składniowego i klucze nie identyfikują automatycznie innych konstruktów programu. Klucze wyrażane są obiektami typu clojure.lang.Keyword.

Implementacja Clojure dla JVM internalizuje słowa kluczowe. Ich publicznym kontraktem pozostaje jednak tożsamość wartości i równość; kod aplikacji nie musi zależeć od konkretnej strategii alokacji.

Słowa kluczowe sprawdzają się w roli prostych typów wyliczeniowych lub indeksów w asocjacyjnych strukturach danych (np. mapach). Jeżeli chodzi o wbudowane mechanizmy języka Clojure, ze słowami kluczowymi spotkamy się m.in. w niektórych makrach i konstrukcjach wyrażających powiązania argumentów nazwanych funkcji.

Ze składniowego punktu widzeni każdy klucz ma nazwę, która musi być łańcuchem znakowym rozpoczynającym się dwukropkiem (:) i mogącym zawierać znaki alfanumeryczne oraz: *, +, !, -, _?. W praktyce możemy korzystać z nieco bogatszego zestawu znaków (np. coraz częstsze jest wykorzystywanie kropki dla oznaczenia wewnętrznych hierarchii w niektórych bibliotekach), jednak może to ulec zmianie w przyszłości, dlatego warto używać nazw kluczy zgodnych z dokumentacją języka.

Klucze są również funkcjami. Gdy umieścimy słowo kluczowe na pierwszym miejscu listowego S-wyrażenia, wywołany zostanie podprogram, który spróbuje odszukać indeks w asocjacyjnej lub zbiorowej kolekcji podanej jako argument.

Podobnie jak symbole, klucze mogą opcjonalnie zawierać określenie przestrzeni nazw.

Przykłady tworzenia kluczy
:klucz                              ; klucz
::klucz                             ; klucz z bieżącą przestrzenią nazw
:przestrzeń/klucz                   ; klucz z przestrzenią
::klucz                             ; klucz w bieżącej przestrzeni nazw
(keyword "klucz")                   ; klucz z łańcucha znakowego
(keyword "przestrzeń" "klucz")      ; jw. lecz z określeniem przestrzeni
{:a 1 :b 2}                         ; mapa z indeksami w postaci kluczy
(defn x [& {:keys [kolor]}] kolor)  ; funkcja z argumentem nazwanym, kluczowym
(x :kolor 123)                      ; wywołanie funkcji z argumentem kluczowym
(:a {:a 1 :b 2})                    ; klucz jako funkcja przeszukująca mapę
(:a #{:a :b :c :d})                 ; klucz jako funkcja przeszukująca zbiór
:klucz ; klucz ::klucz ; klucz z bieżącą przestrzenią nazw :przestrzeń/klucz ; klucz z przestrzenią ::klucz ; klucz w bieżącej przestrzeni nazw (keyword &#34;klucz&#34;) ; klucz z łańcucha znakowego (keyword &#34;przestrzeń&#34; &#34;klucz&#34;) ; jw. lecz z określeniem przestrzeni {:a 1 :b 2} ; mapa z indeksami w postaci kluczy (defn x [&amp; {:keys [kolor]}] kolor) ; funkcja z argumentem nazwanym, kluczowym (x :kolor 123) ; wywołanie funkcji z argumentem kluczowym (:a {:a 1 :b 2}) ; klucz jako funkcja przeszukująca mapę (:a #{:a :b :c :d}) ; klucz jako funkcja przeszukująca zbiór

Zobacz także:

Przestrzenie nazw

W języku Clojure korzysta się z konstruktu zwanego przestrzenią nazw (ang. namespace). W technologii informacyjnej terminem tym określa się mechanizm sterowania widocznością identyfikatorów, który pozwala na ich hierarchiczne grupowanie w celu unikania konfliktów. Popularnymi przykładami przestrzeni nazw mogą być struktury katalogowe systemów plikowych, rekordy DNS czy publiczne adresy IP.

Przestrzenie nazw w programowaniu komputerów pomagają oddzielać od siebie zbiory identyfikatorów używane w różnych komponentach (np. bibliotekach programistycznych) lub kontekstach, dzięki czemu udostępniane nazwy (np. modułów, klas, zmiennych czy funkcji) są unikatowe. Możemy wtedy korzystać w programie z kilku bibliotek, w których zdefiniowano tak samo nazwaną funkcję. Podczas odwoływania się do niej wymagane będzie użycie tzw. nazwy w pełni kwalifikowanej (ang. fully qualified name), zwanej też nazwą jednoznaczną, czyli identyfikatora wzbogaconego o określenie nazwy przestrzeni.

W Clojure przestrzeń nazw jest globalnie zarejestrowanym obiektem typu clojure.lang.Namespace. Utrzymuje odwzorowania, których kluczami są symbole, a wartościami Vary albo zaimportowane klasy Javy. Można ją oglądać jak tablicę symboli, lecz nie jest zwykłą wartością mapy trwałej.

Ponieważ umieszczane w programie symbole mogą opcjonalnie zawierać określenie przestrzeni nazw, więc możliwe jest z ich użyciem odwoływanie się do identyfikatorów z różnych przestrzeni. Jeżeli symbol nie zawiera nazwy jednoznacznej, wtedy podczas jego wartościowania przyjmuje się, że identyfikuje on konstrukcję zdefiniowaną w aktualnej przestrzeni, wskazywanej przez dynamiczną zmienną globalną clojure.core/*ns*:

;; symbol set/select kwalifikowany aliasem przestrzeni clojure.set
;; identyfikuje funkcję

(require '[clojure.set :as set])
(set/select odd? #{1 2 3 4})
;=> #{1 3}

;; symbol + z przestrzeni clojure.core
;; zaimportowanej do bieżącej (user)
;; identyfikuje funkcję

(+ 2 2)
;=> 4
;; symbol set/select kwalifikowany aliasem przestrzeni clojure.set ;; identyfikuje funkcję (require &#39;[clojure.set :as set]) (set/select odd? #{1 2 3 4}) ;=&gt; #{1 3} ;; symbol + z przestrzeni clojure.core ;; zaimportowanej do bieżącej (user) ;; identyfikuje funkcję (+ 2 2) ;=&gt; 4

Pierwsze wywołanie korzysta z symbolu set/select. Jego część przestrzeni nazw jest lokalnym aliasem set, który analiza kompilatora rozpoznaje jako clojure.set; odwzorowanie symbolu select w tej przestrzeni prowadzi do Vara zawierającego funkcję. Argumenty odd?#{1 2 3 4} zostaną następnie zwartościowane i przekazane tej funkcji.

Drugi symbol nie ma części przestrzeni nazw. W przestrzeni user symbol + jest rozpoznawany przez odniesienie do Vara clojure.core/+. Odniesienie zachowuje tożsamość tego samego Vara; nie kopiuje wartości funkcji do user.

Zobacz także:

Obsługa globalnych stanów

Opowiadając o Clojure, podkreśla się, że nie ma tam konwencjonalnych zmiennych, a struktury danych są niemutowalne. Dlaczego więc raz po raz przewija się określenie „zmienna globalna”?

Wiele aplikacji musi obsługiwać zmieniający się stan, którego część bywa współdzielona albo globalnie osiągalna pod stałymi nazwami. Przykładami mogą być: kondycja postaci w grze, układ okien interfejsu użytkownika bądź aktualnie przetwarzana zawartość wczytywanego pliku.

Do reprezentowania opisanych wyżej danych można by użyć konwencjonalnych zmiennych, czyli ustalonych przestrzeni pamięciowych o określonych nazwach, których zawartości są modyfikowane przez odpowiednie podprogramy (np. zmieniający punktację, reagujący na działania użytkownika odnośnie elementów interfejsu czy odczytujący dane z bazy). Jednak w takim modelu nie będziemy mogli posłużyć się wieloma wątkami bez stosowania dodatkowych mechanizmów izolacji. Pamięciowa szufladka zmiennej, reprezentująca ważny parametr, może zostać zreorganizowana przez jeden wątek, podczas gdy drugi będzie jeszcze przeprowadzał z jej użyciem ważną operację. Oznacza to więcej pracy dla programisty, który zamiast skupiać się na logice biznesowej aplikacji, musi pamiętać o zabezpieczaniu programu przed nim samym i wprowadzać semafory, blokady itp.

Clojure oddziela niemutowalne wartości od tożsamości, które mogą z czasem otrzymywać nowe wartości. Lokalne powiązania leksykalne są niemutowalne, natomiast Vary, Atomy, Refy i Agenty oferują różne kontrakty typów referencyjnych dla koordynowanej zmiany. Nie oznacza to zakazu mutacji wewnątrz JVM, lecz projekt, w którym dozwolone miejsca i reguły aktualizacji są jawne.

Aktualizacja referencji zmienia jej logiczną wartość bieżącą zgodnie z regułami koordynacji danego typu. Nowa wartość bywa trwałą kolekcją współdzielącą strukturę ze starą, ale ani odrębny adres pamięci, ani zachowana historia nie należą do ogólnego kontraktu referencji.

Typ Var

Var jest nazwaną referencją używaną przez globalne środowisko Clojure. Może mieć powiązanie główne, współdzielone przez wątki bez powiązania lokalnego. Vary oznaczone ^:dynamic można przepowiązywać za pomocą binding w bieżącym wątku i dynamicznie zagnieżdżonych wywołaniach; zwykłe Vary nie uzyskują izolacji poszczególnych wątków tylko dlatego, że są Varami.

Zwyczajowo obiekty typu Var są w Clojure internalizowane w przestrzeniach nazw, tzn. nie istnieje powszechnie używana forma specjalna czy funkcja, która pozwalałaby tworzyć je bez powiązania z jakimiś symbolicznymi identyfikatorami. Jest jednak makro with-local-vars, które tworzy obiekty Var w zasięgu leksykalnym – muszą być one wtedy identyfikowane podanymi symbolami widocznymi w wyrażeniu podanym jako argument. Wskazywane w ten sposób wartości możemy odczytywać z użyciem funkcji deref lub literału dereferencyjnego (umieszczanego przed symbolem znaku małpki):

(with-local-vars [a 5] @a)
;=> 5
(with-local-vars [a 5] @a) ;=&gt; 5

Zobacz także:

Zmienne globalne

Obiekty typu Var (a dokładniej clojure.lang.Var) są wraz z symbolami wykorzystywane w Clojure do tworzenia zmiennych globalnych (ang. global variables). Zmienne globalne służą do identyfikowania rzadko zmieniających się tożsamości, np. wartości konfiguracyjnych czy zdefiniowanych w programie funkcji.

Działa to tak, że przestrzeń nazw odwzorowuje symboliczną nazwę na konkretny Var. Var udostępnia z kolei swoje bieżące powiązanie wątku, jeżeli takie istnieje, a w przeciwnym razie — powiązanie główne. Odwzorowanie przestrzeni nazw ma więc postać symbol → Var, a nie symbol → bieżąca wartość główna.

Spójrzmy na diagram ilustrujący sposób uzyskiwania obiektu funkcyjnego na podstawie jego nazwy w naszym przykładowym programie:

Rozpoznawanie symbolicznej nazwy funkcji

Analiza kompilatora i wykonanie przeprowadziły tu trzy pojęciowo odrębne kroki, aby dotrzeć do wywoływanej funkcji:

  1. Symbol print został rozpoznany przez odniesienie bieżącej przestrzeni nazw do Vara clojure.core/print.

  2. Podczas działania programu pobrane zostało bieżące powiązanie Vara — powiązanie wątku, gdy istnieje, w przeciwnym razie powiązanie główne.

  3. Powiązanie zawierało obiekt funkcyjny, który został wywołany ze zwartościowanymi argumentami.

Tworzenie zmiennych globalnych możliwe jest z użyciem formy specjalnej def:

 1(def x 8)                ; x wskazuje na wartość 8
 2(def y [1 2 3])          ; y wskazuje na wektor [1 2 3]
 3(def pisz print)         ; pisz wskazuje na wartość bieżącą zmiennej print
 4(declare potem)          ; tworzy zmienną globalną bez powiązania z wartością
 5
 6(pisz x)                 ;=> 8
 7(pisz y)                 ;=> [1 2 3]
 8(pisz "Witaj, Lispie!")  ;=> Witaj, Lispie!
 9
10potem
11;=> #object[clojure.lang.Var$Unbound 0x53245a5b "Unbound: #'user/potem"]
(def x 8) ; x wskazuje na wartość 8 (def y [1 2 3]) ; y wskazuje na wektor [1 2 3] (def pisz print) ; pisz wskazuje na wartość bieżącą zmiennej print (declare potem) ; tworzy zmienną globalną bez powiązania z wartością (pisz x) ;=&gt; 8 (pisz y) ;=&gt; [1 2 3] (pisz &#34;Witaj, Lispie!&#34;) ;=&gt; Witaj, Lispie! potem ;=&gt; #object[clojure.lang.Var$Unbound 0x53245a5b &#34;Unbound: #&#39;user/potem&#34;]

Zobacz także:

Powiązania

Powiązanie (ang. binding) to w ujęciu ogólnym skojarzenie identyfikatoraidentyfikowanym obiektem. Używając tego terminu, unikamy nieporozumień związanych z subtelnymi, ale istotnymi z punktu widzenia programisty różnicami w odwoływaniu się do obiektów z użyciem nazw.

W językach zakorzenionych imperatywnie często korzystamy z pojęcia „zmienna” i siłą nawyku oczekujemy, że będzie ona miała jakąś nazwę. Termin ten pozwala nam wskazać pamięciową szufladkę, w której znajdziemy wartość. W Clojure takie podejście mogłoby wprowadzać w błąd, ponieważ możemy stwarzać zmienne pozbawione nazw (obiekty referencyjne), a także nazywać wartości, które wcale zmiennymi nie są.

W języku Clojure mamy do czynienia z kilkoma rodzajami powiązań:

Ostatnia pozycja wymaga wyjaśnienia, ponieważ – jak łatwo zauważyć – nie zawiera wzmianki o żadnym symbolu, a wielokrotnie przecież wspominaliśmy, że to właśnie symbole służą do identyfikowania innych obiektów.

W tym miejscu warto sobie przypomnieć, że w Clojure symbole samodzielnie nie przechowują odwołań do wartości – tę funkcję pełnią obiekty referencyjne (np. typu Var w przypadku zmiennych globalnych). Wynika to z przyjętego modelu zarządzania zmiennym stanem.

Przykłady powiązań
;; zmienna globalna
;; wartość początkowa wskazywana przez x to 1

(def x 1)
;=> #'user/x

;; wektor parametryczny definicji funkcji
;; dwa pierwsze argumenty staną się parametrami a i b

(fn [a b] (+ a b))
;=> #<Fn@5af25442 user/eval14406[fn]>

;; powiązanie leksykalne
;; symbol a identyfikuje wartość 1

(let [a 1]
  a)
;=> 1

;; dekompozycja pozycyjna w let
;; symbol a powiązany z wartością 1 przez pozycję
;; symbol b powiązany z wartością 2 przez pozycję

(let [[a b] '(1 2 8)]
  (+ a b))
;=> 3

;; dekompozycja asocjacyjna w let
;; symbol a powiązany z wartością 1 przez klucz :a
;; symbol b powiązany z wartością 2 przez klucz :b

(let [{a :a b :b} {:a 1 :b 2 :c 8}]
  (+ a b))
;=> 3

;; dekompozycja asocjacyjna w let (użycie :keys)
;; symbol a powiązany z wartością 1 przez klucz :a
;; symbol b powiązany z wartością 2 przez klucz :b

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

;; powiązanie obiektu referencyjnego typu Var z wartością

(with-local-vars [x 5] x)
;=> #'Var: --unnamed-->

;; powiązanie obiektu referencyjnego typu Atom z wartością

(atom 5)
;=> #<Atom@a1cb453 5>
;; zmienna globalna ;; wartość początkowa wskazywana przez x to 1 (def x 1) ;=&gt; #&#39;user/x ;; wektor parametryczny definicji funkcji ;; dwa pierwsze argumenty staną się parametrami a i b (fn [a b] (+ a b)) ;=&gt; #&lt;Fn@5af25442 user/eval14406[fn]&gt; ;; powiązanie leksykalne ;; symbol a identyfikuje wartość 1 (let [a 1] a) ;=&gt; 1 ;; dekompozycja pozycyjna w let ;; symbol a powiązany z wartością 1 przez pozycję ;; symbol b powiązany z wartością 2 przez pozycję (let [[a b] &#39;(1 2 8)] (+ a b)) ;=&gt; 3 ;; dekompozycja asocjacyjna w let ;; symbol a powiązany z wartością 1 przez klucz :a ;; symbol b powiązany z wartością 2 przez klucz :b (let [{a :a b :b} {:a 1 :b 2 :c 8}] (+ a b)) ;=&gt; 3 ;; dekompozycja asocjacyjna w let (użycie :keys) ;; symbol a powiązany z wartością 1 przez klucz :a ;; symbol b powiązany z wartością 2 przez klucz :b (let [{:keys [:a :b]} {:a 1 :b 2 :c 8}] (+ a b)) ;=&gt; 3 ;; powiązanie obiektu referencyjnego typu Var z wartością (with-local-vars [x 5] x) ;=&gt; #&#39;Var: --unnamed--&gt; ;; powiązanie obiektu referencyjnego typu Atom z wartością (atom 5) ;=&gt; #&lt;Atom@a1cb453 5&gt;

Zobacz także:

Kolekcje

Kolekcja (ang. collection) to struktura danych, która pozwala przechowywać pewną liczbę elementów.

Listy

W Lispie najczęściej używaną strukturą danych jest lista, która – jak mieliśmy okazję zauważyć – służy zarówno do składniowej organizacji kodu programu (listowe S-wyrażenia), jak i do przechowywania uporządkowanej kolekcji elementów dla potrzeb logiki aplikacji. Warto wyobrażać sobie listę jako sposób aranżacji danych, a nie tylko symboliczny zapis z nawiasami.

Istnieje kilka rodzajów list. Najczęściej używane w dialektach Lispu to tzw. listy połączone (ang. linked lists), a dokładniej listy jednokierunkowe (ang. singly linked lists). Cechuje je możliwość elastycznego łączenia ze sobą elementów i szybkiego dodawania nowych do ich początków. Właśnie dlatego dobrze spełniają swe zadanie jako pamięciowe reprezentacje struktur lispowych programów.

Listy we wczesnych Lispach

Historycznie rzecz ujmując, każdy węzeł listy (w Lispie nazywany komórką cons, ang. cons cell) ma dwa sloty: pierwszy wskazuje na wartość bieżącego elementu, a drugi na kolejny element listy (kolejną komórkę cons). Tak naprawdę są to po prostu dwa wskaźniki, które mogą odnosić się do dowolnych wartości.

Dostęp do pierwszego slotu każdego z elementów nazywamy car (z ang. Contents of the Address part of Register number), a do drugiego cdr (z ang. Contents of the Decrement part of Register number). Etymologia tych dziwnych nazw pochodzi z czasów, gdy implementowano Lispa na komputerze IBM 704 (lata pięćdziesiąte). Maszyna ta miała specjalną instrukcję, która dzieliła 36-bitowe słowo maszynowe na 4 części – carcdr to skrócone etykiety dwóch pierwszych, a w Lispie znalazły się dlatego, że autor używał ich do dzielenia zawartości wewnętrznej struktury reprezentującej komórkę listy. Żargonowo pierwszy slot komórki cons określa się więc skrótem CAR, a drugi CDR.

Nic nie stoi na przeszkodzie, aby lista zawierała w sobie inną listę:

Lispowa lista

W kodzie możemy wyrazić powyższą strukturę jako:

(raz dwa (1 2))
(raz dwa (1 2))

czyli:

1(raz   ; pierwszy element listy
2 dwa   ; drugi element listy
3 (     ; trzeci element listy – lista zagnieżdżona
4  1    ; pierwszy element zagnieżdżonej listy
5  2))  ; drugi element zagnieżdżonej listy
(raz ; pierwszy element listy dwa ; drugi element listy ( ; trzeci element listy – lista zagnieżdżona 1 ; pierwszy element zagnieżdżonej listy 2)) ; drugi element zagnieżdżonej listy

A tak wyglądałby zapis z użyciem tzw. notacji pełnej, która w Clojure nie jest używana. W każdej parze nawiasów znajdziemy dwa sloty oddzielone znakiem kropki:

(raz . (dwa . ((1 . (2 . nil)) . nil)))
(raz . (dwa . ((1 . (2 . nil)) . nil)))

Kropki oddzielają tu rejestry komórek CAR i CDR, a elementy nil oznaczają końce list. Jeżeli drugi slot (CDR) ma wskazywać na kolejny element listy, w zapisie ten drugi jest umieszczany po kropce i ujmowany w nawiasy.

Pamiętajmy jednak, że zapis (raz dwa (1 2)) jest poprawnym S-wyrażeniem, ale nie jest formą, chyba że zdefiniujemy funkcję nazwaną raz, a symbol dwa skojarzymy z jakąś wartością. Możemy też użyć cytowania i odebrać konstrukcji specjalne znaczenie, aby wyrażała formę stałą:

'(raz dwa (1 2))
&#39;(raz dwa (1 2))

W pierwszych wydaniach języka Lisp listy tworzyło się z użyciem funkcji cons (z ang. construct, pol. konstruować). Na przykład:

(cons 'raz (cons 'dwa (cons (cons 1 (cons 2 nil)) nil)))
(cons &#39;raz (cons &#39;dwa (cons (cons 1 (cons 2 nil)) nil)))

Co można przedstawić też jako:

1(cons 'raz
2      (cons 'dwa
3            (cons (cons 1
4                        (cons 2 nil))
5                  nil)))
(cons &#39;raz (cons &#39;dwa (cons (cons 1 (cons 2 nil)) nil)))

Takie budowanie list może i gimnastykuje umysł, jednak nie służy produktywnemu pisaniu programów. Obecnie niektóre z dialektów Lispu zrezygnowały z obsługi notacji pełnej, chociaż funkcji cons nadal się używa, ale raczej do przeprowadzania operacji na istniejących listach, niż do ich konstruowania od zera.

Listy w Clojure

W języku Clojure listy reprezentowane są obiektowym typem danych systemu gospodarza o nazwie clojure.lang.PersistentList. Obiekty tego typu znajdziemy w drzewie składniowym, gdzie odzwierciedlą listowe S-wyrażenia, a także w danych aplikacji, gdy użyto funkcji list lub wytworzono literalną listę przez zacytowanie jej symbolicznie wyrażonej postaci.

Przykłady tworzenia list w Clojure
1(list 1 2 3)     ; użycie funkcji list (argumenty będą wartościowane)
2(quote (1 2 3))  ; lista literalna (argumenty nie będą wartościowane)
3'(1 2 3)         ; lista literalna (argumenty nie będą wartościowane)
(list 1 2 3) ; użycie funkcji list (argumenty będą wartościowane) (quote (1 2 3)) ; lista literalna (argumenty nie będą wartościowane) &#39;(1 2 3) ; lista literalna (argumenty nie będą wartościowane)

Wewnętrznie obiekty typu PersistentListlistami dwukierunkowymi (ang. doubly linked lists), chociaż cecha ta jest ukrywana przed programistą. Poza tym w Clojure lista (jak większość struktur danych) jest niemutowalna, tzn. chcąc wprowadzić zmianę w jej strukturze nigdy nie modyfikuje się reprezentującego ją obiektu, lecz wytwarza nowy, który różni się od poprzedniego.

Zobacz także:

„Listy”, rozdział IX.

Sekwencje i obiekty Cons

Poza hermetycznymi listami w Clojure znajdziemy również obiekty typu Cons (a dokładniej clojure.lang.Cons). Są one najbliższym odpowiednikiem komórek cons znanych z innych dialektów języka Lisp. Dokładniej rzecz ujmując, z użyciem cons możemy konstruować tzw. sekwencje (ang. sequences), czyli abstrakcyjne kolekcje, które cechuje jednolity interfejs dostępu w postaci trzech podstawowych operacji:

  • odczyt wartości przechowywanej w komórce,
  • odczyt następnej komórki połączonej z bieżącą,
  • przyłączanie nowej komórki do istniejącej.

Odniesienia do wartości dowolnych typów możemy umieszczać w obiektach Cons i łączyć w sekwencje z użyciem funkcji cons. Przyjmuje ona dwa argumenty: wartość i obiekt, który również wyposażono w sekwencyjny interfejs (zaliczamy do nich m.in. listy, wektorymapy). Wartością zwracaną będzie komórka Cons, dla której następną będzie podana jako argument.

Jeżeli podanym istniejącym obiektem będzie nil, zwrócona zostanie jednoelementowa lista.

Przykład konstruowania sekwencji w Clojure
1(def pierwszy (cons 3 nil))       ; powstaje lista (3)
2(def drugi    (cons 2 pierwszy))  ; powstaje cons(2)-->(3)
3(def ostatni  (cons 1 drugi))     ; powstaje cons(1)-->cons(2)-->(3)
4
5(first ostatni)        ; => 1     ; wartość w ostatnio dodanej
6(first (rest ostatni)) ; => 2     ; wartość w kolejnej
(def pierwszy (cons 3 nil)) ; powstaje lista (3) (def drugi (cons 2 pierwszy)) ; powstaje cons(2)--&gt;(3) (def ostatni (cons 1 drugi)) ; powstaje cons(1)--&gt;cons(2)--&gt;(3) (first ostatni) ; =&gt; 1 ; wartość w ostatnio dodanej (first (rest ostatni)) ; =&gt; 2 ; wartość w kolejnej

Z użyciem Cons możemy zespolić nie tylko pojedyncze wartości, ale również kolekcje (np. zwykłe listy). Mamy wtedy do czynienia ze strukturą złożoną, którą można wyrazić na przykład tak:

1(cons (list 1 2 3)
2      (cons (list 4 5 6)
3            (cons 10
4                  ())))
5
6; => ((1 2 3) (4 5 6) 10)
(cons (list 1 2 3) (cons (list 4 5 6) (cons 10 ()))) ; =&gt; ((1 2 3) (4 5 6) 10)

Powyższe struktury bywają wykorzystywane do szybkiego łączenia już zgrupowanych wartości w większe zestawy, które dopiero podczas odczytywania są wypłaszczane, aby uzyskiwać indywidualne wartości.

Przykład wypłaszczania zagnieżdżonej sekwencji
1(flatten
2    (cons '(1 2 3) (cons '(4 5 6) (cons 10 nil))))
3
4; => (1 2 3 4 5 6 10)
(flatten (cons &#39;(1 2 3) (cons &#39;(4 5 6) (cons 10 nil)))) ; =&gt; (1 2 3 4 5 6 10)

Zobacz także:

„Komórki cons”, rozdział X.

Wektory

Wektor (ang. vector) to uporządkowana kolekcja wartości indeksowana z użyciem liczb całkowitych począwszy od 0 (pierwszy element). W kodzie programu do wyrażania wektorów używamy literału wektorowego, możemy też posłużyć się kilkoma wbudowanymi funkcjami.

Wektory są w Clojure reprezentowane obiektami typu clojure.lang.PersistentVector. Cechuje je bardzo szybkie wyszukiwanie danych i dodawanie nowych elementów do ich końców. Wektory są również funkcjami – przyjmują jeden argument – numer indeksu, którego wartość elementu będzie zwrócona.

Przykłady tworzenia wektorów
;; tworzenie wektora (funkcja)

(vector 1 2 3 4)
;=> [1 2 3 4]

;; tworzenie wektora z S-wyrażenia (literał)

[1 2 3 4]
;=> [1 2 3 4]

;; tworzenie wektora z innej kolekcji

(vec (list 1 2 3 4))
;=> [1 2 3 4]

;; przeszukiwanie wektora (element o indeksie 0)

([1 2 3 4] 0)
;=> 1
;; tworzenie wektora (funkcja) (vector 1 2 3 4) ;=&gt; [1 2 3 4] ;; tworzenie wektora z S-wyrażenia (literał) [1 2 3 4] ;=&gt; [1 2 3 4] ;; tworzenie wektora z innej kolekcji (vec (list 1 2 3 4)) ;=&gt; [1 2 3 4] ;; przeszukiwanie wektora (element o indeksie 0) ([1 2 3 4] 0) ;=&gt; 1

Zobacz także:

Mapy

Mapa (ang. map) to asocjacyjna kolekcja, która przechowuje odwzorowania kluczy na wartości. Klucze i wartości mogą być danymi dowolnego typu, jednak ten sam klucz musi być niepowtarzalny w obrębie danej mapy. Przeszukiwanie map w celu znalezienia wartości identyfikowanej kluczem jest bardzo szybkie, podobnie jak dodawanie nowych elementów.

W Clojure mapy reprezentowane są następującymi typami danych:

  • clojure.lang.PersistentHashMap (mapa bazująca na tablicach mieszających),
  • clojure.lang.PersistentArrayMap (mapa bazująca na zwykłych tablicach),
  • clojure.lang.PersistentTreeMap (mapa sortowana).

W kodzie programu do wyrażania map używamy literału mapowego w postaci nawiasów klamrowych zawierających pary wyrażeń. Możemy też skorzystać z funkcji hash-map, aby wyrazić pary kolejnymi argumentami wywołania.

Przykłady tworzenia map
;; tworzenie mapy (funkcja)

(hash-map :a 1 :b 2)
;=> {:a 1 :b 2}

;; tworzenie mapy sortowanej (funkcja)

(sorted-map :a 1 :b 2)
;=> {:a 1 :b 2}

;; tworzenie mapy z S-wyrażenia (literał)

{:a 1 :b 2}
;=> {:a 1 :b 2}

;; tworzenie mapy z S-wyrażenia (literał)

{:a 1 :b (+ 1 1)}
;=> {:a 1 :b 2}

;; tworzenie mapy z dwóch sekwencyjnych kolekcji

(zipmap [:a :b] [1 2])
;=> {:a 1 :b 2}

;; tworzenie mapy z sekwencji par

(into {} [[:a 1] [:b 2]])
;=> {:a 1 :b 2}

;; tworzenie mapy z sekwencji

(apply hash-map [:a 1 :b 2])
;=> {:a 1 :b 2}
;; tworzenie mapy (funkcja) (hash-map :a 1 :b 2) ;=&gt; {:a 1 :b 2} ;; tworzenie mapy sortowanej (funkcja) (sorted-map :a 1 :b 2) ;=&gt; {:a 1 :b 2} ;; tworzenie mapy z S-wyrażenia (literał) {:a 1 :b 2} ;=&gt; {:a 1 :b 2} ;; tworzenie mapy z S-wyrażenia (literał) {:a 1 :b (+ 1 1)} ;=&gt; {:a 1 :b 2} ;; tworzenie mapy z dwóch sekwencyjnych kolekcji (zipmap [:a :b] [1 2]) ;=&gt; {:a 1 :b 2} ;; tworzenie mapy z sekwencji par (into {} [[:a 1] [:b 2]]) ;=&gt; {:a 1 :b 2} ;; tworzenie mapy z sekwencji (apply hash-map [:a 1 :b 2]) ;=&gt; {:a 1 :b 2}

Zobacz także:

Zbiory

Zbiór (ang. set) jest strukturą danych, która pozwala przechowywać elementy o dowolnych wartościach z zastrzeżeniem, że dana wartość może pojawić się w zbiorze tylko raz. Zbiory cechuje bardzo szybkie dodawanie i wyszukiwanie elementów. W Clojure istnieją wbudowane funkcje przeznaczone do przeprowadzania operacji na zbiorach, np. złączenie, projekcja, suma itd.

Zbiory reprezentowane są obiektami następujących typów danych:

  • clojure.lang.PersistentSet (zbiór bazujący na tablicach mieszających),
  • clojure.lang.PersistentTreeSet (zbiór sortowany).
Przykłady tworzenia zbiorów
;; tworzenie zbioru (funkcja)

(hash-set :a :b :c)
;=> #{:a :b :c}

;; tworzenie zbioru sortowanego (funkcja)

(sorted-set :a :b :c)
;=> #{:a :b :c}

;; tworzenie zbioru z S-wyrażenia (literał)

#{:a :b :c}
;=> #{:a :b :c}

;; tworzenie zbioru z S-wyrażenia (literał)

#{:a :b (keyword "c")}
;=> #{:a :b :c}

;; tworzenie zbioru z sekwencji

(set [:a :b :c])
;=> #{:a :b :c}
;; tworzenie zbioru (funkcja) (hash-set :a :b :c) ;=&gt; #{:a :b :c} ;; tworzenie zbioru sortowanego (funkcja) (sorted-set :a :b :c) ;=&gt; #{:a :b :c} ;; tworzenie zbioru z S-wyrażenia (literał) #{:a :b :c} ;=&gt; #{:a :b :c} ;; tworzenie zbioru z S-wyrażenia (literał) #{:a :b (keyword &#34;c&#34;)} ;=&gt; #{:a :b :c} ;; tworzenie zbioru z sekwencji (set [:a :b :c]) ;=&gt; #{:a :b :c}

Zobacz także:

Funkcje

Funkcja (ang. function) to konstrukcja zawierająca kod, który w trakcie wywołania dokonuje obliczania wartości dla podanego zestawu argumentów (ang. arguments). W językach czysto funkcyjnych jest to jedyny rezultat użycia funkcji, a w pozostałych (m.in. w Clojure) funkcje mogą wywoływać tzw. efekty uboczne (ang. side effects), tzn. poza zwróceniem wartości wpływać na otoczenie (np. modyfikować wartość zmiennej globalnej czy korzystać z podsystemu wejścia/wyjścia).

Poza tym rezultat działań funkcji w Clojure może zależeć od otoczenia. Przykładem będzie odwoływanie się w obliczeniach do globalnych obiektów referencyjnych wyrażających pewien zmienny stan bądź korzystanie z wartości pochodzących z leksykalnego otoczenia definicji funkcji. W tym drugim przypadku funkcję taką nazwiemy domknięciem (ang. closure).

Definicja funkcji w Clojure składa się z listowego S-wyrażenia zawierającego:

  • wektorowe S-wyrażenie definiujące listę argumentów (ang. argument list),
  • jedno lub więcej S-wyrażeń stanowiących ciało funkcji (ang. function body).

W języku Clojure możemy również korzystać z funkcji wieloczłonowych, które mogą obsługiwać więcej, niż jeden zestaw argumentów. W ich przypadku składnia definicji będzie nieco inna.

Wartością zwracaną (ang. return value) funkcji jest obliczona wartość ostatniego S-wyrażenia z jej ciała (lub z ciała danej wartościowości w przypadku funkcji wieloczłonowej).

W Clojure do tworzenia funkcji anonimowych (ang. anonymous functions) używamy formy specjalnej fn, a do tworzenia funkcji nazwanych (ang. named functions) makra defn. Funkcje anonimowe to obiekty funkcyjne, które nie mają nadanych nazw. Funkcje nazwane mają w Clojure nazwy wyrażone zmiennymi globalnymi.

Definiowanie funkcji wyświetlającej napis z wykrzyknikiem
1(defn krzycz          ; definicja funkcji nazwanej
2  [napis]             ; wektor z argumentami
3  (print napis "!"))  ; wywołanie print z parametrem napis
4
5(krzycz "Witaj, Lispie")  ; wywołanie funkcji z argumentem
6;=>> Witaj, Lispie!       ; rezultat wywołania na ekranie
7;=> nil                   ; zwracana wartość
(defn krzycz ; definicja funkcji nazwanej [napis] ; wektor z argumentami (print napis &#34;!&#34;)) ; wywołanie print z parametrem napis (krzycz &#34;Witaj, Lispie&#34;) ; wywołanie funkcji z argumentem ;=&gt;&gt; Witaj, Lispie! ; rezultat wywołania na ekranie ;=&gt; nil ; zwracana wartość

Możemy też stworzyć wersję anonimową powyższej funkcji, jednak dla potrzeb przykładu musimy całą konstrukcję umieścić w S-wyrażeniu reprezentującym formę wywołania funkcji, aby móc zaobserwować rezultat jej działania:

Definiowanie funkcji anonimowej
1(                       ; forma wywołania funkcji
2  (fn                   ; definicja funkcji anonimowej
3    [napis]             ; wektor z argumentami
4    (print napis "!"))  ; wywołanie print z parametrem napis
5  "Witaj, Lispie!")     ; argument wywołania funkcji
6
7;=>> Witaj, Lispie!     ; rezultat wywołania na ekranie
8;=> nil                 ; zwracana wartość
( ; forma wywołania funkcji (fn ; definicja funkcji anonimowej [napis] ; wektor z argumentami (print napis &#34;!&#34;)) ; wywołanie print z parametrem napis &#34;Witaj, Lispie!&#34;) ; argument wywołania funkcji ;=&gt;&gt; Witaj, Lispie! ; rezultat wywołania na ekranie ;=&gt; nil ; zwracana wartość

Powyższe można też wyrazić z użyciem literału funkcji anonimowej:

Użycie literału funkcji anonimowej
1(                    ; forma wywołania funkcji
2  #(print %1 "!")    ; definicja i wywołanie print z pierwszym parametrem
3  "Witaj, Lispie!")  ; argument wywołania funkcji
4
5;=>> Witaj, Lispie!  ; rezultat wywołania na ekranie
6;=> nil              ; zwracana wartość
( ; forma wywołania funkcji #(print %1 &#34;!&#34;) ; definicja i wywołanie print z pierwszym parametrem &#34;Witaj, Lispie!&#34;) ; argument wywołania funkcji ;=&gt;&gt; Witaj, Lispie! ; rezultat wywołania na ekranie ;=&gt; nil ; zwracana wartość

Nic nie stoi też na przeszkodzie, aby zdefiniować funkcję anonimową, a następnie samodzielnie przypisać jej nazwę:

Samodzielne definiowanie funkcji nazwanej
1(def krzycz               ; definicja zmiennej globalnej
2  (fn                     ; definicja funkcji anonimowej
3    [napis]               ; wektor z argumentami
4    (print napis "!")))   ; wywołanie print z parametrem napis
5
6(krzycz "Witaj, Lispie")  ; wywołanie funkcji z argumentem
7;=>> Witaj, Lispie!       ; rezultat wywołania na ekranie
8;=> nil                   ; zwracana wartość
(def krzycz ; definicja zmiennej globalnej (fn ; definicja funkcji anonimowej [napis] ; wektor z argumentami (print napis &#34;!&#34;))) ; wywołanie print z parametrem napis (krzycz &#34;Witaj, Lispie&#34;) ; wywołanie funkcji z argumentem ;=&gt;&gt; Witaj, Lispie! ; rezultat wywołania na ekranie ;=&gt; nil ; zwracana wartość

W powyższym przykładzie definiujemy zmienną globalną krzycz, z wartością początkową przypisaną do rezultatu wywołania formy specjalnej fn, którym będzie obiekt funkcyjny (ang. function object) – reprezentacja istniejącej funkcji.

W Lispie funkcje są jednostkami pierwszej kategorii (ang. first-class citizens), tzn. wartościami, które mogą być traktowane tak, jak każde inne: przekazywane jako argumenty, albo zwracane jako rezultaty działania funkcji.

Zobacz także:

Wartości logiczne

Do wyrażania wartości logicznych w Clojure możemy użyć singletonów typu java.lang.Boolean: true oznaczającego logiczną prawdę i false oznaczającego logiczny fałsz. Wartości te zwracane są przez wbudowane predykaty (funkcje, które na podstawie argumentów zwracają logiczną prawdę lub fałsz) i konstrukcje warunkowe.

Wartość nil

Literał nil oznacza w Clojure wartość braku. Na JVM jest reprezentowany przez javowe null, lecz Clojure nadaje mu jawne reguły dotyczące kolekcji, sekwencji, równości i prawdziwości.

Funkcje mogą zwracać nil, gdy nie ma bardziej swoistego wyniku, a wiele operacji wyszukiwania używa go dla brakującej wartości. W warunkach nilfalse są jedynymi wartościami fałszywymi. Funkcje sekwencyjne także używają nil do oznaczenia braku sekwencji, w tym końca osiągniętego przez next.

Komentarze

Komentarze w programach komputerowych służą do objaśniania fragmentów kodu źródłowego w języku naturalnym. Mogą pełnić funkcję dokumentacyjną lub zawierać notatki dotyczące postępów w realizacji pewnych zadań. Czasami też wykorzystuje się je do zaprezentowania rezultatów obliczeń przykładowych form.

W Clojure komentarz rozpoczynamy znakiem średnika ;, po którym umieszczamy dowolny tekst. Komentarzem będzie dowolna sekwencja znaków, aż do końca linii, w której go rozpoczęto:

 1(print "Witaj, Lispie!")  ; to jest komentarz
 2
 3;; To jest dłuższy komentarz
 4;; zapisany zgodnie z konwencją
 5;; poprzedzania każdej linii
 6;; dwoma średnikami.
 7
 8;; Poniższe komentarze
 9;; służą prezentowaniu rezultatów obliczeń.
10 
11(+ 2 2)  ; => 4
12(- 2 2)  ; => 0
13
14(+ 2 2)
15; => 4
16
17'(1 2 3)
18; => (1 2 3)
(print &#34;Witaj, Lispie!&#34;) ; to jest komentarz ;; To jest dłuższy komentarz ;; zapisany zgodnie z konwencją ;; poprzedzania każdej linii ;; dwoma średnikami. ;; Poniższe komentarze ;; służą prezentowaniu rezultatów obliczeń. (+ 2 2) ; =&gt; 4 (- 2 2) ; =&gt; 0 (+ 2 2) ; =&gt; 4 &#39;(1 2 3) ; =&gt; (1 2 3)

Forma komentująca

Poza komentarzami składniowymi, wyrażanymi średnikiem, w Clojure istnieje specjalne makro, które pozwala wyłączać obliczanie wskazanego S-wyrażenia:

(comment
    (print "Witaj, Lispie!")
    123)
(comment (print &#34;Witaj, Lispie!&#34;) 123)

Warto zauważyć, że comment jest konstrukcją, której jako argumenty należy przekazać poprawne składniowo symboliczne wyrażenia.

Wartością zwracaną przez makro jest nil.

Ignorowanie następnej formy

Makro czytnika #_ sprawia, że czytnik pomija następną kompletną formę. Pominięta forma nie dociera do analizy kompilatora, co jest działaniem wcześniejszym i silniejszym niż samo powstrzymanie jej wartościowania:

#_ (print "Witaj, Lispie!")    ; cała forma zostaje pominięta
(#_#_ print "Witaj, Lispie!")  ; pominięte są dwie formy, pozostaje ()
(print #_ "Witaj, Lispie!")    ; pominięty jest łańcuch, pozostaje (print)
[#_#_ 1 2 3]                   ; pominięte są 1 i 2, pozostaje [3]
#_ (print &#34;Witaj, Lispie!&#34;) ; cała forma zostaje pominięta (#_#_ print &#34;Witaj, Lispie!&#34;) ; pominięte są dwie formy, pozostaje () (print #_ &#34;Witaj, Lispie!&#34;) ; pominięty jest łańcuch, pozostaje (print) [#_#_ 1 2 3] ; pominięte są 1 i 2, pozostaje [3]

Konstrukcje sterujące

Clojure wyposażono w wiele form specjalnych i makr, które służą do sterowania procesem wartościowania wyrażeń, m.in. kolejnością, liczbą powtórzeń czy warunkowaniem przetwarzania.

Wyrażenia warunkowe

Znane z języków imperatywnych instrukcje warunkowe mają w Lispach odpowiedniki jako warunkowe formy specjalne lub makra. W odróżnieniu od zwykłego wywołania funkcji konstrukcje te sterują tym, które formy argumentów zostaną zwartościowane. Dzięki temu można wybrać gałąź bez wartościowania pozostałych.

Formy specjalne i makra będą omówione później. Podstawowa różnica polega na tym, że makro rozwija się do form zastępczych, które są ponownie analizowane, natomiast forma specjalna jest obsługiwana bezpośrednio przez kompilator według wbudowanej gramatyki.

Wykonywanie warunkowe, if

Forma specjalna if służy do warunkowego obliczania wyrażeń.

Użycie:

  • (if predykat wyrażenie-prawda wyrażenie-fałsz?).

Forma if przyjmuje dwa obowiązkowe argumenty. Pierwszym jest warunek do przeliczenia, który (jeżeli nie będzie przyjmował ani wartości false ani nil) sprawi, że kolejny argument zostanie również obliczony. Jeżeli podano opcjonalny trzeci argument, będzie on obliczony tylko wtedy, gdy pierwszy argument ma wartość false lub nil.

Forma specjalna if zwraca wartość ostatnio obliczonego wyrażenia lub nil, jeżeli nie podano trzeciego argumentu, a pierwsze wyrażenie zwróciło logiczny fałsz.

Przykład użycia formy specjalnej if
1(if (= 2 2)
2  "równe"
3  "różne")
4; => "równe"
(if (= 2 2) &#34;równe&#34; &#34;różne&#34;) ; =&gt; &#34;równe&#34;

Wraz z formą if często wykorzystywane jest makro and, które służy do wyrażania operacji koniunkcji logicznej (iloczynu logicznego). Dokonuje ono obliczania wartości wyrażeń podanych jako jego argumenty (w porządku występowania), dopóki ich wartością jest logiczna prawda (nie wartość false i nie nil). Zwracaną wartością jest wtedy wartość ostatnio podanego wyrażenia. Jeżeli któreś z wyrażeń zwróci wartość false lub nil, przetwarzanie kolejnych argumentów zostaje wstrzymane i zwracana jest jego wartość.

Przykład użycia makra and
1(if (and (= 2 2) (< 2 3)) true)
2; => true
(if (and (= 2 2) (&lt; 2 3)) true) ; =&gt; true

Innym makrem, które znajdziemy w towarzystwie if jest or. Służy ono do wyrażania operacji sumy logicznej (logicznej alternatywy). Makro to oblicza wartości kolejnych wyrażeń podanych jako jego argumenty (w kolejności występowania) do momentu, aż wartością któregoś będzie logiczna prawda (nie wartość false i nie nil). Zwracaną wartością jest wtedy wartość ostatnio przetwarzanego wyrażenia. Jeżeli wartości wszystkich wyrażeń to false lub nil, zwracana jest wartość ostatniego podanego wyrażenia (false lub nil).

Przykład użycia makra or
1(if (or (= 2 2) (< 4 3)) true)
2; => true
(if (or (= 2 2) (&lt; 4 3)) true) ; =&gt; true

Wykonywanie warunkowe, if-not

Makro if-not jest odwrotną wersją formy specjalnej if.

Użycie:

  • (if-not predykat wyrażenie-fałsz wyrażenie-prawda?).

Makro przyjmuje dwa obowiązkowe argumenty. Pierwszym jest warunek do przeliczenia, który jeżeli będzie przyjmował wartość logicznego fałszu (false lub nil), sprawi, że kolejny argument zostanie również obliczony. Jeżeli podano opcjonalny trzeci argument, będzie on obliczony tylko wtedy, gdy pierwszy argument reprezentuje logiczną prawdę (ma wartość różną od false i różną od nil).

Makro if-not zwraca wartość ostatnio obliczonego wyrażenia lub nil, jeżeli nie podano trzeciego argumentu, a pierwsze wyrażenie zwróciło logiczną prawdę.

Przykład użycia makra if-not
1(if-not (= 2 2)
2  "różne"
3  "równe")
4; => "równe"
(if-not (= 2 2) &#34;różne&#34; &#34;równe&#34;) ; =&gt; &#34;równe&#34;

Wykonywanie warunkowe, when

Makro when działa podobnie do konstrukcji if, przy czym jest nieco prostsze, ponieważ nie ma w nim miejsca na wyrażenie alternatywne (wykonywane, gdy podany warunek nie jest spełniony).

Użycie:

  • (when predykat & wyrażenie…).

Pierwszy argument powinien być formą wyrażającą warunek logiczny (zwracającą true lub false, ew. nil), a każdy pozostały będzie potraktowany jak wyrażenie, które zostanie obliczone, gdy wartością zwracaną przez konstrukcję warunkową będzie logiczna prawda (wartość różna od false i różna od nil).

Przykład użycia makra when
1(when (= 2 2)
2  (println "można podać")
3  (println "wiele wyrażeń"))
4
5; >> można podać
6; >> wiele wyrażeń
(when (= 2 2) (println &#34;można podać&#34;) (println &#34;wiele wyrażeń&#34;)) ; &gt;&gt; można podać ; &gt;&gt; wiele wyrażeń

Wykonywanie warunkowe, when-not

Makro when-not jest odwrotną wersją makra when.

Użycie:

  • (when-not predykat & wyrażenie…).

Pierwszy argument powinien być predykatem, a każdy pozostały będzie potraktowany jak wyrażenie, które zostanie obliczone, gdy wartością predykatu będzie logiczny fałsz (wartość false lub nil).

Przykład użycia makra when-not
1(when-not (= 2 1)
2  (println "można podać")
3  (println "wiele wyrażeń"))
4
5; >> można podać
6; >> wiele wyrażeń
(when-not (= 2 1) (println &#34;można podać&#34;) (println &#34;wiele wyrażeń&#34;)) ; &gt;&gt; można podać ; &gt;&gt; wiele wyrażeń

Lista warunków, cond

Makro cond pozwala zapisać listę warunków z przypisanymi do niej wyrażeniami, które zostaną obliczone, gdy dany warunek będzie spełniony.

Użycie:

  • (cond & para…),

gdzie para to:

  • predykat wyrażenie,
  • :else wyrażenie-domyślne.

Makro przyjmuje parzystą liczbę argumentów. Dla każdej pary sprawdza, czy podane wyrażenie warunkowe zwraca wartość różną od false i od nil. Jeżeli tak jest, wartościowany będzie przypisany mu argument z pary (wyrażenie) i przetwarzanie dalszych argumentów zostanie wstrzymane, a zwracaną wartością będzie wartość obliczonego wyrażenia.

Opcjonalnie można dodać ostatnią parę (opcję), której pierwszy element równy będzie :else. Przypisane mu wyrażenie zostanie obliczone, gdy żadne wcześniejsze nie zakończyło pracy. Pozwala to ustawiać domyślną wartość zwracaną.

Jeżeli nie podano opcji domyślnej, a żaden warunek nie był spełniony, zwracaną wartością będzie nil.

Przykład użycia makra cond
1(cond
2  (= 2 3) "2 i 3 są równe"
3  (< 2 3) "2 jest mniejsze niż 3"
4  :else   "nic")
5
6; => "2 jest mniejsze niż 3"
(cond (= 2 3) &#34;2 i 3 są równe&#34; (&lt; 2 3) &#34;2 jest mniejsze niż 3&#34; :else &#34;nic&#34;) ; =&gt; &#34;2 jest mniejsze niż 3&#34;

Lista przypadków, case

Makro case działa podobnie do cond, ale zamiast wyrażeń warunkowych w opcjach należy podawać stałe wartości, które będą dopasowywane do wartości pierwszego przekazanego argumentu wywołania.

Użycie:

  • (case wartość opcja…),

gdzie opcja to:

  • wartość wyrażenie,
  • (wartość…) wyrażenie,
  • wyrażenie-domyślne.

Jeżeli chcemy porównywać podaną wartość z wieloma z serii dla każdej z możliwych opcji, należy tę serię możliwości zapisać w postaci listowego S-wyrażenia.

Wartości, z którymi porównywana będzie wartość pierwszego argumentu, muszą być stałymi czasu kompilacji. Możemy więc podawać literały albo listy literalnych wartości testowych, ale nie dowolne wyrażenia, których wartości poznamy dopiero podczas działania programu. Wyrażenia obliczane po udanym teście mogą być dowolnymi formami.

Opcjonalnie możliwe jest podanie jako ostatniego argumentu wyrażenia, które zostanie obliczone, jeżeli badana wartość nie spełni żadnego z wcześniej podanych warunków. Warto z tej możliwości korzystać, ponieważ w przeciwnym wypadku program zgłosi wyjątek, gdy żadne z dopasowań nie będzie spełnione.

Przykład formy specjalnej case
1(def x 'herbatniki) ; x odnosi się do symbolu herbatniki
2
3(case x
4  ""                         "nic"
5  (draże miętusy)       "cukierki"
6  (herbatniki pierniki)  "ciastka"
7  (str x " to produkt nieznany"))
8
9; => "ciastka"
(def x &#39;herbatniki) ; x odnosi się do symbolu herbatniki (case x &#34;&#34; &#34;nic&#34; (draże miętusy) &#34;cukierki&#34; (herbatniki pierniki) &#34;ciastka&#34; (str x &#34; to produkt nieznany&#34;)) ; =&gt; &#34;ciastka&#34;

Symbole i listy użyte jako stałe testowe celowo nie zostały zacytowane. case jest makrem o własnej składni: te pozycje są danymi testowymi czasu kompilacji, a nie zwykłymi argumentami funkcji.

Lista warunków z predykatem, condp

Makro condp działa podobnie jak cond, ale jest o wiele bardziej elastyczne.

Użycie:

  • (condp predykat wyrażenie & para…)

Jako pierwszy argument należy podać predykat, czyli funkcję, która powinna zwracać wartość logiczną, a jako drugi wyrażenie. Kolejne, nieobowiązkowe argumenty to składające się z par opcje, które mogą być wyrażone następującymi zapisami:

  • wyrażenie-testowe wyrażenie-wynikowe,
  • wyrażenie-testowe :>> funkcja-wynikowa,
  • wyrażenie-domyślne.

Symboliczny zapis :>> to słowo kluczowe, które ma w kontekście makra condp specjalne znaczenie.

Dla każdej podanej opcji będzie wartościowana przypisana jej forma wywołania funkcji:

  • (predykat wyrażenie-warunkowe wyrażenie).

Predykat będzie wywoływany dla każdej podanej w kolejności opcji z dwoma przekazywanymi argumentami. Pierwszy będzie wartością wyrażenia testowego danej opcji, a drugi wyrażeniem podanym jako argument wywołania całego makra.

Jeżeli predykat zwróci wartość odpowiadającą logicznej prawdzie (różną od false i różną od nil), sprawdzanie kolejnych opcji zakończy się i zostanie zwrócone wyrażenie wynikowe przypisane do bieżącej opcji lub wartość powstała na skutek wywołania funkcji wynikowej (w przypadku, gdy użyto słowa kluczowego :>>). W tym ostatnim przypadku do funkcji (która powinna przyjmować jeden argument) zostanie przekazana wartość zwracana przez predykat.

W przypadku, gdy jako ostatnią z opcji podano dodatkowe wyrażenie (wyrażenie-domyślne), jego wartość zostanie zwrócona, jeżeli żadna z opcji nie zostanie dopasowana do podanych danych. Gdy taka sytuacja wystąpi, ale nie podano domyślnego wyrażenia, zostanie rzucony IllegalArgumentException.

Przykład użycia makra condp
 1(condp
 2  =           ; predykat
 3  (+ 2 2)     ; wyrażenie (da w wyniku 4)
 4  1 "jeden"   ; opcja 1: łańcuch znakowy
 5  2 "dwa"     ; opcja 2: łańcuch znakowy
 6  3 "trzy"    ; opcja 3: łańcuch znakowy
 7  4 "cztery"  ; opcja 4: łańcuch znakowy
 8  "żadna")    ; wyrażenie opcji domyślnej
 9
10; => "cztery"
(condp = ; predykat (+ 2 2) ; wyrażenie (da w wyniku 4) 1 &#34;jeden&#34; ; opcja 1: łańcuch znakowy 2 &#34;dwa&#34; ; opcja 2: łańcuch znakowy 3 &#34;trzy&#34; ; opcja 3: łańcuch znakowy 4 &#34;cztery&#34; ; opcja 4: łańcuch znakowy &#34;żadna&#34;) ; wyrażenie opcji domyślnej ; =&gt; &#34;cztery&#34;

Dostęp do składowych Javy

Dostęp do składowej, .

Forma specjalna . to w Clojure sposób na dostęp do obiektów Javy. Jest to de facto operator wyboru składowej (ang. member-access operator).

  • Gdy celem jest symbol nazywający klasę Javy, kompilator potraktuje zapis jako dostęp do statycznej składowej tej klasy.

  • W przeciwnym razie wyrażenie celu zostanie zwartościowane do instancji. Składnia składowej może oznaczać pole albo metodę; poprzedzenie nazwy składowej łącznikiem (-) jawnie wskazuje dostęp do pola. Podpowiedzi typów mogą pomóc kompilatorowi rozpoznać niejednoznaczne albo przeciążone składowe hosta bez refleksji.

  • Jeżeli podano kolejne argumenty poza pierwszym, wywołana zostanie metoda instancji o podanej jako pierwszy argument nazwie. Dodatkowe argumenty zostaną (od lewej do prawej) przekazane do wywołania po ewaluacji ich wartości.

Dodatkowe argumenty mogą być też listowym S-wyrażeniem. W takim przypadku na pierwszym miejscu powinien znajdować się symbol identyfikujący metodę do wywołania.

Istnieje też skrócona składnia, w której kropka poprzedza nazwę składowej na pozycji operatora, np. (.trim wartość).

Przykłady użycia formy specjalnej .
(. " a " trim)             ; => a
(.trim " a ")              ; => a
(. "abc" substring 0 1)    ; => a
(. "abc" (substring 0 1))  ; => a
(. &#34; a &#34; trim) ; =&gt; a (.trim &#34; a &#34;) ; =&gt; a (. &#34;abc&#34; substring 0 1) ; =&gt; a (. &#34;abc&#34; (substring 0 1)) ; =&gt; a

Dostęp do składowych, ..

Makro o nazwie .. jest lukrem składniowym i pozwala wyrażać zagnieżdżony dostęp do składowych obiektów Javy w czytelny sposób. Efektem jego użycia jest stworzenie kaskady wywołań omówionej wyżej formy specjalnej ..

W praktyce .. wykonuje serię wywołań metod, w której wynik jednej staje się obiektem wejściowym dla kolejnej. Każde wyrażenie po .. jest interpretowane podobnie jak w formie ., ale bez konieczności ręcznego zagnieżdżania.

Przykłady użycia makra ..
(.. " a " trim length)                             ; => 1
(.. " a " trim length toString (replace "1" "X"))  ; => "X"
(.. &#34; a &#34; trim length) ; =&gt; 1 (.. &#34; a &#34; trim length toString (replace &#34;1&#34; &#34;X&#34;)) ; =&gt; &#34;X&#34;

Organizowanie kodu

Formy Clojure są wyrażeniami i wytwarzają wartości, ale ich wartościowanie może też powodować efekty. Czyste przekształcenia i jawne granice efektów mogą więc współistnieć bez udawania, że każda użyteczna operacja jest czysta.

Grupowanie S-wyrażeń, do

Chyba najbardziej powszechną konstrukcją, która pozwala na chwilę zapomnieć o tym, że mamy do czynienia z wartościowanymi wyrażeniami i potraktować elementy programu jak instrukcje jest forma specjalna do. Służy ona do grupowania wielu wyrażeń w taki sposób, że tylko wartość ostatniego zostanie zwrócona, chociaż ewentualne efekty uboczne (np. wyświetlanie tekstu na ekranie) będą mogły powstawać we wszystkich.

Użycie:

  • (do & wyrażenie…).

Forma specjalna do przyjmuje zero lub więcej S-wyrażeń, które będą obliczone w kolejności ich podawania. Wartością zwracaną będzie rezultat wartościowania ostatniego z nich.

Jeżeli nie podamy żadnego S-wyrażenia, wartością zwracaną będzie nil.

Przykłady użycia formy specjalnej do
 1(do)
 2; => nil
 3
 4(do 123 456)
 5; => 456
 6
 7(do
 8  (println "test")
 9  123
10  (+ 1 1)
11  (+ 2 2))
12; >> test
13; => 4
(do) ; =&gt; nil (do 123 456) ; =&gt; 456 (do (println &#34;test&#34;) 123 (+ 1 1) (+ 2 2)) ; &gt;&gt; test ; =&gt; 4

Przyłączanie argumentu, doto

Makro doto wartościuje cel jeden raz, wstawia wynik po operatorze każdej następnej formy listowej, wartościuje te formy dla ich efektów i zwraca pierwotny cel. Najczęściej służy do zapisu serii wywołań metod Javy na jednym obiekcie.

  • (doto wartość & wyrażenie…).

Makro przyjmuje jedno obowiązkowe wyrażenie celu. Każda następna lista zostanie przekształcona tak, aby wartość celu stała się jej pierwszym argumentem.

Wartością zwracaną przez doto jest zwartościowana wartość celu.

Przykłady użycia makra doto
 1(doto 2
 2  (println "za pierwszym razem")
 3  (println "za drugim razem"))
 4
 5; >> 2 za pierwszym razem
 6; >> 2 za drugim razem
 7; => 2
 8
 9(doto (java.util.ArrayList.)
10  (.add 1)
11  (.add 2)
12  (.add 3))
13
14; => [1 2 3]
(doto 2 (println &#34;za pierwszym razem&#34;) (println &#34;za drugim razem&#34;)) ; &gt;&gt; 2 za pierwszym razem ; &gt;&gt; 2 za drugim razem ; =&gt; 2 (doto (java.util.ArrayList.) (.add 1) (.add 2) (.add 3)) ; =&gt; [1 2 3]

Liczenie czasu, time

Makro time pozwala nam wyświetlić czas wykonania podanego wyrażenia. Przydaje się, gdy chcemy optymalizować fragmenty programu pod kątem wydajnościowym.

Użycie:

  • (time wyrażenie).

Argumentem makra powinno być S-wyrażenie, którego wartość będzie obliczona. Można użyć do, jeżeli chcemy podać więcej wyrażeń.

Wartością zwracaną makra time jest wartość przeliczonego wyrażenia, które podano.

Efektem ubocznym wywołania makra jest wyświetlenie na standardowym wyjściu raportu z czasu wykonywania podanego wyrażenia.

Przykłady użycia makra time
1(time 123)
2; >> "Elapsed time: 0.012667 msecs"
3; => 123
4
5(time (Thread/sleep 500))
6; >> "Elapsed time: 505.079375 msecs"
7; => nil
(time 123) ; &gt;&gt; &#34;Elapsed time: 0.012667 msecs&#34; ; =&gt; 123 (time (Thread/sleep 500)) ; &gt;&gt; &#34;Elapsed time: 505.079375 msecs&#34; ; =&gt; nil

Przewlekanie, ->

Makro przewlekania (ang. threading macro), zwane też makrem przewlekania pierwszych argumentów (ang. thread-first macro), umożliwia przedstawianie kodu źródłowego z zagnieżdżonymi wywołaniami w bardziej czytelny sposób. Jego działanie polega na przyjęciu zestawu S-wyrażeń i potraktowaniu każdego poza pierwszym jako formy wywołania (nawet, jeżeli nie ujęto go w nawiasy), a następnie wstawianiu w miejsce pierwszego argumentu każdej z form wartości zwracanej przez wywołanie umieszczone wcześniej. Dzięki temu możemy na chwilę zapomnieć o notacji polskiej.

Użycie:

  • (-> wyrażenie & wyrażenie…).

Makro przyjmuje wyrażenie początkowe i zero lub więcej form wywołania. Wynikiem jest wartość całego rozwiniętego łańcucha wywołań.

Przykład użycia makra ->
1(-> [1 2 3 4]  ; rezultat [1 2 3 4]
2    last       ; rezultat (last [1 2 3 4]) => 4
3    (- 2)      ; rezultat (- 4 2)          => 2
4    println    ; rezultat (println 2)      => nil
5)
6
7; >> 2
(-&gt; [1 2 3 4] ; rezultat [1 2 3 4] last ; rezultat (last [1 2 3 4]) =&gt; 4 (- 2) ; rezultat (- 4 2) =&gt; 2 println ; rezultat (println 2) =&gt; nil ) ; &gt;&gt; 2

Spójrzmy na te same operacje uszeregowane w kodzie bez używania makra:

1(println
2  (- (last
3      [1 2 3 4]))
4     2)
5
6; => nil
7; >> 2
(println (- (last [1 2 3 4])) 2) ; =&gt; nil ; &gt;&gt; 2

Porównajmy obydwa zapisy w postaciach bardziej kompaktowych:

1(println (- (last [1 2 3 4])) 2)   ; zapis zagnieżdżony
2(-> [1 2 3 4] last (- 2) println)  ; liniowy zapis przepływu danych
(println (- (last [1 2 3 4])) 2) ; zapis zagnieżdżony (-&gt; [1 2 3 4] last (- 2) println) ; liniowy zapis przepływu danych

Obie postacie rozwijają się do tych samych zagnieżdżonych wywołań i podlegają tym samym regułom wartościowania. Różni je układ źródła: -> przedstawia przepływ danych od lewej do prawej, wstawiając każdą formę pośrednią jako pierwszy argument kolejnego wywołania.

Przewlekanie ostatnich, ->>

Makro przewlekania ostatnich argumentów (ang. thread-last macro) działa podobnie do makra przewlekania, lecz wartość każdego poprzedniego S-wyrażenia nie będzie podstawiana jako pierwszy, ale jako ostatni argument kolejnego.

Użycie:

  • (->> wyrażenie & wyrażenie…).

Makro przyjmuje wyrażenie początkowe i zero lub więcej form wywołania. Wynikiem jest wartość całego rozwiniętego łańcucha wywołań.

Przykład użycia makra ->>
1(->> [1 2 3 4]  ; rezultat [1 2 3 4]
2     last       ; rezultat (last [1 2 3 4]) => 4
3     (- 2)      ; rezultat (- 2 4)          => -2
4     println    ; rezultat (println -2)     => nil
5)
6
7; => nil
8; >> -2
(-&gt;&gt; [1 2 3 4] ; rezultat [1 2 3 4] last ; rezultat (last [1 2 3 4]) =&gt; 4 (- 2) ; rezultat (- 2 4) =&gt; -2 println ; rezultat (println -2) =&gt; nil ) ; =&gt; nil ; &gt;&gt; -2

W powyższym przykładzie widzimy, że różnica w obliczeniach w stosunku do przykładów poprzednich pojawia się w linii nr 3. Dochodzi do zamiany argumentów odejmowania z powodu podstawienia wartości jako drugi argument odejmowania.

Przewlekanie wartościowych, some->

Makro some-> działa podobnie do ->, lecz natychmiast zatrzymuje przewlekanie i dalsze wartościowanie wyrażeń, gdy którakolwiek forma w łańcuchu zwróci nil. Przydaje się na przykład wtedy, gdy wyszukiwanie może zwrócić nil, a kolejnych operacji nie należy wywoływać na tej wartości.

Użycie:

  • (some-> wyrażenie & wyrażenie…).

Makro przyjmuje wyrażenie początkowe i zero lub więcej form wywołania. Zwraca końcowy wynik przewlekania albo nil, gdy tylko wynik pośredni będzie równy nil.

Przykład użycia makra some->
1(some-> [1 2 3 4]  ; rezultat [1 2 3 4]
2        (get 50)   ; rezultat (get [1 2 3 4] 50)  => nil
3        (+ 2)      ; brak wartościowania
4        println    ; brak wartościowania
5)
6
7; => nil
(some-&gt; [1 2 3 4] ; rezultat [1 2 3 4] (get 50) ; rezultat (get [1 2 3 4] 50) =&gt; nil (+ 2) ; brak wartościowania println ; brak wartościowania ) ; =&gt; nil

Widzimy, że w linii nr 2 doszło do przerwania dalszego wartościowania S-wyrażeń z łańcucha, ponieważ forma (get [1 2 3 4] 50) zwróciła nil (przy próbie dostępu do nieistniejącego elementu o indeksie 50). Spójrzmy co by się stało, gdyby zamiast some-> użyć ->:

1(-> [1 2 3 4]  ; rezultat [1 2 3 4]
2    (get 50)   ; rezultat (get [1 2 3 4] 50)  => nil
3    (+ 2)      ; rezultat (+ nil 2)  (wyjątek)
4    println    ; brak wartościowania (wyjątek)
5)
6
7; >> java.lang.NullPointerException:
(-&gt; [1 2 3 4] ; rezultat [1 2 3 4] (get 50) ; rezultat (get [1 2 3 4] 50) =&gt; nil (+ 2) ; rezultat (+ nil 2) (wyjątek) println ; brak wartościowania (wyjątek) ) ; &gt;&gt; java.lang.NullPointerException:

Zamiast zakończyć łańcuch i zwrócić nil, wartościowanie rzuca wyjątek, gdy + otrzymuje nil. REPL zgłasza wyjątek i pozostaje dostępny; dalszy los aplikacji zależy od otaczającej polityki obsługi błędów.

Przewlekanie wartościowych, some->>

Makro some->> działa podobnie do makra some->, ale wartości poprzednio wyliczonych S-wyrażeń podstawiane są jako ostatnie argumenty kolejnych.

Użycie:

  • (some->> wyrażenie & wyrażenie…).

Makro przyjmuje wyrażenie początkowe i zero lub więcej form wywołania. Zwraca końcowy wynik przewlekania albo nil, gdy tylko wynik pośredni będzie równy nil.

Przykład użycia makra some->>
1(some->> [2 4 6 8]      ; rezultat [2 4 6 8]
2         (filter odd?)  ; rezultat (filter odd? [2 4 6 8]) => ()
3         first          ; rezultat (first ()) => nil
4         (+ 2)          ; brak wartościowania
5         println        ; brak wartościowania
6)
7
8; => nil
(some-&gt;&gt; [2 4 6 8] ; rezultat [2 4 6 8] (filter odd?) ; rezultat (filter odd? [2 4 6 8]) =&gt; () first ; rezultat (first ()) =&gt; nil (+ 2) ; brak wartościowania println ; brak wartościowania ) ; =&gt; nil

W linii nr 2 użyliśmy nieomawianej jeszcze funkcji filter. Dokonuje ona filtrowania kolekcji podanej jako drugi argument, a do decydowania o tym, czy element zostanie wpisany do wyjściowej struktury danych, czy pominięty, używa predykatu przekazanego jako pierwszy argument wywołania.

Tu rolę predykatu pełni funkcja odd?, która zwraca logiczną prawdę, gdy wartość przekazanego jej argumentu jest liczbą nieparzystą. W efekcie filter wygeneruje pustą kolekcję, ponieważ wejściowa zawiera same liczby parzyste.

Wywołanie first w odniesieniu do pustej kolekcji zwróci nil i na tym przewlekanie się zakończy. Gdybyśmy nie zastosowali makra some->>, wtedy S-wyrażenie (+ 2 nil) doprowadziłoby do zgłoszenia wyjątku podczas wartościowania.

Przewlekanie warunkowe, cond->

Makro cond-> działa podobnie do ->, jednak S-wyrażenia interpretowane są parami. Pierwsze z każdej pary jest wyrażeniem warunkowym, a drugie będzie wartościowane wtedy, gdy pierwsze jest prawdziwe (jego wartością nie jest false ani nil). Każde drugie wyrażenie par traktowane jest jak forma wywołania, nawet jeżeli nie ujęto go w nawiasy.

Warunkowe przewlekanie pierwszego argumentu przydaje się do czytelnego wyrażenia zestawu obliczeń, które zależą od aktualnych warunków.

Użycie:

  • (cond-> wyrażenie & para…),

gdzie para to:

  • predykat wyrażenie.

Makro cond-> przyjmuje wyrażenie początkowe oraz pary S-wyrażeń, gdzie pierwsze jest predykatem, a drugie będzie wartościowane w taki sposób, że jako pierwszy argument będzie wstawiony rezultat wartościowania poprzedniego wyrażenia z łańcucha. Wartością zwracaną jest wartość ostatnio obliczonego S-wyrażenia.

Przykład użycia makra cond->
 1(def dodaj-jeden?    true)
 2(def odejmij-jeden? false)
 3
 4(cond-> 123      ; rezultat 123
 5         dodaj-jeden?   inc  ; rezultat (inc 123) => 124
 6         odejmij-jeden? dec  ; brak wartościowania
 7         true       println  ; rezultat (println 124) => nil
 8)
 9
10; => nil
11; >> 124
(def dodaj-jeden? true) (def odejmij-jeden? false) (cond-&gt; 123 ; rezultat 123 dodaj-jeden? inc ; rezultat (inc 123) =&gt; 124 odejmij-jeden? dec ; brak wartościowania true println ; rezultat (println 124) =&gt; nil ) ; =&gt; nil ; &gt;&gt; 124

Przewlekanie warunkowe, cond->>

Makro warunkowego przewlekania ostatniego argumentu działa podobnie do cond->, ale – jak wskazuje nazwa – dochodzi do podstawiania wartości ostatnio przeliczanego S-wyrażenia w miejsce ostatniego argumentu wyrażenia aktualnie przeliczanego.

Warunkowe przewlekanie ostatniego argumentu przydaje się do czytelnego wyrażenia transformacji dokonywanych na kolekcjach, ponieważ wiele wbudowanych funkcji operujących na tego typu strukturach danych przyjmuje je jako ostatnie argumenty.

Użycie:

  • (cond->> wyrażenie & para…),

gdzie para to:

  • predykat wyrażenie.

Makro cond->> przyjmuje wyrażenie początkowe oraz pary S-wyrażeń, gdzie pierwsze jest predykatem, a drugie będzie wartościowane w taki sposób, że jako pierwszy argument będzie wstawiony rezultat wartościowania poprzedniego wyrażenia z łańcucha. Wartością zwracaną jest wartość ostatnio obliczonego S-wyrażenia.

Przykład użycia makra cond->
 1(def parzyste?     true)
 2(def nieparzyste? false)
 3
 4(cond->> [1 2 3 4]                   ; [1 2 3 4]
 5         parzyste?    (filter even?) ; (filter even? [1 2 3 4]) => (2 4)
 6         nieparzyste? (filter odd?)  ; brak wartościowania
 7         true         println        ; (println '(2 4)) => nil
 8)
 9
10; => nil
11; >> (2 4)
(def parzyste? true) (def nieparzyste? false) (cond-&gt;&gt; [1 2 3 4] ; [1 2 3 4] parzyste? (filter even?) ; (filter even? [1 2 3 4]) =&gt; (2 4) nieparzyste? (filter odd?) ; brak wartościowania true println ; (println &#39;(2 4)) =&gt; nil ) ; =&gt; nil ; &gt;&gt; (2 4)

Wiązanie z nazwą, as->

Makro as-> służy do przewlekania rezultatów wyrażeń, ale zamiast ustalonej pozycji mamy do czynienia z wytwarzaniem powiązań z symboliczną nazwą. Przydaje się to w sytuacjach, gdy różne wywołania wymagają umieszczenia istotnego argumentu w różnych miejscach.

Użycie:

  • (as-> wyrażenie symboliczna-nazwa & wyrażenie…).

Argumentami makra jest wyrażenie początkowe, którego wartość będzie powiązana z symboliczną nazwą (podaną jako drugi argument), a następnie przewlekana przez podane dalej S-wyrażenia w taki sposób, że wartość każdego z nich będzie powiązana z symboliczną nazwą przed wywołaniem kolejnego.

Przykład użycia makra as->
1(as-> [1 2 3 4] wynik       ; po podstawieniu wyniku:
2      (filter even? wynik)  ; (filter even? [1 2 3 4])
3      (last wynik)          ; (last [2 4])
4      [wynik (inc wynik)])  ; [4 5]
5
6; => [4 5]
(as-&gt; [1 2 3 4] wynik ; po podstawieniu wyniku: (filter even? wynik) ; (filter even? [1 2 3 4]) (last wynik) ; (last [2 4]) [wynik (inc wynik)]) ; [4 5] ; =&gt; [4 5]

Podsumowanie

Tłumaczenie kodu źródłowego

Poniższy diagram przedstawia poszczególne etapy tłumaczenia kodu źródłowego programu z postaci tekstowej na wykonywalną, włączając proces uruchamiania i uzyskiwania wartości wyrażeń.

Translacja programu do postaci wykonywalnej w Clojue

Pytania i odpowiedzi

Odpowiedzmy sobie na kilka pytań, które pozwolą podsumować wprowadzenie do podstawowych mechanizmów obecnych w Lispach (i w Clojure):

  • Jaka jest różnica między S-wyrażeniem a listą?

    S-wyrażenie jest tekstowym zapisem danych symbolicznych. Lista to sekwencyjna struktura danych w pamięci. Odczytanie S-wyrażenia ujętego w nawiasy zwykle wytwarza listę, która może być później wartościowana jako forma albo przetwarzana jak zwykłe dane.

  • Jaka jest różnica między S-wyrażeniem a atomem?

    W tradycyjnej terminologii Lispu atom jest niezłożonym S-wyrażeniem. Nie należy go mylić z typem referencyjnym Atom w Clojure.

  • Jaka jest różnica między S-wyrażeniem a formą?

    Forma jest wartością Clojure przedstawioną do wartościowania. Mogła powstać z S-wyrażenia odczytanego z tekstu albo zostać skonstruowana bezpośrednio jako dane.

  • Jaka jest różnica między symbolem a zmienną globalną?

    Obiekt przestrzeni nazw odwzorowuje symbol na Var. Var udostępnia powiązanie główne, a gdy dozwolone jest dynamiczne przepowiązanie — ewentualnie bieżące powiązanie wątku. Symbol nazywa Var, ale sam nie przechowuje odwołania do jego wartości.

Zobacz także:

Jesteś w sekcji Poczytaj mi Clojure
Tematyka:

Taksonomie: