Tutorial zakłada, że znasz Javę i dowolny język skryptowy.

Clojure jest językiem funkcyjnym. To tak jakby uczyć się programowania od nowa. Na początku może się wydawać, że w języku brakuje podstawowych konstrukcji. Z czasem wszystko staje się klarowne. Dlatego sugeruję uzbroić się w odrobinę cierpliwości.

Zacznijmy od wprawek w trybie interaktywnym.

Instalacja Clojura i rozpoczęcie pracy

Ściągnij najnowszego stabilnego Clojura z Google Code i rozpakuj archiwum. Cały język mieści się w jednym pliku clojure.jar. Pozostałe pliki to źródła, licencja itp. Z konsoli wejdź do katalogu z plikiem clojure.jar. Uruchom Clojura w trybie interaktywnym (REPL):

D:\clojure> java -cp clojure.jar clojure.main
Clojure
user=>

Wydaj pierwsze polecenie (fragment „user⇒” to znak zachęty):

user=> (println "Witaj, Clojure!")
Witaj, Clojure!
nil

Mamy XXI wiek. Spróbujmy zatem wyświetlić klasyczne powitanie w ładnym okienku:

(. javax.swing.JOptionPane (showMessageDialog nil "Witaj, Clojure!"))

Składnia

W Clojurze wszystko jest wyrażeniem, czyli ma wartość. Wyrażenia zapisujemy w postaci listy. Pierwszym elementem listy jest nazwa funkcji, kolejnymi jej argumenty:

(nazwa_funkcji arg1 arg2 arg3 arg4 ...)

Przykład 1:

user=> (* 3 8)
24

Funkcja (*) zwróciła wartość 24.

Przykład 2:

user=> (println "ala ma kota")
"ala ma kota"
nil

Funkcja (println) wyświetliła tekst "ala ma kota" i zwróciła wartość nil. Wyświetlenie tekstu to tzw. skutek uboczny. Skutek uboczny to każde działanie funkcji, które modyfikuje świat zewnętrzny inaczej niż poprzez zwróconą wartość.

Przykład 3:

(* 5 4 3 2 1)
=> 120

Jak widać, wielu funkcjom, np. mnożeniu, możemy przekazać dowolną liczbę argumentów. Podobnie jak w innych językach, wyrażenia można zagnieżdżać:

(println "Wynik mnożenia:" (* 3 2) )
Wynik mnożenia: 6

Przejrzyj teraz uważnie pozostałe kluczowe elementy składni:

; to jest komentarz
tekst ; identyfikator (w oryginale: "symbol"), który musiał być wcześniej zdefiniowany - np. nazwa funkcji lub stałej
"tekst" ; literał napisowy, wewnętrznie String
125 ; literał liczbowy, wewnętrznie Integer lub BigInteger (auto konwersja)
3.99 ; literał liczbowy, wewnętrznie Double
\a ; literał znakowy, wewnętrznie Character
true, false ; literał logiczny, wewnętrznie Boolean
:keyword ; słowo kluczowe (w oryginale: "keyword"), używany głównie jako klucz w słownikach, w Ruby mówi się na to symbol
("a" "b" "c") ; lista i jednocześnie wyrażenie, pierwszy el. to wywołanie funkcji, następne to jej argumenty
'("a", "b", "c") ; lista, w której pierwszy element nie będzie traktowany jako wywołanie funkcji
["a" "b" "c"] ; wektor, wewnętrznie LazilyPersistentVector
["a", "b", "c"] ; wektor (można dodać przecinki dla czytelności)
{:a 1 :b 2 :c 3} ; słownik, wewnętrznie PersistentArrayMap
{:a 1, :b 2, :c 3} ; słownik (można dodać przecinki dla czytelności)
#{"a" "b" "c"} ; zbiór, wewnętrznie PersistentHashSet
#{"a", "b", "c"} ; zbiór (można dodać przecinki dla czytelności)

Wołanie metod Javy

Clojure nie wstydzi się Javy. Zasadniczo nie opakowuje jej bibliotek. Zamiast tego oferuje wygodną składnię do bezpośredniego wołania javowych metod.

(.toLowerCase "HELLO WORLD")
"hello world"

Takie wywołanie jest szybkie i idiomatyczne. Częste korzystanie z bibliotek Javy jest w Clojurze całkowicie naturalne.

Jeśli chcemy zawołać kilka metod łańcuchowo, można skorzystać z cukierka składniowego:

// Java
"hello".getClass().getProtectionDomain()
; Clojure
(.. "hello" getClass getProtectionDomain)

Niektóre najczęściej wykorzystywane biblioteki Javy są już opakowane w Clojurze dla wygodniejszego i bardziej idiomatycznego użytku (np. IO).

Instalacja NetBeans i zbudowanie projektu

Ściągnij i zainstaluj najnowszy NetBeans. Następnie ściągnij wtyczkę enclojure, zapisz ją w dowolnym katalogu - np. w instalacji NetBeans. Uruchom NetBeans, wybierz Tools/Plugins. W zakładce Downloaded dodaj wszystkie elementy enclojure. Teraz w zakładce Available Plugins będzie można je zainstalować.

Wtyczka enclojure zawiera samego Clojura (obecnie Clojure-1.0.0), ale w ogólnym przypadku może to być wersja przestarzała. Podmień zatem wszystkie wystąpienia „wtyczkowego” clojure.jar na najnowszą wersję pliku, którą ściągnąłeś na początku tutoriala.

Wtyczka enclojure jest w wersji beta. Jest wystarczająco dojrzała, żeby była użyteczna, ale należy liczyć się z pewnymi niedoskonałościami.

Tworzymy nowy projekt: File/New project. Wybieramy Clojure. Zostaw domyślne ustawienia. Jeśli zdecydujesz się je zmienić - np. nazwę pakietu - to po utworzeniu projektu trzeba te zmiany wprowadzić także w Main.java i defpackage.clj

Zbuduj projekt (Shift + F11).

Uruchom aplikację (F6).

Na konsoli IDE pojawi się wynik działania programu (Hello World!).

Zajrzyj do Main.java - to jest wraper uruchamiający Clojura. Kod Clojura możesz teraz pisać w pliku *.clj

Od tej pory wszystkie eksperymenty robimy w pliku *.clj. Fantastyczny skrót F6 zapisze bieżącą wersję pliku, zbuduje projekt i grzecznie odpali program.

Definiowanie funkcji

Zdefiniujmy funkcję obliczającą pole koła:

(defn circle-area [r]
(println "Zawołana funkcja circle-area z argumentem" r)
(* r r (. PI Math)))

(println (circle-area 3))

; Zawołana funkcja circle-area z argumentem 3
; 28.274333882308138

Zauważamy, że:

  • (defn) to „funkcja” do definiowania funkcji (w uproszczeniu; ale o tym innym razem).
  • [r] to wektor argumentów funkcji, ogólnie: [arg1 arg2 arg3 …].
  • Funkcja zwraca wartość ostatniego wyrażenia.
  • Operatory matematyczne mogą przyjmować dowolną liczbę argumentów, np. tu mnożenie przyjmuje 3 argumenty.
  • Dobry edytor z podświetlaniem pary nawiasów jest kluczowy dla komfortowej pracy.

Definiowanie "zmiennych" (bindings)

Jak wiesz, String w Javie jest niezmienny (immutable). Każda operacja „na nim” powoduje utworzenie nowego Stringa. Stary pozostaje niezmieniony.

To bardzo cenna cecha - dlatego w Clojurze dotyczy wszystkich struktur danych. Stringi, listy, wektory, słowniki - wszystkie są niezmienne.

W tej sytuacji mówienie o zmiennych byłoby niezręczne. Mówimy zatem o wiązaniach (bindings). Symbol ma „przywiązaną” wartość.

Wiązania globalne tworzymy z wykorzystaniem „funkcji” (def).

(def pi 3.14) ; Symbol pi jest globalnie przywiązany do liczby 3.14

(defn foo []
(def g "Wartość")) ; Symbol g jest **globalnie** przywiązany do Stringa "Wartość"

(def printer ; Symbol printer jest globalnie przywiązany do funkcji
(fn [s]
(println s)))

; Powyższe jest równoważne:
(defn printer []
(println s))

Wiązań globalnych (w ramach swojej przestrzeni nazw) używa się głównie do definiowania funkcji i stałych.

Wiązania lokalne tworzymy „funkcją” (let):

(defn square-corners [bottom left size]
(let [top (+ bottom size)
right (+ left width)]
[[bottom let] [top left] [top right] [bottom right]]))

W powyższym przykładzie symbole top i right są lokalnie przywiązane do obliczonych wartości. Zasięg tych symboli kończy się wraz z końcem (let).

Wyrażenie if na przykładzie silnii

Zaczniemy od klasycznej definicji silni.

(defn factorial-classical [n]
(if (zero? n)
1
(* n (factorial-classical (- n 1)))))

W tej implementacji fajne jest to, że odpowiada 1:1 definicji matematycznej:

0! == 1
n! == n * (n-1)!

Zwróć uwagę na konstrukcję if. Przyjmuje ona 3 argumenty: warunek, wyrażenie gdy spełniony, wyrażenie gdy niespełniony (opcjonalne).

W Clojurze nie jest to jednak rozwiązanie optymalne. Ze względu na ograniczenia JVM, w tej sytuacji rekurencja nie będzie ogonowa. Oznacza to, że stos będzie rósł przy każdym wywołaniu. Do zapewnienia rekurencji ogonowej, Clojure posiada mechanizm loop+recur, który wyjaśnimy przy lepszej okazji.

Tymczasem idiomatyczna silnia wygląda tak:

(defn factorial [n]
(apply * (range 1 (inc n))))

Kod wyjaśnimy rozbijając go na czynniki pierwsze:

(inc 4)   ; => 5
(range 1 5) ; => [1 2 3 4]
(apply * [1 2 3 4]) ; równoważne 1 * 2 * 3 * 4

„Funkcji” apply używamy, gdy chcemy wywołać jakąś funkcję (tu *) z dynamicznie utworzoną listę argumentów. W tym przypadku liczba argumentów mnożenia zależy od n. „Funkcja” apply przypomina zatem rozpakowanie listy znane z języków skryptowych.

Formatowanie tekstu na przykładzie tabliczki mnożenia

Chcemy wyświetlić na ekranie tabliczkę mnożenia o rozmiarze n.

(defn print-multiplication-table [n]
  (dotimes [x n]
    (dotimes [y n]
      (print (format "%3d " (* (inc x) (inc y)))))
    (println)))
(print-multiplication-table 6)

; =>  
 1   2   3   4   5   6  
 2   4   6   8  10  12 
 3   6   9  12  15  18 
 4   8  12  16  20  24 
 5  10  15  20  25  30 
 6  12  18  24  30  36 

W tym przykładzie warto zwrócić uwagę na dwie rzeczy. Funkcja (dotimes [i K] (expression)) zawoła wyrażenie expression K razy z licznikiem i. Funkcja (format „łańcuch formatujący” arg1 arg2 arg3 …) zwróci Stringa sformatowanego przy pomocy znanych z Javy „tricków” z procentami (%s, %d itd).