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

16 KiB

Pure Clojure: Practical Recipes

Table of Contents

  1. Common Patterns
  2. Data Transformation Recipes
  3. State Management Recipes
  4. Async Recipes
  5. Validation Recipes
  6. API Design Patterns
  7. Testing Recipes
  8. 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