- 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
58 KiB
Чист Clojure: Изчерпателно Ръководство
Съдържание
- Въведение в Clojure
- Първи стъпки
- Основен синтаксис и форми
- Структури от данни
- Функции
- Управление на потока
- Серии и мързеливо оценяване
- Деструктуриране
- Пространства от имена
- Макроси
- Конкурентност
- Протоколи и записи
- Многометодни функции
- Тестване
- REPL
- Core.async
- Добри практики
- Индекс
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 се придържа към няколко принципа:
- Простота: Трудните неща трябва да бъдат прости, простите неща трябва да бъдат тривиални
- Неизменяемост: Предпочитайте неизменяеми структури от данни за безопасност и конкурентност
- Абстракция: Изграждайте слоеве на абстракция за управление на сложността
- Ориентираност към изрази: Всичко е израз, който връща стойност
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 Правила за оценяване
- Числа, низове, булеви стойности, nil и ключови думи се оценяват като себе си
- Символи се оценяват като стойността на променливата, която именуват
- Списъци се оценяват като извиквания на функции (ако първият елемент е извикаем)
- Цитираните изрази предотвратяват оценяване
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
- Едно namespace на файл
- Използвайте смислени имена (напр.
myapp.http.client) - Използвайте последователно aliasing
- Минимизирайте
:use, предпочитайте:requireс:refer - Събирайте свързани кодове заедно
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
- Поддържайте транзакциите кратки
- Избягвайте странични ефекти в транзакции
- Използвайте commute за комутативни операции
- Използвайте ref-set за прости присвоявания
- 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 Съвети за производителност
- Използвайте
transduceвместоinto+ трансформация - Използвайте
mapvкогато се нуждаете от векторен резултат - Използвайте
filtervза филтрирани вектори - Използвайте
reduce-kvза итерация върху map - Помислете за
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
atom- 11.1agent- 11.3and- 3.5are- 14.1apply- 7.3.3as->- 17.6assert- 5.5assoc- 4.9async/chan- 16.1
B
C
case- 6.1.5comment- 3.5comp- 5.3concat- 4.7cond- 6.1.3condp- 6.1.4conj- 4.8cons- 4.7def- 3.3.1defmacro- 10.1defmethod- 13.1defmulti- 13.1defn- 5.1.1defprotocol- 12.1defrecord- 12.2defref- 11.2delay- 11.6destructure- 8disj- 4.10dissoc- 4.9doseq- 6.2.4dosync- 11.2dotimes- 6.2.3drop- 7.3.6drop-while- 7.3.6
E
F
fdef- 5.5filter- 7.3.2filterv- 7.3.2find-doc- 15.1first- 4.7flatten- 7.3.7flip- 11.2fn- 5.2for- 6.2.3force- 11.6format- 2.3future- 11.5
G
H
I
if- 3.3.3if-let- 6.1.2if-not- 6.1.1import- 9.3inc- 4.2indexed- 7.3.7into- 7.3.7interleave- 7.3.8interpose- 7.3.8iterate- 7.2
J
juxt- 5.3
K
keys- 8.2
L
M
macroexpand- 10.5macroexpand-1- 10.5map- 7.3.1map-indexed- 7.3.1mapcat- 7.3.5mapv- 7.3.1max-key- 5.3merge- 4.9merge-with- 4.9meta- 3.3.1min-key- 5.3mod- 4.1.1
N
namespace- 9.5neg?- 4.2nil?- 4.4not- 4.4not-empty- 4.12ns- 9.1ns-publics- 9.5ns-resolve- 9.5
O
or- 3.5
P
parallelize- 11.7partition- 7.3.7partition-all- 7.3.7partition-by- 7.3.7partial- 5.3peek- 4.7persist- 7.2pmap- 7.3.1pop- 4.8pos?- 4.2promise- 11.6
Q
quote- 3.3.4
R
rand- 7.4rand-int- 7.4range- 7.4recur- 6.2.1reduce- 7.3.3reduce-kv- 7.3.3reductions- 7.3.3ref- 11.2ref-set- 11.2release-pending-sends- 11.3remove- 7.3.2repeat- 7.4repeatedly- 7.4replicate- 7.4require- 9.3reset!- 11.1rest- 4.7reverse- 7.3.9
S
select-keys- 4.9send- 11.3send-off- 11.3seq- 7.1set- 4.10set!- 11.4short-circuit- 3.5shuffle- 7.3.9shutdown-agents- 11.3some- 7.3.2some->- 17.6some-fn- 5.3sort- 7.3.9sort-by- 7.3.9split-at- 7.3.6split-with- 7.3.6str- 4.2subs- 4.2superiors- 13.3swap!- 11.1
T
take- 7.3.6take-nth- 7.3.6take-while- 7.3.6test- 14.1thread-bound?- 11.4throw- 6.3tree-seq- 7.5try- 6.3type- 12.2
U
V
val- 7.1vals- 4.9var- 3.3.1var-get- 11.4var-set- 11.4vec- 4.8vector- 4.8vector-of- 4.8volatile!- 11.1
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