Ubuntu TechHive
save-tokens-and-build-with-data-driven-clarity-using-clojure-script-part.md
Clojure(Script) का उपयोग करके टोकन बचाएं और डेटा-संचालित स्पष्टता के साथ निर्माण करें
article.विवरण

Clojure(Script) का उपयोग करके टोकन बचाएं और डेटा-संचालित स्पष्टता के साथ निर्माण करें

reading.प्रगति 13 मिनट पढ़ें

Clojure(Script) का उपयोग करके टोकन बचाएं और डेटा-संचालित स्पष्टता के साथ निर्माण करें

संक्षिप्त कहानी: Clojure(Script) और वेब

2000 के दशक की शुरुआत और उसके बाद

  • Vim और Emacs सीखा लेकिन दोनों में ही फिसड्डी रहा
  • HTTP और वेब
  • LAMP से लेकर SPA और अन्य फ्रेमवर्क तक
  • ढेर सारी स्क्रिप्टिंग और ढेर सारी मेटाप्रोग्रामिंग (JavaScript, Ruby, Groovy, Scala ...)
  • JavaScript एक खिलौने से बदलकर एक आधारभूत तकनीक बन गई

LISP

  • बहुत सारे कोष्ठक (parens)
  • दिलचस्प और अजीब
  • जादूगरों ने किताबें लिखीं (Practical Common Lisp, SICP ...)
  • इसके बारे में बहुत सारी अजीब कहानियाँ सुनीं (PG, HN, Hackers and Painters ...)
  • विभिन्न LISP कार्यान्वयन, सभी बहुत अजीब

व्यावहारिक LISP

  • 2009 के आसपास एक जावा कॉन्फ्रेंस में Clojure के बारे में पता चला
  • अंततः JVM की वजह से कुछ प्रासंगिक मिला जिसे आज़माने लायक था
  • पहला गंभीर प्रयास: एक Spring ऐप में Java Interface + Clojure Implementation
  • यह और बेहतर हुआ: JavaScript के ऊपर एक नया Clojure (Google Closure Library, Google Closure Compiler: प्रोडक्शन-ग्रेड आक्रामक मिनिफिकेशन)
  • प्रगतिशील ज्ञान: (verb data)

Clojure(Script): स्पष्ट रूप से सोचने का एक उपकरण

  • REPL
  • भाषा डोमेन के अनुसार ढल जाती है
  • सिंटैक्स? कैसा सिंटैक्स? यह सब डेटा स्ट्रक्चर है
  • आइसोमोर्फिक (Isomorphic)
  • Clojure एक होस्ट की गई भाषा है (JVM, JavaScript, Dart और अन्य)
  • बिना किसी फालतू के सीधे मुख्य बात पर पहुँचें
  • Specs + functions + data + validation: (specked-verb data)

Clojure(Script) की मूल बातें

नीचे दिया गया प्रत्येक कोड ब्लॉक इस तरह लिखा गया है कि आप इसे लाइव डेमो के दौरान ClojureScript REPL में ज्यों का त्यों पेस्ट कर सकें।
Lisp सिंटैक्स में, शुरुआती कोष्ठक के बाद पहली चीज़ फंक्शन होती है:

(+ 1 2 3)
;; => 6

(str "Hello, " "ClojureScript")
;; => "Hello, ClojureScript"

डेटा लिटरल्स: पहले मान (values)

Clojure(Script) कोड मुख्य रूप से सरल डेटा आकृतियों से बना होता है:

42                         ;; संख्या
"Ada"                      ;; स्ट्रिंग
:admin                     ;; कीवर्ड, अक्सर डेटा लेबल के रूप में उपयोग किया जाता है
[1 2 3]                    ;; वेक्टर, क्रमबद्ध मान
{:name "Ada" :visits 1}    ;; मैप, कुंजियों से मान
#{:cljs :repl :data}        ;; सेट, अद्वितीय मान

नेमस्पेस और require

REPL पर, किसी नेमस्पेस को लोड करने और उसे एक छोटा उपनाम (alias) देने के लिए require का उपयोग करें।
वास्तविक फ़ाइलें शीर्ष पर ns फॉर्म का उपयोग करती हैं, लेकिन लाइव REPL डेमो के लिए require अधिक अनुकूल है।

(require '[clojure.string :as str])

(str/upper-case "fast twitch")
;; => "FAST TWITCH"

(str/trim "  hello  ")
;; => "hello"

कीवर्ड छोटे लुकअप फंक्शन की तरह भी काम कर सकते हैं:

(def user {:name "Ada" :visits 1})

(:name user)
;; => "Ada"

(assoc user :active? true)
;; => {:name "Ada", :visits 1, :active? true}

(update user :visits inc)
;; => {:name "Ada", :visits 2}

कुंजियाँ और नेमस्पेस वाली कुंजियाँ

मैप की कुंजी वह लेबल है जिसका उपयोग मान खोजने के लिए किया जाता है। कीवर्ड सामान्य मैप कुंजियाँ हैं क्योंकि वे छोटे, पठनीय होते हैं और लुकअप फंक्शन के रूप में काम करते हैं।

(def response
  {:status 200
   :body "ok"})

(get response :status)
;; => 200

(:body response)
;; => "ok"

नेमस्पेस वाले कीवर्ड स्लैश से पहले एक उपसर्ग जोड़ते हैं। वे अभी भी केवल कीवर्ड ही हैं, लेकिन उपसर्ग बड़े डेटा मैप्स में टकराव से बचने में मदद करता है।

(def person
  {:person/name "Ada"
   :account/name "ada-lovelace"
   :account/id 42})

(:person/name person)
;; => "Ada"

(:account/name person)
;; => "ada-lovelace"

(select-keys person [:person/name :account/id])
;; => {:person/name "Ada", :account/id 42}

आप किसी कीवर्ड के नेमस्पेस भाग और नाम भाग का निरीक्षण कर सकते हैं:

(namespace :account/id)
;; => "account"

(name :account/id)
;; => "id"

(keyword "account" "email")
;; => :account/email

फंक्शन्स

defn एक फंक्शन को नाम देता है। नाम के बाद वाला वेक्टर तर्क सूची (argument list) है।

(defn greet [name]
  (str "Hello, " name "!"))

(greet "Ada")
;; => "Hello, Ada!"

अनाम (Anonymous) फंक्शन्स तब उपयोगी होते हैं जब फंक्शन छोटा और स्थानीय हो:

((fn [x] (* x 10)) 7)
;; => 70

(#(* % 10) 7)
;; => 70

मल्टी-एरिटी फंक्शन्स

एक फंक्शन का नाम अलग-अलग संख्या में तर्कों का समर्थन कर सकता है।

(defn greeting
  ([] (greeting "friend"))
  ([name] (str "Hello, " name))
  ([title name] (str "Hello, " title " " name)))

(greeting)
;; => "Hello, friend"

(greeting "Ada")
;; => "Hello, Ada"

(greeting "Dr." "Hopper")
;; => "Hello, Dr. Hopper"

यह सरल डिफॉल्ट्स को पूर्ण संस्करण के करीब रखता है।

map

map एक संग्रह (collection) में प्रत्येक आइटम पर एक फंक्शन चलाता है। यह एक लेज़ी सीक्वेंस लौटाता है, इसलिए जब आप REPL में वेक्टर देखना चाहते हैं तो vec का उपयोग करें।

(map inc [1 2 3])
;; => (2 3 4)

(vec (map inc [1 2 3]))
;; => [2 3 4]

(def todos
  [{:title "Read the schema" :done? true}
   {:title "Try the REPL" :done? false}])

(map :title todos)
;; => ("Read the schema" "Try the REPL")

->

थ्रेड-फर्स्ट मैक्रो -> पिछले परिणाम को अगले फॉर्म में पहले डेटा तर्क के रूप में रखता है। यह बाएं-से-दाएं डेटा क्लीनअप को पढ़ने में आसान बनाता है।

(-> {:name "Ada" :visits 1}
    (assoc :active? true)
    (update :visits inc))
;; => {:name "Ada", :visits 2, :active? true}

इसे ऐसे पढ़ें: इस मैप से शुरू करें, फिर एक मान जोड़ें (associate), फिर एक मान अपडेट करें।

->>

थ्रेड-लास्ट मैक्रो ->> पिछले परिणाम को अगले फॉर्म के अंत में रखता है। यह कलेक्शन पाइपलाइनों के लिए सामान्य है।

(def todos
  [{:title "Read the schema" :done? true}
   {:title "Try the REPL" :done? false}])

(->> todos
     (filter :done?)
     (map :title)
     vec)
;; => ["Read the schema"]

इसे ऐसे पढ़ें: todos लें, पूर्ण (done) आइटम रखें, उनके शीर्षक लें, एक वेक्टर बनाएं।

let के साथ स्थानीय नाम

let मानों को अस्थायी नाम देता है। ये नाम केवल let के अंदर ही मौजूद रहते हैं।

(let [title "Try the REPL"
      done? false]
  {:title title
   :status (if done? "done" "open")})
;; => {:title "Try the REPL", :status "open"}

डिस्ट्रक्चरिंग (Destructuring)

डिस्ट्रक्चरिंग मैन्युअल get कॉल के बिना मैप्स और वेक्टर से नाम बाहर निकालती है।

(def user {:name "Ada" :visits 1})

(let [{:keys [name visits]} user]
  (str name " visited " visits " time(s)"))
;; => "Ada visited 1 time(s)"

(let [ [first-item second-item & remaining] ["a" "b" "c" "d"]]
  {:first first-item
   :second second-item
   :remaining remaining})
;; => {:first "a", :second "b", :remaining ("c" "d")}

यह विशेष रूप से रिक्वेस्ट मैप्स के लिए उपयोगी है:

(def request
  {:request-method :get
   :params {:id "42"}
   :headers {"accept" "application/json"}})

(let [{:keys [request-method params]} request
      {:keys [id]} params]
  {:method request-method
   :todo-id id})
;; => {:method :get, :todo-id "42"}

if, when, if-let, और when-let

if दो मानों के बीच चयन करता है। Clojure(Script) में केवल false और nil ही असत्य (falsey) हैं; 0, "", और खाली संग्रह सत्य (truthy) हैं।

(if (:done? {:title "Read" :done? true})
  "Done"
  "Open")
;; => "Done"

(if 0 "yes" "no")
;; => "yes"

when का उपयोग "इसे केवल तभी करें यदि परीक्षण सत्य है" के लिए किया जाता है; अन्यथा यह nil लौटाता है।

(when (seq "Ada")
  "There is a name")
;; => "There is a name"

(when (seq "")
  "This will not appear")
;; => nil

if-let और when-let केवल तभी एक नाम को बाइंड करते हैं जब मान सत्य हो। यह पैटर्न अक्सर रूट मैचिंग, कुकी हैंडलिंग और ब्राउज़र रेंडरिंग में दिखाई देता है।

(def request-with-user {:session {:user "Ada"}})

(if-let [user (get-in request-with-user [:session :user])]
  (str "Signed in as " user)
  "Please log in")
;; => "Signed in as Ada"

(when-let [title (get-in {:params {:todo {:title "Ship demo"}}}
                         [:params :todo :title])]
  (str "Todo: " title))
;; => "Todo: Ship demo"

cond, case, और condp

जब आपके पास कई परीक्षण हों तो cond का उपयोग करें। यह उन्हें क्रम में जांचता है।

(defn todo-status [{:keys [done? blocked?]}]
  (cond
    blocked? "blocked"
    done? "done"
    :else "open"))

(todo-status {:done? false :blocked? true})
;; => "blocked"

जब एक मान की तुलना ज्ञात स्थिरांकों (constants) से की जाती है तो case का उपयोग करें।

(defn method-action [request-method]
  (case request-method
    :get "read"
    :post "write"
    :delete "remove"
    "unknown"))

(method-action :post)
;; => "write"

condp इस रेपो में कम सामान्य है, लेकिन तब उपयोगी होता है जब प्रत्येक शाखा एक ही प्रेडिकेट का उपयोग करती है।

(defn score-band [score]
  (condp <= score
    90 :great
    75 :solid
    :needs-practice))

(score-band 82)
;; => :solid

cond->

cond-> एक मान के साथ शुरू होता है और प्रत्येक चरण को केवल तभी लागू करता है जब उसका परीक्षण सत्य हो। यह प्रोजेक्ट इसका उपयोग रिक्वेस्ट मैप्स, हेडर और मिडलवेयर कॉन्फ़िगरेशन बनाने के लिए करता है।

(defn response-map [{:keys [body location]}]
  (cond-> {:status 200
           :headers {}}
    body
    (assoc :body body)

    location
    (assoc-in [:headers "Location"] location)))

(response-map {:body "ok" :location "/todos"})
;; => {:status 200, :headers {"Location" "/todos"}, :body "ok"}

some->

some-> -> की तरह है, लेकिन जैसे ही कोई चरण nil लौटाता है, यह रुक जाता है और nil लौटा देता है। यह तब उपयोगी होता है जब डेटा गायब हो सकता है।

(some-> {:pathname {:groups {:id "42"}}}
        :pathname
        :groups
        :id)
;; => "42"

(some-> nil
        :pathname
        :groups
        :id)
;; => nil

नेस्टेड मैप्स: get-in, assoc-in, और update-in

रिक्वेस्ट मैप्स और रिस्पॉन्स मैप्स नेस्टेड होते हैं। ये सहायक मैन्युअल खुदाई से बचते हैं।

(def response {:status 200 :headers {}})

(assoc-in response [:headers "Content-Type"] "text/plain")
;; => {:status 200, :headers {"Content-Type" "text/plain"}}

(get-in {:params {:todo {:title "Learn get-in"}}}
        [:params :todo :title])
;; => "Learn get-in"

(update-in {:todos {1 {:done? false}}}
           [:todos 1 :done?]
           not)
;; => {:todos {1 {:done? true}}}

atom, @, swap!, और reset! के साथ स्थिति (State)

अधिकांश डेटा अपरिवर्तनीय (immutable) है। जब ऐप को स्थिति बदलने की आवश्यकता होती है, तो यह रेपो एटम (atoms) का उपयोग करता है, उदाहरण के लिए टोडोस, काउंटर और ब्राउज़र UI स्थिति।

(def store
  (atom {:clicks 0
         :chips []}))

@store
;; => {:clicks 0, :chips []}

(swap! store update :clicks inc)
;; => {:clicks 1, :chips []}

(swap! store update :chips conj "REPL")
;; => {:clicks 1, :chips ["REPL"]}

(reset! store {:clicks 0 :chips []})
;; => {:clicks 0, :chips []}

सोर्स फ़ाइलों में, यह रेपो अक्सर ब्राउज़र या सर्वर स्थिति के लिए defonce का उपयोग करता है। defonce का अर्थ है "इसे केवल तभी परिभाषित करें यदि यह पहले से मौजूद नहीं है", जो REPL रीलोड के दौरान उपयोगी है। लाइव पेस्ट डेमो में, सादा def कम आश्चर्यजनक है क्योंकि यह हर बार उदाहरण को रीसेट करता है।

reduce

reduce एक एक्युमुलेटर (accumulator) को ले जाते हुए एक संग्रह के माध्यम से चलता है। सोर्स इसका उपयोग मैप्स, हेडर और कॉन्फ़िगरेशन बनाने के लिए करता है।

(reduce
 (fn [total n]
   (+ total n))
 0
 [10 20 30])
;; => 60

(reduce
 (fn [headers [k v]]
   (assoc headers k v))
 {}
 [ [:content-type "text/plain"]
  [:etag "v1"]])
;; => {:content-type "text/plain", :etag "v1"}

loop और recur

जब आपको म्यूटेबल काउंटर के बिना एक स्पष्ट लूप की आवश्यकता हो तो loop के साथ recur का उपयोग करें। कई मामलों में, map, filter, या reduce सरल होंगे।

(loop [n 3
       result []]
  (if (zero? n)
    result
    (recur (dec n) (conj result n))))
;; => [3 2 1]

जावास्क्रिप्ट इंटरऑप

ClojureScript जावास्क्रिप्ट पर होस्ट किया गया है, इसलिए जब हमें प्लेटफॉर्म की आवश्यकता होती है तो हम सीधे जावास्क्रिप्ट को कॉल कर सकते हैं।

(.log js/console "Hello from ClojureScript")
;; JS कंसोल में प्रिंट करता है

(.toISOString (js/Date.))
;; => ISO स्ट्रिंग के रूप में वर्तमान समय

(.round js/Math 4.6)
;; => 5

API सीमाओं पर clj->js और js->clj का उपयोग करें:

(def json-text
  (.stringify js/JSON (clj->js {:ok true :items [1 2 3]}) nil 2))

json-text
;; => "{\n  \"ok\": true,\n  \"items\": [\n    1,\n    2,\n    3\n  ]\n}"

(js->clj (.parse js/JSON json-text) :keywordize-keys true)
;; => {:ok true, :items [1 2 3]}

नेटिव जावास्क्रिप्ट ऑब्जेक्ट्स के लिए, प्रॉपर्टी एक्सेस स्पष्ट है:

(def js-user #js {:name "Ada" :visits 2})

(.-name js-user)
;; => "Ada"

(aget js-user "visits")
;; => 2

जावास्क्रिप्ट प्रॉमिस

कई ब्राउज़र, Node, Deno, और Bun API प्रॉमिस लौटाते हैं। यह रेपो अक्सर प्रॉमिस चेन को पठनीय बनाने के लिए -> का उपयोग करता है।

(-> (js/Promise.resolve "hello")
    (.then (fn [text]
             (.toUpperCase text)))
    (.then (fn [loud]
             (.log js/console loud)))
    (.catch (fn [error]
              (.error js/console error))))
;; "HELLO" लॉग करता है; एक प्रॉमिस लौटाता है

try, catch, और throw

जब जावास्क्रिप्ट या पार्सिंग कोड थ्रो (throw) कर सकता है तो try का उपयोग करें। ClojureScript में, (catch :default error ...) सामान्य जावास्क्रिप्ट त्रुटियों को पकड़ता है।

(try
  (.parse js/JSON "{bad json")
  (catch :default error
    (.-message error)))
;; => एक JSON पार्स त्रुटि संदेश

(try
  (throw (js/Error. "Boom"))
  (catch :default error
    (str "Caught: " (.-message error))))
;; => "Caught: Boom"

मैक्रो की मूल बातें

मैक्रो कोड के चलने से पहले उसे रूपांतरित करते हैं। हमने पहले ही मैक्रो का उपयोग किया है: ->, ->>, when, cond->, और रेपो के env-var / serve सहायक।

(macroexpand-1
 '(-> {:status 200}
      (assoc :body "ok")))
;; => (assoc {:status 200} :body "ok")

(macroexpand-1
 '(when true
    "runs when true"))
;; => (if true (do "runs when true"))

इस रेपो में, कस्टम मैक्रो जैसे env-var और serve src/fast_twitch/macros.cljc में रहते हैं। नए लोगों के लिए लाइव डेमो के लिए, कस्टम मैक्रो लिखने से पहले कोर मैक्रो पर macroexpand-1 से शुरुआत करें।

एक बार विचार समझ में आ जाने के बाद, कंडीशनल थ्रेडिंग का सामान्य रूप से मूल्यांकन करें:

(cond-> {:status 200}
  true
  (assoc :body "ok")

  false
  (assoc :debug true))
;; => {:status 200, :body "ok"}

उपयोगी लाइब्रेरीज़

लाइब्रेरी स्निपेट्स के लिए, ऐसे प्रोजेक्ट या उदाहरण का उपयोग करें जिसमें क्लासपाथ पर निर्भरता हो। इस रेपो में, examples/07-json-api-malli-openapi में पहले से ही Malli और Integrant दोनों हैं।

Malli

Malli हमें डेटा के साथ डेटा का वर्णन करने देता है। एक स्कीमा केवल एक Clojure(Script) मान है जिसे हम मान्य (validate), समझा (explain), रूपांतरित (transform), या डॉक्स के लिए पुन: उपयोग कर सकते हैं।

(require '[malli.core :as m])
(require '[malli.error :as me])

(def TodoCreate
  [:map
   {:closed true}
   [:title [:string {:min 1}]]
   [:done {:optional true} :boolean]])

(m/validate TodoCreate {:title "Learn Malli" :done false})
;; => true

(m/validate TodoCreate {:title "" :extra "not allowed"})
;; => false

(me/humanize
 (m/explain TodoCreate {:title "" :extra "not allowed"}))
;; => {:title ["should be at least 1 character"],
;;     :extra ["disallowed key"]}

जब आप एक पुन: प्रयोज्य सत्यापन फंक्शन चाहते हैं तो m/validator का उपयोग करें:

(require '[malli.core :as m])

(def TodoCreate
  [:map
   {:closed true}
   [:title [:string {:min 1}]]
   [:done {:optional true} :boolean]])

(def valid-todo? (m/validator TodoCreate))

(valid-todo? {:title "Ship the demo"})
;; => true

(valid-todo? {:done true})
;; => false

शुरुआती लोगों के लिए महत्वपूर्ण विचार: कई कंडीशनल में सत्यापन नियमों को छिपाने के बजाय, हम मान्य डेटा के आकार को एक छोटे डेटा मान में रखते हैं।

Integrant

Integrant एक कॉन्फ़िगरेशन मैप से एक ऐप शुरू करता है। प्रत्येक शीर्ष-स्तरीय कुंजी सिस्टम के एक हिस्से का वर्णन करती है, और ig/ref कहता है कि एक हिस्सा दूसरे पर निर्भर करता है।

(require '[integrant.core :as ig])

(defmethod ig/init-key ::settings [_ options]
  options)

(defmethod ig/init-key ::greeter [_ {:keys [settings]}]
  (fn [name]
    (str (:prefix settings) ", " name "!")))

(def config
  {::settings {:prefix "Hello"}
   ::greeter {:settings (ig/ref ::settings)}})

(def system (ig/init config))

((::greeter system) "Ada")
;; => "Hello, Ada!"

क्या हुआ:

  • ::settings पहले इनिशियलाइज़ हुआ।
  • ::greeter को इनिशियलाइज़्ड सेटिंग्स प्राप्त हुईं।
  • system इनिशियलाइज़्ड भागों का एक मैप है।

उन भागों के लिए ig/halt-key! के साथ क्लीनअप जोड़ें जो संसाधनों के मालिक हैं:

(require '[integrant.core :as ig])

(defmethod ig/init-key ::store [_ initial-state]
  (atom initial-state))

(defmethod ig/halt-key! ::store [_ store]
  (reset! store {}))

(def store-system
  (ig/init {::store {:todos []}}))

@(::store store-system)
;; => {:todos []}

(ig/halt! store-system)
;; => रिटर्न मान को अनदेखा करें; halt! साइड इफेक्ट्स के लिए है

शुरुआती लोगों के लिए महत्वपूर्ण विचार: वायरिंग को डेटा के रूप में रखें, और स्टार्टअप और शटडाउन व्यवहार को उन डेटा कुंजियों से जुड़े छोटे तरीकों में रखें।

FastTwitch

सोर्स: FastTwitch 🏃💨

  • प्रमुख जावास्क्रिप्ट रनटाइम्स के ऊपर वेब बैकएंड
  • वेब बैकएंड सामान करने के लिए वास्तव में जो आवश्यक है उसे कैप्चर करें
  • कई प्रयास
  • ExpressJs
  • Fastify
  • अब केवल fetch API और वेब मानक
  • ring से प्रेरित API