Files
dimgigov 23252826a0 feat: add Clojure/Nim chapter to the book + restructure for GitLab
- New chapter 05-clojure-nim.md (EN + BG) covering:
  - Native compilation pipeline (Clojure → Nim → C → binary)
  - AI-powered development (error explanation, code generation)
  - JSON REPL for AI agents
  - loop/recur with real TCO
  - Cross-compilation: JS, shared libs, WASM
  - Persistent data structures (HAMT)
  - Concurrency: atoms, agents, channels
- Updated book README.md with Clojure/Nim focus
- Added Clojure/Nim terms to subject indices (EN + BG)
- Removed books/ from .gitignore so it can be pushed to GitLab
2026-05-08 23:32:11 +03:00

58 KiB
Raw Permalink Blame History

Чист Clojure: Изчерпателно Ръководство

Съдържание

  1. Въведение в Clojure
  2. Първи стъпки
  3. Основен синтаксис и форми
  4. Структури от данни
  5. Функции
  6. Управление на потока
  7. Серии и мързеливо оценяване
  8. Деструктуриране
  9. Пространства от имена
  10. Макроси
  11. Конкурентност
  12. Протоколи и записи
  13. Многометодни функции
  14. Тестване
  15. REPL
  16. Core.async
  17. Добри практики
  18. Индекс

1. Въведение в Clojure

1.1 Какво е Clojure?

Clojure е съвременен, динамичен и функционален език за програмиране, който работи на Java Virtual Machine (JVM), .NET Common Language Runtime (CLR) и JavaScript двигатели чрез ClojureScript. Създаден от Rich Hickey през 2007 г., Clojure е диалект на Lisp, който набляга на неизменяемостта, функционалното програмиране и кратката изразителност.

1.2 Ключови характеристики на Clojure

1.2.1 Неизменяемост по подразбиране

В Clojure всички структури от данни са неизменяеми по подразбиране. Когато "модифицирате" структура от данни, вие всъщност създавате нова версия с желаните промени, докато оригиналът остава непроменен. Този подход води до по-безопасни конкурентни програми и по-чист код.

(def original [1 2 3])
(def modified (conj original 4))
;; original => [1 2 3]
;; modified => [1 2 3 4]

1.2.2 Функционално програмиране

Clojure насърчава чисти функции без странични ефекти. Функциите са обекти от първи клас, които могат да бъдат предавани като аргументи, връщани от други функции и композирани заедно.

(def double (partial * 2))
(def add-ten (partial + 10))
(def transform (comp double add-ten))
(transform 5) ;; => 30

1.2.3 Наследство от Lisp

Като диалект на Lisp, Clojure наследява силата на макросите и еднаквото представяне на код като данни. Всичко е израз, който връща стойност, а синтаксисът е прост и последователен.

1.2.4 Полиморфизъм по време на изпълнение

Clojure предоставя множество механизми за полиморфизъм:

  • Протоколи: Дефинират сигнатури на методи за типове данни
  • Многометодни функции: Диспечират въз основа на произволни критерии
  • Записи: Конкретни типове данни, които имплементират протоколи

1.3 Защо Чист Clojure?

Въпреки че Clojure работи на JVM и има отлична Java интеграция, тази книга се фокусира върху чист Clojure — основните функции на езика, които не разчитат на Java интеграция. Този подход:

  • Учи фундаменталните концепции на Clojure
  • Прави кода преносим (включително ClojureScript)
  • Насърчава мисленето в парадигмата на Clojure
  • Избягва ненужно смесване на парадигми

1.4 Философията на Clojure

Clojure се придържа към няколко принципа:

  1. Простота: Трудните неща трябва да бъдат прости, простите неща трябва да бъдат тривиални
  2. Неизменяемост: Предпочитайте неизменяеми структури от данни за безопасност и конкурентност
  3. Абстракция: Изграждайте слоеве на абстракция за управление на сложността
  4. Ориентираност към изрази: Всичко е израз, който връща стойност

2. Първи стъпки

2.1 Инсталация

2.1.1 Използване на CLI инструменти

Препоръчителният начин за инсталиране на Clojure е чрез официалния CLI инструмент:

Linux/macOS:

curl -O https://download.clojure.org/install/linux-install-1.11.1.1202.sh
chmod +x linux-install-1.11.1.1202.sh
./linux-install-1.11.1.1202.sh

macOS (Homebrew):

brew install clojure

2.1.2 Ръчна инсталация

Изтеглете Clojure JAR файла и го използвайте директно:

java -jar clojure-1.11.1.jar

2.2 Вашият първи Clojure проект

2.2.1 Създаване на проект с deps.edn

Създайте нова директория за вашия проект и добавете файл deps.edn:

{:deps {org.clojure/clojure {:mvn/version "1.11.1"}}}

Стартирайте Clojure:

clj -M

2.2.2 Основи на REPL

REPL (Read-Eval-Print Loop) е вашата основна среда за разработка:

user=> (+ 1 2 3)
6
user=> (println "Hello, Clojure!")
Hello, Clojure!
nil
user=> (def message "Hello, World!")
#'user/message
user=> message
"Hello, World!"

2.3 Настройка на редактора

Популярни редактори за Clojure разработка:

  • VS Code: Разширение Calva
  • Emacs: Режим CIDER
  • Vim/Neovim: Добавка Conjure
  • IntelliJ: Добавка Cursive

3. Основен синтаксис и форми

3.1 S-Изрази

Кодът на Clojure се пише като s-изрази (символни изрази), които са вложени списъци:

(оператор операнди...)

Първият елемент е операторът (функция, макрос или специална форма), а останалите са операнди.

(+ 1 2)        ;; Събиране: 3
(* 2 3 4)       ;; Умножение: 24
(< 1 2 3)       ;; Сравнение: true
(and true false) ;; Логическо И: false

3.2 Данни като код

В Lisp данните и кодът са едно и също нещо. Това означава, че можете да манипулирате програмите си като данни:

'(+ 1 2 3)  ;; Цитиран списък: (+ 1 2 3)
(list + 1 2) ;; Списък със + функцията и числа

3.3 Специални форми

Специалните форми са примитиви, които не могат да бъдат изразени като функции, защото имат специални правила за оценяване.

3.3.1 def

Дефинира глобална променлива:

(def x 10)
(def name "Clojure")
(def items [1 2 3])

3.3.2 let

Създава локални свързвания:

(let [x 10
      y 20]
  (+ x y))  ;; => 30

3.3.3 if

Условно изпълнение:

(if условие
  тогава-израз
  else-израз)

3.3.4 quote

Предотвратява оценяване:

(quote (+ 1 2))  ;; => (+ 1 2)
'(+ 1 2)         ;; Кратка форма: (+ 1 2)

3.4 Правила за оценяване

  1. Числа, низове, булеви стойности, nil и ключови думи се оценяват като себе си
  2. Символи се оценяват като стойността на променливата, която именуват
  3. Списъци се оценяват като извиквания на функции (ако първият елемент е извикаем)
  4. Цитираните изрази предотвратяват оценяване

3.5 Коментари

;; Едноредов коментар

;; Многоредов коментар
;; (няма специален многоредов синтаксис,
;;  просто използвайте няколко едноредови коментара)

(comment
  "Това е коментар блок, който няма да бъде оценен"
  (+ 1 2))

3.6 Интервали и форматиране

  • Clojure е безразличен към интервалите (с изключение в рамките на символи)
  • Стандартна конвенция: една интервал след отваряща скоба, преди затваряща
  • Подравнявайте аргументите вертикално за четливост:
(do-something arg1
              arg2
              arg3)

4. Структури от данни

Clojure предоставя богат набор от неизменяеми структури от данни. Разбирането им е фундаментално за писане на идиоматичен Clojure.

4.1 Числа

4.1.1 Целочислени типове

42        ;; Десетично
017       ;; Осмично (15)
0x2A      ;; Шестнадесетично (42)
2r101010  ;; Двоично (42)

4.1.2 Числа с плаваща запетая

3.14
6.022e23

4.1.3 Рационални числа

Clojure запазва точността с рационални числа:

1/3        ;; Тип рационално
22/7       ;; Приближение на pi
(/ 1 3)    ;; 1/3

4.2 Низове

"Hello, World!"
"Multi-line
string"

;; Конкатенация
(str "Hello" " " "World")  ;; => "Hello World"

;; Подниз
(subs "Hello" 0 5)  ;; => "Hello"

;; Функции за низове
(count "Clojure")   ;; => 7
(reverse "Clojure") ;; => "erujolC"

4.3 Символи (Characters)

\a      ;; Символ a
\newline ;; Нов ред
\space  ;; Интервал

4.4 Булеви стойности

true
false
nil     ;; Представлява отсъствие на стойност

Правила за истинност:

  • Всичко с изключение на false и nil е истина
  • and, or, if, when използват това правило

4.5 Ключови думи (Keywords)

Ключовите думи са интернирани низове, използвани като идентификатори, често за ключове в map-ове:

:foo
:bar
:user/name   ;; Пространство от имена на ключова дума
::local-key  ;; Автоматично с пространство от имена

Ключовите думи се оценяват като себе си и могат да се използват като функции за търсене на стойности в map-ове.

4.6 Символи (Symbols)

Символите се оценяват като променливите, които именуват:

'x          ;; Символ x (цитиран)
(def x 10)  ;; Дефинира променлива x със стойност 10
x           ;; Оценява се до 10

4.7 Списъци (Lists)

Списъците са свързани списъци, ефективни за последователен достъп в началото:

'(1 2 3)              ;; Цитирайте, за да предотвратите оценяване
(list 1 2 3)         ;; Създава списък
'(+ 1 2)             ;; Списък съдържащ символа +

;; Достъп
(first '(1 2 3))     ;; => 1
(second '(1 2 3))    ;; => 2
(rest '(1 2 3))      ;; => (2 3)
(nth '(1 2 3) 0)      ;; => 1

;; Модификация (връща нов списък)
(cons 0 '(1 2 3))    ;; => (0 1 2 3)
(concat '(1 2) '(3 4)) ;; => (1 2 3 4)

4.8 Вектори (Vectors)

Векторите са индексирани колекции, ефективни за случаен достъп:

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

;; Достъп
(get [10 20 30] 1)      ;; => 20
([10 20 30] 1)          ;; => 20 (достъп като с ключова дума)
(first [1 2 3])         ;; => 1
(second [1 2 3])        ;; => 2
(last [1 2 3])          ;; => 3

;; Модификация (връща нов вектор)
(conj [1 2] 3)          ;; => [1 2 3]
(pop [1 2 3])           ;; => [1 2]
(assoc [1 2 3] 1 20)    ;; => [1 20 3]
(subvec [1 2 3 4 5] 1 3) ;; => [2 3]

4.9 Map-ове (Maps)

Map-овете са асоциативни структури ключ-стойност:

{:name "Alice" :age 30}
(hash-map :a 1 :b 2 :c 3)
(assoc {:a 1} :b 2)      ;; => {:a 1 :b 2}
(dissoc {:a 1 :b 2} :a) ;; => {:b 2}
(get {:a 1} :a)         ;; => 1
({:a 1} :a)             ;; => 1
(:a {:a 1})             ;; => 1 (ключовите думи са функции!)

;; Вложен достъп
(get-in {:user {:address {:city "Sofia"}}}
         [:user :address :city])  ;; => "Sofia"

;; Сливане
(merge {:a 1} {:b 2} {:c 3})  ;; => {:a 1 :b 2 :c 3}

4.10 Множества (Sets)

Множествата са колекции от уникални стойности:

#{1 2 3}
(hash-set 1 2 3 2 1)    ;; => #{1 2 3}
(set [1 2 2 3 3 3])     ;; => #{1 2 3}

;; Операции
(conj #{1 2} 3)         ;; => #{1 2 3}
(disj #{1 2 3} 2)       ;; => #{1 3}
(contains? #{1 2 3} 2)  ;; => true
(get #{1 2 3} 2)        ;; => 2
(clojure.set/union #{1 2} #{2 3})    ;; => #{1 2 3}
(clojure.set/intersection #{1 2 3} #{2 3 4}) ;; => #{2 3}
(clojure.set/difference #{1 2 3} #{2 3})     ;; => #{1}

4.11 Структуриране на данни

;; Представяне на потребител с map
(def user {:name "John"
           :email "john@example.com"
           :roles [:admin :user]})

;; Вложени данни
(def company {:name "TechCorp"
              :employees [{:name "Alice" :dept "Engineering"}
                          {:name "Bob" :dept "Sales"}]
              :locations {:HQ "New York"
                         :branch "Boston"}})

4.12 Библиотека за колекции

Основни функции за колекции, които работят еднакво върху различни структури:

;; Предикати
(empty? [])            ;; => true
(empty? [1 2 3])       ;; => false
(every? even? [2 4 6])  ;; => true
(some odd? [2 4 5 6])   ;; => true
(not-empty [1 2 3])     ;; => [1 2 3]
(not-empty [])          ;; => nil

;; Брой
(count [1 2 3])         ;; => 3
(count {:a 1 :b 2})      ;; => 2

;; Конверсия
(vec '(1 2 3))          ;; => [1 2 3]
(list [1 2 3])          ;; => (1 2 3)
(set [1 2 2 3])          ;; => #{1 2 3}
(mapv inc [1 2 3])       ;; => [2 3 4]

5. Функции

5.1 Дефиниране на функции

5.1.1 Основен синтаксис

(defn greeting
  "Връща поздравително съобщение"
  [name]
  (str "Hello, " name "!"))

(greeting "World")  ;; => "Hello, World!"

5.1.2 Множество арности

Функциите могат да имат различен брой аргументи:

(defn add
  ([x] (add x 0))
  ([x y] (+ x y))
  ([x y z] (+ x y z)))

(add 5)    ;; => 5
(add 5 3)  ;; => 8
(add 1 2 3) ;; => 6

5.1.3 Променлив брой аргументи

Използвайте & за останали параметри:

(defn sum [& numbers]
  (reduce + numbers))

(sum 1 2 3 4 5)  ;; => 15

5.2 Анонимни функции

(fn [x] (* x x))
#(* % %)                ;; Имплицитен аргумент
#(* %1 %2)              ;; Множество аргументи
#(reduce + %&)          ;; Останали аргументи

5.3 Функции от по-висок ред

Функции, които приемат или връщат други функции:

(def double #( * % 2))
(def square #(* % %))

(map double [1 2 3 4])    ;; => (2 4 6 8)
(map square [1 2 3 4])    ;; => (1 4 9 16)

(filter even? [1 2 3 4 5 6])  ;; => (2 4 6)

(reduce + [1 2 3 4 5])   ;; => 15
(reduce max [3 1 4 1 5])  ;; => 5

;; Композиране на функции
(def transform (comp square double))
(transform 3)  ;; => 36 (3*2=6, 6*6=36)

5.4 Затваряния (Closures)

Функции, които улавят своята среда:

(defn make-adder [x]
  (fn [y] (+ x y)))

(def add-5 (make-adder 5))
(add-5 10)  ;; => 15
(add-5 3)   ;; => 8

;; Пример за брояч
(defn make-counter []
  (let [count (atom 0)]
    {:increment #(swap! count inc)
     :decrement #(swap! count dec)
     :value #(deref count)}))

5.5 Пред- и пост-условия

(defn absolute-value [n]
  {:pre [(number? n)]
   :post [(>= % 0)]}
  (if (neg? n)
    (- n)
    n))

5.6 Многометодни функции чрез defn

Въпреки че истинските многометодни функции използват defmulti и defmethod, обикновените функции могат да симулират dispatch по поведение:

(defn process [x]
  (cond
    (string? x) (clojure.string/upper-case x)
    (number? x) (inc x)
    :else "unknown"))

6. Управление на потока

6.1 Разклоняване

6.1.1 if / if-not

(if условие
  тогава-израз
  else-израз)

(if (pos? -5)
  "положително"
  "не е положително")  ;; => "не е положително"

;; if-not е просто (if (not условие)...)
(if-not (even? 4)
  "нечетно"
  "четно")  ;; => "четно"

6.1.2 when / when-not

Единично разклонение без else:

(when (pos? 5)
  (println "Положително!")
  (inc 5))  ;; => 6

(when-not (neg? 3)
  "неотрицателно")  ;; => "неотрицателно"

6.1.3 cond

Множество условия:

(defn classify [n]
  (cond
    (neg? n) "отрицателно"
    (zero? n) "нула"
    (even? n) "положително четно"
    :else "положително нечетно"))

(classify -5)  ;; => "отрицателно"
(classify 0)   ;; => "нула"
(classify 4)   ;; => "положително четно"
(classify 7)   ;; => "положително нечетно"

6.1.4 condp

Dispatch базиран на предикат:

(defn respond [msg]
  (condp = msg
    "hello" "Здравей!"
    "bye" "Довиждане!"
    "how are you?" "Добре!"
    "Неизвестно съобщение"))

(respond "hello")  ;; => "Здравей!"

6.1.5 case

Dispatch с константно време (използва хеш сравнение):

(defn http-status [code]
  (case code
    200 "OK"
    301 "Преместен постоянно"
    404 "Не е намерен"
    500 "Вътрешна грешка на сървъра"
    "Неизвестен"))

(http-status 200)   ;; => "OK"
(http-status 999)   ;; => "Неизвестен"

6.2 Итерация

6.2.1 Рекурсия

(defn factorial [n]
  (if (<= n 1)
    1
    (* n (factorial (dec n)))))

;; Със recur (оптимизирано за опашка)
(defn factorial [n]
  (letfn [(fac [n acc]
            (if (<= n 1)
              acc
              (recur (dec n) (* acc n))))]
    (fac n 1)))

6.2.2 loop/recur

Явна итерация с опашкова рекурсия:

(loop [i 0
       result []]
  (if (= i 10)
    result
    (recur (inc i) (conj result i))))

;; => [0 1 2 3 4 5 6 7 8 9]

6.2.3 for (list comprehension)

(for [x (range 5)
      :let [y (* x x)]
      :when (even? y)]
  y)
;; => (0 4 16)

(for [x [:a :b :c]
      y [1 2]]
  [x y])
;; => ([a 1] [a 2] [b 1] [b 2] [c 1] [c 2])

6.2.4 doseq (странични ефекти)

(doseq [x (range 3)
        y (range 3)]
  (println [x y]))
;; Отпечатва:
;; [0 0]
;; [0 1]
;; [0 2]
;; ...

6.3 Обработка на изключения

(try
  (/ 1 0)
  (catch ArithmeticException e
    (str "Грешка: " (.getMessage e)))
  (finally
    (println "Почистване")))

;; С throw
(try
  (throw (ex-info "Потребителска грешка" {:code 123}))
  (catch Exception e
    (ex-data e)))  ;; => {:code 123}

6.4 do

Изпълнява множество изрази, връща последния:

(do
  (println "Страничен ефект")
  (println "Още един")
  (+ 1 2))  ;; => 3

7. Серии и мързеливо оценяване

7.1 Абстракцията Серия (Sequence)

Clojure предоставя унифициран интерфейс за последователни колекции. Ключовите функции са:

  • first - Първи елемент
  • rest - Всички елементи след първия
  • cons - Добавя елемент в началото
;; Работи върху списъци, вектори, низове, map-ове, множества и т.н.
(first [1 2 3])    ;; => 1
(rest [1 2 3])     ;; => (2 3)
(cons 0 [1 2 3])   ;; => (0 1 2 3)

(first "hello")    ;; => \h
(rest "hello")     ;; => (\e \l \l \o)
(first {:a 1 :b 2}) ;; => [:a 1]

7.2 Мързеливи серии

Мързеливите серии се изчисляват при поискване, което позволява: -безкрайни серии

  • ефективност на паметта
  • оптимизация на производителността
;; range произвежда безкрайна мързелива серия
(take 10 (range))  ;; => (0 1 2 3 4 5 6 7 8 9)

;; Поредица на Фибоначи
(def fibs
  (lazy-cat [0 1] (map + fibs (rest fibs))))

(take 10 fibs)  ;; => (0 1 1 2 3 5 8 13 21 34)

;; iterate
(take 5 (iterate inc 0))  ;; => (0 1 2 3 4)
(take 5 (iterate #(* 2 %) 1)) ;; => (1 2 4 8 16)

7.3 Функции за серии

7.3.1 map

Трансформира всеки елемент:

(map inc [1 2 3])      ;; => (2 3 4)
(map + [1 2 3] [4 5 6]) ;; => (5 7 9)
(map str "abc")        ;; => ("a" "b" "c")

7.3.2 filter / remove

Селектира/отхвърля елементи:

(filter even? (range 10))     ;; => (0 2 4 6 8)
(remove even? (range 10))     ;; => (1 3 5 7 9)
(filterv even? (range 10))   ;; => [0 2 4 6 8] (вектор)

7.3.3 reduce

Обработва елементи с натрупване:

(reduce + [1 2 3 4 5])        ;; => 15
(reduce + 10 [1 2 3])         ;; => 16 (с начална стойност)
(reduce (fn [[sum cnt] x]
          [(+ sum x) (inc cnt)])
        [0 0]
        [1 2 3 4 5])
;; => [15 5]

7.3.4 fold

Паралелно намаляване (използва reducers):

(require '[clojure.core.reducers :as r])
(r/fold + (range 1000))

7.3.5 mapcat

Map-ва и след това изравнява:

(mapcat reverse [[1 2] [3 4] [5 6]])  ;; => (2 1 4 3 6 5)

7.3.6 take / drop

(take 3 (range 10))        ;; => (0 1 2)
(drop 3 (range 10))        ;; => (3 4 5 6 7 8 9)
(take-while pos? [3 2 1 0 -1]) ;; => (3 2 1)
(drop-while pos? [3 2 1 0 -1]) ;; => (0 -1)
(split-at 3 (range 5))    ;; => [(0 1 2) (3 4)]

7.3.7 flatten / partition

(flatten [[1 2] [3 [4 5]]])    ;; => (1 2 3 4 5)
(partition 2 (range 8))       ;; => ((0 1) (2 3) (4 5) (6 7))
(partition-all 3 (range 10))  ;; => ((0 1 2) (3 4 5) (6 7 8) (9))
(partition-by even? [1 2 3 4 5 6]) ;; => ((1) (2 3 4) (5 6))

7.3.8 Interpose / Interleave

(interpose "," ["a" "b" "c"])     ;; => ("a" "," "b" "," "c")
(apply str (interpose "," ["a" "b" "c"]))  ;; => "a,b,c"
(interleave [1 2 3] [:a :b :c])  ;; => (1 :a 2 :b 3 :c)

7.3.9 distinct / sort / shuffle

(distinct [1 2 2 3 3 3 4])  ;; => (1 2 3 4)
(sort [3 1 4 1 5 9 2])      ;; => (1 1 2 3 4 5 9)
(sort-by :age [{:age 30} {:age 20} {:age 40}])
;; => ({:age 20} {:age 30} {:age 40})
(shuffle (range 5))         ;; => случайна наредба

7.4 Създаване на серии

(range)           ;; Безкрайни 0, 1, 2, ...
(range 5)         ;; (0 1 2 3 4)
(range 1 10 2)    ;; (1 3 5 7 9) start, end, step
(repeat 5 :x)     ;; (:x :x :x :x :x)
(repeatedly 5 #(rand-int 100))  ;; Случайни стойности
(cycle [:a :b])   ;; Безкрайни (:a :b :a :b ...)

7.5 Обхождане на колекции

;; tree-seq: обхожда вложена структура
(tree-seq sequential? seq [1 [2 [3 4]] 5])
;; => ([1 [2 [3 4]] 5] 1 [2 [3 4]] 2 [3 4] 3 4 5)

;; flatten работи с tree-seq
(flatten [1 [2 [3 4]] 5])  ;; => (1 2 3 4 5)

;; Postwalk и prewalk
(require '[clojure.walk :as walk])
(walk/postwalk #(if (number? %) (* 2 %) %) [1 [2 3] 4])
;; => [2 [4 6] 8]

8. Деструктуриране

Деструктурирането ви позволява да свързвате локални променливи към части от колекции.

8.1 Деструктуриране на вектори

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

;; Пропускане на елементи
(let [[a _ c] [1 2 3]]
  c)  ;; => 3

;; Останал pattern
(let [[a & rest] [1 2 3 4]]
  rest)  ;; => (2 3 4)

;; Със стойности по подразбиране
(let [[a b c d] [1 2]]
  [a b c d])  ;; => [1 2 nil nil]

;; Използване на :or за подразбиране
(let [[a b :or {b 10}] [1]]
  b)  ;; => 10

8.2 Деструктуриране на map-ове

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

;; Преименуване на ключове
(let [{x :a y :b :as original} {:a 1 :b 2}]
  [x y original])  ;; => [1 2 {:a 1 :b 2}]

;; Със стойности по подразбиране
(let [{name :name :or {name "Анонимен"}} {}]
  name)  ;; => "Анонимен"

;; Използване на :keys за автоматично именуване
(let [{:keys [name age city]} {:name "John" :age 30 :city "Boston"}]
  [name age city])  ;; => ["John" 30 "Boston"]

;; Използване на :strs за ключове низове
(let [{:strs [name age]} {"name" "John" "age" 30}]
  name)  ;; => "John"

;; Използване на :syms за ключове символи
(let [{:syms [x y]} {'x 1 'y 2}]
  x)  ;; => 1

8.3 Вложено деструктуриране

(let [[[x y] [a b]] [[1 2] [3 4]]]
  (+ x y a b))  ;; => 10

(let [{name :user {:keys [city state]} :address}
      {:user "John" :address {:city "Boston" :state "MA"}}]
  city)  ;; => "Boston"

8.4 Деструктуриране в параметри на функции

(defn process [[first second & rest]]
  {:first first
   :second second
   :rest rest})

(process [1 2 3 4 5])
;; => {:first 1 :second 2 :rest (3 4 5)}

(defn greet [{:keys [name age]}]
  (str "Здравей, " name "! На " age " години си."))

(greet {:name "Alice" :age 25})
;; => "Здравей, Alice! На 25 години си."

8.5 Деструктуриране с :as

(defn total [{:keys [a b c] :as numbers}]
  (+ a b c))

(total {:a 1 :b 2 :c 3 :d 4})  ;; => 6, numbers все още има :d

9. Пространства от имена

9.1 Създаване и превключване на пространства от имена

(ns myapp.core)

(ns myapp.utils
  (:require [clojure.string :as str]))

;; В REPL
(in-ns 'myapp.core)

9.2 Отнасяне и импортиране

(ns myapp.core
  (:require [clojure.string :as str]
            [clojure.set :as set]
            [clojure.walk :as walk])
  (:import [java.util Date UUID]))  ;; Java interop, показано за пълнота

9.3 Често използвани директиви за namespace

(:require [module :as alias])
(:require [module :refer [fn1 fn2]])
(:require [module :refer :all])  ;; Избягвайте в production

(:use [module])  ;; Остаряло, предпочитайте :require с :refer

(:import [java.util Date])  ;; Java interop

9.4 Опции на ns макроса

Опция Предназначение
:require Зарежда модули с незадължителен alias
:use Зарежда и отнася символи
:import Импортира Java класове
:refer-clojure Контролира referrals към core
:load Зарежда произволен код
:gen-class Генерира Java клас

9.5 Работа с пространства от имена

;; Създаване на var
(def x 10)

;; Вземане на текущото namespace
*ns*  ;; => #namespace[user]

;; Resolve на символ
(resolve 'x)  ;; => #'user/x

;; Създаване на namespace
(create-ns 'myapp.data)

;; Intern на var
(intern 'myapp.data (symbol "y") 20)

;; Вземане на всички vars в namespace
(ns-publics 'myapp.core)

9.6 Добри практики за namespace

  1. Едно namespace на файл
  2. Използвайте смислени имена (напр. myapp.http.client)
  3. Използвайте последователно aliasing
  4. Минимизирайте :use, предпочитайте :require с :refer
  5. Събирайте свързани кодове заедно

10. Макроси

10.1 Какво са макросите?

Макросите са код, който трансформира код преди оценяване. Те получават неоценен код и връщат нов код за оценяване.

;; Прост макрос
(defmacro unless [condition & body]
  `(if (not ~condition)
     (do ~@body)))

;; Употреба
(unless (= 1 2)
  (println "Математиката работи!")
  (+ 1 2))

10.2 Синтаксис цитат (Syntax Quote)

Обратната кавичка (`) предотвратява оценяване и позволява темплейти:

(defmacro debug [expr]
  `(let [result ~expr]
     (println "Debug:" '~expr "=" result)
     result))

10.3 Unquoting

  • ~ (unquote) - Оценява и вмъква
  • ~@ (unquote-splicing) - Оценява и разгъва последователност
(defmacro with-logging [expr]
  `(do
     (println "Изпълнява:" '~expr)
     (let [result ~expr]
       (println "Резултат:" result)
       result)))

;; Splicing пример
(defmacro chain [& forms]
  `(do ~@forms))

(chain
  (println "Първо")
  (println "Второ"))

10.4 Кога да използваме макроси

Използвайте макроси когато:

  • Трябва да контролирате оценяването (като if, when, unless)
  • Трябва да свързвате символи, не стойности (като let, doseq)
  • Трябва да правите изчисления по време на компилация

Използвайте функции когато:

  • Логиката може да се изрази като трансформация на данни
  • Върнатата стойност е данни, не код

10.5 Разгъване на макроси

;; Вижте какво произвежда макрос без да го изпълнявате
(macroexpand '(when (> x 10)
                (println "Голям")
                (inc x)))

;; macroexpand-1 за една стъпка

10.6 Често срещани модели за макроси

10.6.1 Анафорични макроси (имплицитно свързване)

(defmacro with-local-vars [& body]
  `(let []
     ~@(map (fn [form]
              `(quote ~(transform form)))
            body)))

;; По-прост: threading macros
(->> x
     (filter even?)
     (map inc)
     (take 5))

10.6.2 Условна компилация

(defmacro when-bind [[sym test] & body]
  `(let [~sym ~test]
     (when ~sym
       ~@body)))

(when-bind [x (find-value data)]
  (process x))

10.7 Хигиена

По подразбиране Clojure макросите са хигиенични - не изпускат нежелани свързвания. Въпреки това можете да създавате gensyms за ясен контрол:

(defmacro my-macro []
  (let [temp# (gensym "temp")]
    `(let [~temp# 10]
       ~temp#)))

;; temp# auto-gensyms за всяка употреба

11. Конкурентност

Clojure предоставя множество безопасни модели за конкурентност. Всички структури от данни в Clojure са неизменяеми, което елиминира цели класове от грешки свързани с конкурентността.

11.1 Атоми (Atoms)

Атомите предоставят синхронна, независима работа със състояние:

(def counter (atom 0))

;; Четете стойността
(deref counter)  ;; => 0
@counter         ;; => 0

;; Обновявате с функция
(swap! counter inc)  ;; => 1
(swap! counter + 5)   ;; => 6

;; Нулирате към стойност
(reset! counter 0)    ;; => 0

;; Обновяване с множество аргументи
(swap! counter + 1 2 3)  ;; => 6

11.2 Референции (Refs)

Референциите предоставят синхронизирано, координирано състояние чрез Software Transactional Memory (STM):

(def account1 (ref 100))
(def account2 (ref 200))

;; dosync създава транзакция
(dosync
  (alter account1 - 50)
  (alter account2 + 50))

;; Refs могат да бъдат модифицирани само в рамките на dosync

11.3 Агенти (Agents)

Агентите предоставят асинхронни, независими обновления на състояние:

(def logger (agent []))

;; Изпратете обновление (асинхронно)
(send logger conj "event-1")

;; Изчакайте завършване
(await logger)

;; Send-off за блокиращи операции
(send-off logger #(Thread/sleep 1000))

11.4 Променливи (Vars)

Vars предоставят thread-local и namespace-scoped състояние:

(def ^:dynamic *max-connections* 100)

;; Динамично свързване
(binding [*max-connections* 50]
  (*max-connections*))  ;; => 50

;; Thread-local
(def ^:dynamic *thread-id* nil)

(defn get-thread-id []
  (binding [*thread-id* (java.lang.Thread/currentThread)]
    *thread-id*))

11.5 Futures

Futures изпълняват код конкурентно:

(def my-future (future (+ 1 2 3)))

;; Dereference за да получите резултата
@my-future  ;; => 6

;; Проверете дали е завършило
(future-done? my-future)  ;; => true

;; Отказ (ако е възможно)
;; (future-cancel my-future)

11.6 Promises и Delivered

Promises са placeholders за единична стойност:

(def p (promise))

;; Доставяте стойност
(deliver p 42)

;; Блокирате докато бъде доставено
@promise  ;; => 42

;; Timeout
(deref p 1000 :timeout)  ;; Връща :timeout след 1000ms

11.7 Threads

;; Стартирате thread
(.start (Thread. #(println "Работи в thread")))

;; С повече контрол
(let [t (Thread. ^Runnable (fn []
                             (println "Thread тяло")))]
  (.start t))

11.8 Насоки за STM

  1. Поддържайте транзакциите кратки
  2. Избягвайте странични ефекти в транзакции
  3. Използвайте commute за комутативни операции
  4. Използвайте ref-set за прости присвоявания
  5. Retry се случва автоматично при конфликт
;; commute за комутативни операции (редът няма значение)
(dosync
  (commence total count operation))

12. Протоколи и записи

12.1 Протоколи

Протоколите дефинират сигнатури на методи, които типовете могат да имплементират:

(defprotocol Shape
  (area [this])
  (perimeter [this]))

(defprotocol Movable
  (move [this dx dy]))

12.2 Записи

Записите са конкретни типове данни, които могат да имплементират протоколи:

(defrecord Point [x y]
  Shape
  (area [this] 0)
  (perimeter [this] 0)
  Movable
  (move [this dx dy] (->Point (+ x dx) (+ y dy))))

;; Създаване на инстанция
(->Point 3 4)  ;; => #user.Point{:x 3 :y 4}
(Point. 3 4)   ;; Java-style конструктор

;; Factory функция (автогенерирана)
(map->Point {:x 10 :y 20})

12.3 Разширяване на съществуващи типове

Разширете типове да имплементират протоколи:

(extend-protocol Shape
  java.awt.geom.Area
  (area [this] (.getBounds this))

  nil
  (area [this] 0))

;; extend за единични инстанции
(defmethod area :default [this]
  (when (sequential? this)
    (count this)))

12.4 Reify

Създавате анонимни инстанции:

(def circle
  (reify Shape
    (area [this] (* Math/PI (.radius this) (.radius this)))
    (perimeter [this] (* 2 Math/PI (.radius this)))
    :radius 5))

;; Не може лесно да улови външно състояние - използвайте records за това

13. Многометодни функции

Многометодните функции предоставят полиморфизъм чрез произволен dispatch:

13.1 Дефиниране на многометодни функции

(defmulti process type)

(defmethod process :default [x]
  (str "Неизвестно: " x))

(defmethod process Number [x]
  (inc x))

(defmethod process String [x]
  (clojure.string/upper-case x))

13.2 Функции за dispatch

;; Dispatch по стойност
(defmulti kind identity)

;; Dispatch по множество стойности
(defmulti describe
  (fn [x y]
    [(type x) (type y)]))

;; Dispatch по property
(defrecord User [role])
(defmethod describe [:user :admin] [_] "Администратор")
(defmethod describe [:user :guest] [_] "Гост")

13.3 Йерархии

;; Derive създава наследяване за dispatch
(derive ::rect ::shape)
(derive ::circle ::shape)
(derive ::square ::rect)

;; Dispatch работи с йерархията
(defmulti area :type)

(defmethod area ::rect [r]
  (* (:width r) (:height r)))

(defmethod area ::circle [c]
  (* Math/PI (:radius c) (:radius c)))

13.4 remove-method

(remove-method process String)

14. Тестване

14.1 Clojure.test

(ns myapp.core-test
  (:require [clojure.test :as t]
            [myapp.core :as core]))

(t/deftest addition-test
  (t/testing "основно събиране"
    (t/is (= 4 (+ 2 2)))
    (t/is (= 5 (+ 2 2)))  ;; Неуспех
    (t/are [x y] (= x y)
      2 (+ 1 1)
      4 (+ 2 2))))

(t/deftest collection-test
  (t/is (vector? []))
  (t/is (empty? []))
  (t/is (= 3 (count [1 2 3]))))

14.2 Fixtures

(defn setup [f]
  (направете нещо преди)
  (f)
  (направете нещо след))

(t/use-fixtures :each setup)  ;; Изпълнява за всеки тест
(t/use-fixtures :once setup)   ;; Изпълнява веднъж за всички тестове

14.3 Пускане на тестове

clojure -M:test
lein test

14.4 Генеративно тестване (test.check)

(require '[clojure.test.check :as tc]
         '[clojure.test.check.generators :as gen]
         '[clojure.test.check.properties :as prop])

(def sort-idempotent
  (prop/for-all [v (gen/vector gen/int)]
    (= (sort v) (sort (sort v)))))

(tc/quick-check 100 sort-idempotent)

15. REPL

15.1 Команди на REPL

Команда Описание
doc Преглед на документация
find-doc Търсене в документите
source Преглед на source код
pst Отпечатване на stack trace
apropos Търсене на символи
dir Списък на vars в namespace

15.2 REPL работен процес

;; Зареждане на код
(require '[myapp.core :as core] :reload)

;; Изчистване на REPL състояние
(remove-all-methods multimethod :default)

;; Хващане на изключения
 CompilerException ...

;; Pretty print
(require '[clojure.pprint :as pp])
(pp/pprint data)

15.3 Интеграция с редактор

  • VS Code + Calva: :jack-in за стартиране на REPL
  • Emacs + CIDER: cider-jack-in
  • Vim + Conjure: Свързва се автоматично

16. Core.async

Core.async предоставя асинхронно програмиране с канали.

16.1 Канали

(require '[clojure.core.async :as async])

(def ch (async/chan))

;; Поставяне на стойност (блокира ако буферът е пълен)
(async/>!! ch "hello")

;; Вземане на стойност (блокира ако е празен)
(async/<!! ch)  ;; => "hello"

;; Затваряне на канал
(async/close! ch)

16.2 Threaded Channels

;; >!! и <!! блокират OS threads (използвайте пестеливо)
;; >! и <! работят с go blocks (леки)

16.3 Go Blocks

(async/go
  (let [msg (<! ch)]  ;; <! вместо <!!
    (println "Получено:" msg)))

;; Поставяне в go block
(async/go
  (>! out-ch "result"))

16.4 Buffers

(async/chan 10)           ;; Фиксиран буфер
(async/chan (async/sliding-buffer 100))  ;; Пуска стари
(async/chan (async/dropping-buffer 100)) ;; Пуска нови

16.5 Pipeline

(async/pipeline-async 4
  out-ch
  (fn [input ch]
    (async/go
      (async/>! ch (process input))))
  in-ch)

17. Добри практики

17.1 Организация на кода

;; Типична структура на namespace
(ns myapp.core
  (:require [myapp.util :as util]
            [myapp.spec :as spec]
            [clojure.string :as str])
  (:import [java.util Date]))  ;; Показано само за пълнота

17.2 Неизменяеми данни

Предпочитайте неизменяеми структури от данни. Когато мутация е нужна:

  • Използвайте atoms за независимо състояние
  • Използвайте refs със STM за координирано състояние
  • Избягвайте странични ефекти в чисти функции

17.3 Конвенции за именуване

Тип Конвенция Пример
Vars kebab-case defn calculate-total
Класове/Записи PascalCase defrecord UserProfile
Константи UPPER-SNAKE def MAX-RETRY
Private vars trailing underscore defn- internal-func
Dynamic vars заобиколени def *max-connections*

17.4 Обработка на грешки

(defn safe-parse
  [s]
  (try
    (Long/parseLong s)
    (catch NumberFormatException _
      nil)))

;; С ex-info за структурирани грешки
(defn validate [x]
  (when (neg? x)
    (throw (ex-info "Трябва да е положително" {:value x}))))

17.5 Съвети за производителност

  1. Използвайте transduce вместо into + трансформация
  2. Използвайте mapv когато се нуждаете от векторен резултат
  3. Използвайте filterv за филтрирани вектори
  4. Използвайте reduce-kv за итерация върху map
  5. Помислете за transducers за ефективни трансформации

17.6 Threading Macros

Направете кода по-четим:

;; Thread-first (->)
(-> user
    (assoc :last-login (java.time Instant/now))
    (update :login-count inc)
    :last-login)

;; Thread-last (->>)
(->> users
     (map :name)
     (filter #(.startsWith % "A"))
     (sort)
     (take 10))

;; Thread-as (some->, some->>)
(some-> {:user {:profile {:avatar "url"}}}
        :user :profile :avatar
        clojure.string/upper-case)

18. Индекс

A

B

C

E

F

G

H

I

J

K

L

M

N

O

P

Q

R

S

T

U

V

W

Z


Приложение А: Бърза справка

Основни функции

Функция Описание
inc / dec Инкремент / декремент
+ / - / * / / Аритметика
= / == / not= Равенство
< / > / <= / >= Сравнение
and / or / not Логически
first / rest / next Операции със серии
cons / conj / concat Изграждане на колекции
map / filter / reduce Трансдюсъри
get / assoc / dissoc Операции с map
get-in / assoc-in Вложени операции
apply / partial Приложение на функция
comp / juxt / memoize Комбинатори на функции

Обобщение на структурите от данни

Тип Литерал Достъп Неизменяем?
Списък '(1 2 3) first, nth Да
Вектор [1 2 3] get, nth Да
Map {:a 1} get, keys Да
Множество #{1 2 3} get, contains? Да

Приложение Б: Речник

Атом (Atom) - Мутабилен контейнер, който осигурява синхронни, независими обновления.

Затваряне (Closure) - Функция, която улавя и запазва достъп до променливи от обграждащия си обхват.

Деструктуриране (Destructuring) - Свързване на локални променливи към части от колекция или map.

Хигиеничен макрос (Hygienic Macro) - Макрос, който не изпуска нежелани свързвания.

Мързелива серия (Lazy Sequence) - Серия, чиито елементи се изчисляват при поискване.

Протокол (Protocol) - Именовано множество от сигнатури на методи, които типовете могат да имплементират.

Референция (Ref) - Мутабилен контейнер, управляван от STM за координирани обновления.

S-Израз (S-Expression) - Списък с кръгли скоби в Lisp синтаксиса.

STM - Software Transactional Memory, модел за конкурентност използващ транзакции.

Var - Мутабилен контейнер, който осигурява thread-local и namespace-scoped състояние.


Чист Clojure: Изчерпателно Ръководство Версия 1.0