23252826a0
- 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
16 KiB
16 KiB
Pure Clojure: Practical Recipes
Table of Contents
- Common Patterns
- Data Transformation Recipes
- State Management Recipes
- Async Recipes
- Validation Recipes
- API Design Patterns
- Testing Recipes
- Performance Recipes
1. Common Patterns
1.1 Maybe/Option Pattern
(defn safe-divide [a b]
(if (zero? b)
nil
(/ a b)))
(defn map-safe [f coll]
(sequence (comp (filter some?)
(map f))
coll))
(map-safe #(safe-divide 10 %) [2 0 4 0 5])
;; => (5 2 2)
1.2 Either/Result Pattern
(defn parse-int [s]
(try
{:success (Long/parseLong s)}
(catch NumberFormatException _
{:error "Invalid number"})))
(defn bind [result f]
(if (:error result)
result
(f (:success result))))
(-> (parse-int "42")
(bind #(* % 2))
(bind #(+ % 1)))
;; => {:success 85}
1.3 State Machine Pattern
(defprotocol StateMachine
(transition [state event])
(current-state [state]))
(defrecord TrafficLight [state]
StateMachine
(transition [_ event]
(case [state event]
[:green :timeout] (->TrafficLight :yellow)
[:yellow :timeout] (->TrafficLight :red)
[:red :timeout] (->TrafficLight :green)
_))
(current-state [_] state))
1.4 Builder Pattern
(defn make-builder [defaults]
(let [state (atom defaults)]
(reify
Object
(toString [_] (str @state))
clojure.core.protocols/Coll
(coll [_] (seq @state))
clojure.lang.IFn
(invoke [_ k v]
(swap! state assoc k v)
this)
(invoke [_ m]
(swap! state merge m)
this))))
(def builder (make-builder {:debug false :timeout 5000}))
(-> builder
(assoc :host "localhost")
(merge {:port 8080})
str)
2. Data Transformation Recipes
2.1 Nested Data Access
(defn get-in-safe [m keys default]
(try
(get-in m keys)
(catch NullPointerException _
default)))
;; With spec validation
(get-in-safe {:user {:address {:city "Sofia"}}}
[:user :address :city]
"Unknown")
;; Deep update
(defn update-in-safe [m keys f & args]
(if (get-in m keys)
(apply update-in m keys f args)
m))
2.2 Grouping and Aggregating
;; Group by multiple keys
(defn group-by-multiple [ks coll]
(reduce
(fn [acc item]
(update-in acc (map item ks) conj item))
{}
coll))
(group-by-multiple [:department :role]
[{:name "Alice" :department "Eng" :role "Dev"}
{:name "Bob" :department "Eng" :role "Dev"}
{:name "Carol" :department "Sales" :role "Mgr"}])
;; Rolling aggregations
(defn rolling [f n coll]
(let [window (vec (take n coll))]
(lazy-seq
(cons (f window)
(rolling f n (rest coll))))))
2.3 Pivot Tables
(defn pivot-table [data row-key col-key value-fn]
(reduce
(fn [table row]
(let [r (row-key row)
c (col-key row)
v (value-fn row)]
(assoc-in table [r c] v)))
{}
data))
(pivot-table [{:month "Jan" :region "East" :sales 100}
{:month "Jan" :region "West" :sales 150}
{:month "Feb" :region "East" :sales 200}]
:month :region :sales)
;; => {"Jan" {"East" 100 "West" 150}
"Feb" {"East" 200}}
2.4 Tree Operations
;; Sum all numeric leaves
(defn tree-sum [tree]
(reduce + 0
(tree-seq sequential? seq tree)))
;; Map over tree
(defn tree-map [f tree]
(postwalk #(if (sequential? %) (mapv f %) %) tree))
;; Find in tree
(defn tree-find [pred tree]
(first (filter pred (tree-seq sequential? seq tree))))
3. State Management Recipes
3.1 Service Pattern with Atoms
(defprotocol Service
(start [this])
(stop [this])
(process [this input]))
(defn make-service [config]
(let [state (atom {:config config
:running false
:cache {}})]
(reify
Service
(start [_]
(swap! state assoc :running true))
(stop [_]
(swap! state assoc :running false))
(process [_ input]
(when-not (:running @state)
(throw (ex-info "Service not running" {})))
(if-let [cached (get-in @state [:cache input])]
cached
(let [result (compute input)]
(swap! state assoc-in [:cache input] result)
result))))))
3.2 Event Sourcing
(defn make-event-store []
(let [events (atom [])
snapshots (atom {})]
(reify
Object
(toString [_] (pr-str @events))
clojure.core.protocols/Coll
(coll [_] (seq @events))
clojure.lang.IFn
(invoke [_ event]
(let [new-state (apply-event @snapshots event)]
(swap! events conj event)
(when (seq? new-state)
(reset! snapshots new-state))))
(invoke [_ n]
(get @snapshots n)))))
(defn apply-event [state event]
(case (:type event)
:created (assoc state (:id event) (:data event))
:updated (update state (:id event) merge (:data event))
:deleted (dissoc state (:id event))
state))
3.3 Cooldown Mechanism
(defn make-cooldown [timeout-ms]
(let [last-call (atom 0)]
(fn []
(let [now (System/currentTimeMillis)]
(when (> (- now @last-call) timeout-ms)
(reset! last-call now)
true)))))
(def rate-limiter (make-cooldown 1000))
;; Usage
(when (rate-limiter)
(do-something))
3.4 Circuit Breaker
(defn make-circuit-breaker [failure-threshold reset-timeout]
(let [state (atom {:status :closed
:failures 0
:last-failure 0})]
(fn [f]
(let [current @state]
(case (:status current)
:open
(if (> (- (System/currentTimeMillis) (:last-failure current))
reset-timeout)
(do (swap! state assoc :status :half-open)
(try
(let [result (f)]
(swap! state assoc :status :closed :failures 0)
result)
(catch Exception e
(swap! state assoc :status :open :last-failure (System/currentTimeMillis))
(throw e))))
(throw (ex-info "Circuit open" {})))
:half-open
(try
(let [result (f)]
(swap! state assoc :status :closed :failures 0)
result)
(catch Exception e
(swap! state assoc :status :open :last-failure (System/currentTimeMillis))
(throw e)))
:closed
(try
(let [result (f)]
(swap! state assoc :failures 0)
result)
(catch Exception e
(let [failures (inc (:failures current))]
(swap! state assoc :failures failures
:last-failure (System/currentTimeMillis))
(when (>= failures failure-threshold)
(swap! state assoc :status :open))
(throw e)))))))))
4. Async Recipes
4.1 Channel Pipeline
(defn channel-pipeline [in-f out-f & channels]
(doseq [ch channels]
(async/go
(loop []
(when-let [value (<! ch)]
(out-f value)
(recur))))))
;; Usage
(let [input-chan (async/chan 100)
process-chan (async/chan 100)
output-chan (async/chan 100)]
(async/pipeline 10 process-chan (map process-item) input-chan)
(async/pipeline 10 output-chan (map format-output) process-chan))
4.2 Multiplexing Channels
(defn multiplex [in-chan & out-chans]
(async/go
(loop [value (<! in-chan)]
(when-not (nil? value)
(doseq [ch out-chans]
(>! ch value))
(recur (<! in-chan))))))
;; Usage
(let [input (async/chan)
out1 (async/chan)
out2 (async/chan)]
(multiplex input out1 out2)
;; Now input is broadcast to both outputs
)
4.3 Timeout Patterns
;; Timeout on operation
(async/go
(let [result (async/alts!! [work-chan
(async/timeout 5000)])]
(if (= result :timed-out)
{:status :timeout}
{:status :success :value (first result)})))
;; Retry with backoff
(defn with-retry [f max-attempts delay-ms]
(async/go
(loop [attempts 0]
(let [result (async/<! (async/timeout delay-ms))]
(if (= :failure result)
(if (< attempts max-attempts)
(recur (inc attempts))
{:status :failed})
{:status :success :value result})))))
4.4 Windowing
(defn windowed [in size overlap]
(let [out (async/chan)]
(async/go
(loop [window (vec (take size (async/<! in)))]
(when (seq window)
(>! out window)
(let [next-items (vec (take overlap (rest window)))
remaining (- size overlap)
new-items (vec (take remaining (async/<! in)))]
(recur (into next-items new-items))))))
out))
5. Validation Recipes
5.1 Multi-stage Validation
(defn validate-stages [data & validators]
(reduce
(fn [result validator]
(let [errors (validator result)]
(if (seq errors)
(reduced {:status :error :errors errors})
result)))
{:status :ok :data data}
validators))
(defn non-blank [field]
(fn [result]
(when (clojure.string/blank? (get-in result [:data field]))
[field "cannot be blank"])))
(defn max-length [field length]
(fn [result]
(when (> (count (get-in result [:data field])) length)
[field (str "must be at most" length "characters")])))
(validate-stages
{:name ""}
(non-blank :name)
(max-length :name 50))
;; => {:status :error :errors [:name "cannot be blank"]}
5.2 Schema Validation
(def email-regex #"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")
(defn validate-email [email]
(when-not (re-find email-regex email)
"Invalid email format"))
(defn validate-user [user]
(reduce-kv
(fn [errors field validate]
(if-let [error (validate (get user field))]
(conj errors [field error])
errors))
[]
{:name [#(when (clojure.string/blank? %) "Name required")]
:email [validate-email
#(when (> (count %) 100) "Email too long")]
:age [#(when (or (nil? %) (neg? %)) "Age must be positive")]}))
(validate-user {:name "John" :email "john@example.com" :age 30})
;; => []
5.3 Contract Testing
(defmacro defcontract [name input-spec output-spec & body]
`(defn ~name [& args#]
(let [input# (first args#)
output# (apply ~(into `fn input-spec `@body) args#)]
(when-not (~output-spec output#)
(throw (ex-info "Contract violation"
{:function '~name
:input input#
:output output#})))
output#)))
;; Usage
(defcontract add-positive
[a number? b number?] ;; Input spec
number? ;; Output spec
[a b]
(+ a b))
6. API Design Patterns
6.1 Ring Handlers (Pure Functions)
(defn wrap-logging [handler]
(fn [request]
(println "Request:" (:uri request))
(let [response (handler request)]
(println "Response:" (:status response))
response)))
(defn wrap-cors [handler]
(fn [request]
(let [response (handler request)]
(assoc response :headers
(merge (:headers response {})
{"Access-Control-Allow-Origin" "*"})))))
;; Pure handler
(defn handle-get-user [request]
{:status 200
:headers {"Content-Type" "application/json"}
:body (pr-str {:name "John" :email "john@example.com"})})
6.2 Middleware Stack
(defn apply-middleware [handler middlewares]
(reduce
(fn [h middleware]
(middleware h))
handler
middlewares))
(def app
(-> handler-get-user
(apply-middleware [wrap-cors
wrap-logging
wrap-auth])))
6.3 Route Definitions
(def routes
[[:get "/users" list-users]
[:get "/users/:id" get-user]
[:post "/users" create-user]
[:put "/users/:id" update-user]
[:delete "/users/:id" delete-user]])
(defn match-route [method path]
(some
(fn [[m p handler]]
(when (and (= method m)
(re-matches (路由->regex p) path))
{:handler handler
:params (extract-params p path)}))
routes))
6.4 Error Handling Middleware
(defn wrap-exception [handler]
(fn [request]
(try
(handler request)
(catch Exception e
{:status 500
:headers {"Content-Type" "application/json"}
:body (pr-str {:error (ex-message e)
:data (ex-data e)})}))))
(defn wrap-not-found [handler]
(fn [request]
(let [response (handler request)]
(if (= (:status response) 404)
{:status 404
:body "Not Found"}
response))))
7. Testing Recipes
7.1 Property-Based Testing
(defcommutative +
[a integer? b integer?]
(= (+ a b) (+ b a)))
(defassociative +
[a integer? b integer? c integer?]
(= (+ (+ a b) c) (+ a (+ b c))))
;; Idempotent operations
(defidempotent conj
[coll vector? item any?]
(= (conj (conj coll item) item)
(conj coll item)))
7.2 Test Fixtures with Random Data
(defn with-sample-data [f]
(let [samples (gen/sample (s/gen ::user) 10)]
(doseq [sample samples]
(f sample))))
(t/use-fixtures :each with-sample-data)
(t/deftest user-validation-test
[sample]
(t/is (nil? (validate-user sample))))
7.3 Mutation Testing
;; Simple mutation testing
(defn mutate-and-test [original-fn test-fn mutation]
(let [mutated (mutation original-fn)]
(try
(test-fn mutated)
false ;; Test passed on mutation = bad
(catch AssertionError _
true)))) ;; Test caught mutation = good
;; Random mutation generator
(defn random-mutation [f]
(let [mutations [(fn [x] (inc x))
(fn [x] (dec x))
(fn [x] (* x 2))]]
(some #(% f) mutations)))
8. Performance Recipes
8.1 Batched Processing
(defn batch-process [items batch-size f]
(into []
(mapcat f)
(partition-all batch-size items)))
;; Usage
(batch-process (range 10000) 100
(fn [batch]
(mapv expensive-operation batch)))
8.2 Caching with TTL
(defn make-ttl-cache [ttl-ms]
(let [cache (atom {})
cleanup (fn []
(let [now (System/currentTimeMillis)]
(swap! cache
(fn [m]
(into {}
(filter #(< (- now (val %)) ttl-ms))
m)))))]
(fn [f]
(fn [k]
(cleanup)
(if-let [entry (get @cache k)]
(val entry)
(let [result (f k)]
(swap! cache assoc k [(System/currentTimeMillis) result])
result))))))
(def cached-heavy-operation (make-ttl-cache 60000) heavy-operation)
8.3 Lazy File Processing
(defn lazy-file-lines [filepath]
(line-seq (clojure.java.io/reader filepath)))
(defn lazy-csv-rows [filepath]
(map #(clojure.string/split % #",")
(lazy-file-lines filepath)))
;; Process huge files line by line
(into []
(comp
(drop 1) ;; Skip header
(map #(update % 2 parse-long)) ;; Transform column
(filter #(= "active" (% 3))))
(take 1000 (lazy-csv-rows "large-file.csv")))
8.4 Parallel Collection Processing
(defn parallel-map [f coll n]
(let [parts (partition-all (/ (count coll) n) coll)
results (pmap #(doall (map f %)) parts)]
(apply concat results)))
;; With reducers for better performance
(require '[clojure.core.reducers :as r])
(defn parallel-reduce [f init coll]
(r/fold (/ (count coll) 4) f coll))
Pure Clojure: Practical Recipes