Clojure (вимовляється приблизно кложер, так само як англ. closure — Замикання) — сучасний діалект мови програмування Lisp. Це мова загального призначення, що підтримує інтерактивну розробку, зорієнтовану на функціональне програмування, спрощує багатопотокове програмування, та містить риси сучасних скриптових мов.

Clojure
Парадигма функціональна, мультипарадигматична
Дата появи 2007
Творці Rich Hickey
Розробник Річард Хіккі
Останній реліз 1.11.1 (5 квітня 2022; 2 роки тому (2022-04-05))
Система типізації динамічна типізація, сувора типізація
Під впливом від Common Lisp, Scheme, Prolog, ML, Haskell, Erlang[1]
Мова реалізації Java[2]
Платформа JVM, CLR
Ліцензія Eclipse Public License
Звичайні розширення файлів .clj, .cljs, .cljc, .edn або .cljr
Репозиторій вихідного коду github.com/clojure/clojure
Вебсайт Офіційний сайт

Clojure працює на Java Virtual Machine і Common Language Runtime. Як і інші Lisp-подібні мови, Clojure розглядає код як дані і має потужну систему макросів.

Вихідний код компілятора Clojure, бібліотек і runtime-компонентів розповсюджується в рамках ліцензії Eclipse Public License.

Філософія ред.

Річард Гіккі розробив Clojure, оскільки йому був потрібен сучасний Lisp для функціонального програмування, розрахований на інтеграцію з розповсюдженою платформою Java й розроблений для паралельного програмування.[3][4]

Підхід Clojure до паралельності характеризується концепцією тотожностей,[5] що представляють серію незмінних станів протягом часу. Оскільки стани є незмінними значеннями, будь-яка кількість обробників може паралельно обробляти їх, і конкуренція зводиться до питання керування змінами від одного стану до іншого. З цією метою, Clojure надає декілька типів змінюваних посилань, кожен з яких має добре визначену семантику переходу між станами.

Синтаксис ред.

Як і інші Lisp-подібні мови, синтаксис Clojure побудовано на S-виразах (англ. S-expression), які в процесі синтаксичного розбору спершу перетворюються на структури даних за допомогою функції-читача (англ. reader), перш ніж компілюються. Clojure's reader підтримує літеральний синтаксис для хеш-таблиць, множин та векторів на додаток до списків, і вони передаються компілятору як є. Іншими словами, компілятор Clojure компілює не лише спискові структури даних, але й безпосередньо підтримує всі названі вище типи. Clojure — Lisp-1, і не є сумісним за кодом з іншими діалектами мови Lisp.

Макроси ред.

Система макросів Clojure дуже схожа на використовувану в Common Lisp, з тією відмінністю, що версія синтаксичного цитування Clojure (з використанням знаку `) доповнює символи їхніми просторами імен. Це допомагає запобігти ненавмисному перехопленню імен, оскільки прив'язка до імен, доповнених простором імен, заборонена. Є можливість форсувати таке захоплення імен, але це має бути зроблено явно. Clojure також не дозволяє переприв'язку глобальних імен з інших просторів імен, які були імпортовані в поточний простір.

Можливості мови ред.

Варіанти ред.

Реалізації Clojure для відмінних від JVM платформ:

Приклади ред.

Hello world!:

(println "Привіт, світе!")

Hello World з графічним інтерфейсом користувача:

(javax.swing.JOptionPane/showMessageDialog nil "Привіт, світе!" )

Thread-безпечний генератор унікальних серійних номерів:

(let [i (atom 0)]
  (defn generate-unique-id
    "Повертає унікальний числовий ID при кожному виклику."
    []
    (swap! i inc)))

Анонімний субклас java.io.Writer, що не пише нікуди, і макрос, що використовує це для відключення друку в своїх межах:

(def bit-bucket-writer
  (proxy [java.io.Writer] []
    (write [buf] nil)
    (close []    nil)
    (flush []    nil)))

(defmacro noprint
  "Обчислює задані вирази з відключеним друком в *out*."
  [& forms]
  `(binding [*out* bit-bucket-writer]
     ~@forms))

(noprint
 (println "Привіт, ніхто!"))

10 тредів, що маніпулюють однією спільною структурою даних, яка складається з 100 векторів, кожен з яких містить 10 (початково послідовних) унікальних чисел. Кожен тред багато разів обирає дві випадкові позиції у двох випадкових векторах і обмінює їх. Усі зміни векторів відбуваються в транзакціях шляхом використання системи програмної пам'яті транзакцій Clojure. Ось чому навіть після 100 000 ітерацій кожного треда жодне число не втрачено.

(defn run [nvecs nitems nthreads niters]
  (let [vec-refs (vec (map (comp ref vec)
                           (partition nitems (range (* nvecs nitems)))))
        swap #(let [v1 (rand-int nvecs)
                    v2 (rand-int nvecs)
                    i1 (rand-int nitems)
                    i2 (rand-int nitems)]
                (dosync
                 (let [temp (nth @(vec-refs v1) i1)]
                   (alter (vec-refs v1) assoc i1 (nth @(vec-refs v2) i2))
                   (alter (vec-refs v2) assoc i2 temp))))
        report #(do
                 (prn (map deref vec-refs))
                 (println "Distinct:"
                          (count (distinct (apply concat (map deref vec-refs))))))]
    (report)
    (dorun (apply pcalls (repeat nthreads #(dotimes [_ niters] (swap)))))
    (report)))

(run 100 10 10 100000)

Вивід попереднього прикладу:

([0 1 2 3 4 5 6 7 8 9] [10 11 12 13 14 15 16 17 18 19] ...
 [990 991 992 993 994 995 996 997 998 999])
Distinct: 1000
 
([382 318 466 963 619 22 21 273 45 596] [808 639 804 471 394 904 952 75 289 778] ...
 [484 216 622 139 651 592 379 228 242 355])
Distinct: 1000

Ханойська вежа. Функція, що генерує розв'язок задачі у вигляді лінивої послідовності, та форматований вивід результату:

(defn lazy-hanoi [n a b c]
  (lazy-seq
   (when-not (zero? n)
     (lazy-cat (lazy-hanoi (dec n) a c b)
               [[a b]]
               (lazy-hanoi (dec n) c b a)))))

(defn print-hanoi [n a b c]
  (doseq [[from to] (lazy-hanoi n a b c)] 
         (printf "%s -> %s;\t" from to)))

Макрос, що здійснює обмін двох глобальних змінних. При кожному виклику макроса замість temp# підставляється новий унікальний ідентифікатор.

(defmacro swap [a b] 
   `(do (def temp# ~a) 
        (def ~a ~b) 
        (def ~b temp#)))

Відмінності від інших діалектів Lisp ред.

  • Clojure — мова, чутлива до регістру символів
  • Clojure — Lisp-1 (для змінних та функцій використовується спільний простір імен — так само як у Scheme, але не в Common Lisp)
  • () — не те ж саме, що й nil
  • Рідер вільний від побічної дії.
  • Ключові слова не є символами.
  • Символи не є місцем зберігання даних (див. змінні).
  • nil — не символ.
  • t нема у синтаксисі, його функцію виконує true
  • Таблиця читання зараз[коли?] недоступна для користувацьких програм.
  • let здійснює зв'язування послідовно.
  • do — не оператор циклу
  • Хвостова рекурсія не підтримується, для її заміни можна використати recur (якщо процедура викликає сама себе) чи trampoline (якщо декілька процедур викликають одна одну).
    • recur являє собою перехід на початок процедури чи циклу з присвоєнням нових значень параметрів. Обов'язково має бути у «хвостовій» позиції, що контролюється транслятором.
    • trampoline — функція, що забезпечує взаємний виклик функцій без переповнення стеку. Функції, що взаємодіють через trampoline, не викликають одна одну безпосередньо, а повертають функцію-замикання, що містить виклик. Якщо результатом функції є функція, trampoline викликає її, повторює цикл вже з її результатом і т. д., якщо ж результат має інший тип, trampoline завершує роботу і повертає його.
  • Синтаксичне цитування (syntax-quote, оператор `) здійснює розв'язку символів, доповнюючи їх просторами імен, тому `x — не те ж саме, що 'x.
  • Синтаксичні цитати дозволяють автоматично генерувати символи.
  • ~ (тильда) — розцитовування (unquote), тоді як ',' (кома) вважається пробілом.
  • Існує синтаксис на рівні рідера для асоціативних таблиць (maps), множин та векторів.
  • cons, first та rest маніпулюють з абстракцією послідовності, необов'язково з елементами cons.
  • Більшість структур даних — незмінювані.
  • Замість lambda використовується fn, що може мати декілька методів.
  • = — предикат рівності (equality), що може порівнювати не лише числа, а й інші об'єкти.
  • Всі (глобальні) змінні можна динамічно переприв'язувати без конфлікту з лексичними локальними прив'язками. Спеціальні оголошення для розрізнення динамічних та лексичних прив'язок — не потрібні . Оскільки Clojure — Lisp-1, (глобальні) функції можуть динамічно переприв'язуватись (перевизначатись).
  • Нема letrec, labels чи flet — для посилання на себе використовується (fn ім'я [аргументи]…), для взаємних посилань — letfn.
  • У Clojure nil позначає відсутність значення будь-якого типу і не прив'язаний до списків чи послідовностей.
  • Порожні колекції не є еквівалентом nil. Clojure не прирівнює nil до '().
  • false — одне з двох можливих булевих значень, інше — true
  • Серед колекцій є не лише списки. Можуть існувати порожні колекції, деякі з них підтримуються літералами ([], {} та ()). Тому не може бути якогось сторожового значення порожньої колекції.
  • Порівнюючи зі Scheme, nil можна розглядати як аналог #f.
  • Найбільша відмінність Clojure — послідовності. Це не якийсь окремий тип колекцій, особливо враховуючи, що їм необов'язково бути саме списками. При спробі отримати з порожньої колекції послідовність її елементів (викликом seq) повертається nil. При спробі отримати з послідовності (на її останньому елементі) залишок (rest) буде повернуто іншу логічну послідовність. Перевірити, чи ця послідовність порожня, можна шляхом виклику seq. Це дозволяє послідовностям та протоколу послідовностей бути лінивими.
  • Деякі функції для роботи з послідовностями відповідають функціям зі Scheme та CL, що маніпулюють лише з парами/cons ('списками') і повертають сторожове значення ('() або nil), яке представляє 'порожній' список. Їх результат у Clojure відрізняється тим, що повертається не специфічна порожня колекція, а інша логічна послідовність. Частина функцій для роботи з послідовностями не мають відповідників у Scheme/CL і являють собою Haskell/ML-подібні функції. Деякі з них повертають нескінченні або обчислювані послідовності, де аналогія з конкретними структурами даних, такими як списки у Scheme/CL, лише приблизна.
  • Це допомагає відокремити колекції/структури даних від послідовностей/ітерацій. Як у CL, так і у Scheme вони об'єднані, у Clojure — відокремлені.

Примітки ред.

  1. Rich Hickey (30 червня 2009). Books that influenced Clojure. Архів оригіналу за 18 квітня 2012. Процитовано 11 вересня 2009.
  2. The clojure Open Source Project on Open Hub: Languages Page — 2006.
  3. Rationale. Rich Hickey. clojure.org. Архів оригіналу за 18 квітня 2012. Процитовано 17 жовтня 2008.
  4. Архівована копія. Архів оригіналу за 9 листопада 2017. Процитовано 1 березня 2011.{{cite web}}: Обслуговування CS1: Сторінки з текстом «archived copy» як значення параметру title (посилання)
  5. On State and Identity. Rich Hickey. clojure.org. Архів оригіналу за 12 липня 2013. Процитовано 1 березня 2010.
  6. clojure/clojure-clr · GitHub. Github.com. Архів оригіналу за 25 серпня 2013. Процитовано 28 червня 2012.
  7. clojure/clojurescript · GitHub. Github.com. Архів оригіналу за 18 травня 2014. Процитовано 28 червня 2012.
  8. aemoncannon (30 грудня 2010). Home · aemoncannon/las3r Wiki · GitHub. Github.com. Архів оригіналу за 2 січня 2015. Процитовано 28 червня 2012.
  9. halgari/clojure-py · GitHub. Github.com. Архів оригіналу за 23 січня 2016. Процитовано 10 липня 2012.
  10. rouge-lang/rouge · GitHub. Github.com. Процитовано 25 січня 2013.[недоступне посилання з лютого 2019]

Література ред.

Посилання ред.