(defn get-y [point]
(let [[_ y] point] ;; x-value of point is unused, so mark it with _
y))
關鍵字會快取並內部化。這表示關鍵字會在程式中重複使用(減少記憶體),而相等性檢查會變成身分檢查(速度很快)。此外,關鍵字可以呼叫,以在映射中查詢自己,因此這能啟用從映射集合中萃取特定欄位的常見模式。
「reader」頁面將關鍵字定義為「類似符號」,而符號「以非數字字元開頭」,因此原始意圖是 :1
會無效。事實上,它之所以可以讀取,完全是因為關鍵字正規表示式中的一個錯誤。這個錯誤在 1.6 alpha 版中已修復,但我們很快發現許多實際專案都在使用這些關鍵字。為了避免中斷現有的工作程式碼,變更已回滾,且此格式將持續支援。(仍有一些開放的問題需要在程式碼和/或文件中釐清這一點。)
請注意,名稱空間關鍵字的名稱開頭為數字時,從未可讀或有效,例如 :foo/1
。但是,自動解析的關鍵字(例如 ::1
)可以讀取,但無法從列印轉換為讀取。
一般來說,最好避免使用開頭為數字的關鍵字,除非是在狹窄且受控的範圍內。
keyword
函式可根據使用者資料或其他輸入來源,用於以程式方式建立關鍵字。類似地,namespace
和 name
函式可將關鍵字拉回並分解為組成部分。程式通常會使用此功能建立關鍵字,以用作識別碼或對應鍵,而不會列印和讀回該資料。
由於這個用例(以及一般的效能),不會對 keyword
(或 symbol
)的輸入執行驗證檢查,這使得可以建立關鍵字,在列印時無法讀回為關鍵字(因為有空白或其他不允許的字元)。如果您覺得這很重要,您應該先驗證關鍵字輸入,然後再建立關鍵字。
讀取器會取得文字(Clojure 來源)並傳回 Clojure 資料,然後編譯和評估。讀取器巨集會告訴 Clojure 讀取器如何讀取非典型的 S 表達式(例如引號 '
和匿名函式 #()
)。讀取器巨集可定義讀取器讀取的全新語法(例如:JSON、XML 或其他格式) - 這是比一般巨集(在編譯時才發揮作用)更強大的語法功能。
但是,與 Lisp 不同,Clojure 不允許使用者擴充這組讀取器巨集。這避免了建立其他使用者無法讀取的程式碼的可能性(因為他們沒有適當的讀取器巨集)。Clojure 透過標記式字面值提供了一些讀取器巨集的威力,讓您可以建立通用可讀的資料,而且仍然可擴充。
另請參閱Clojure 歷史文件中的相關章節(搜尋「讀取器巨集」)。
在 Clojure 中,_ 作為符號沒有特別的意義。然而,使用 _(或前導 _)來表示在表達式中不會使用的繫結是一種慣例。一個常見的情況是在順序解構中跳過不需要的值
(defn get-y [point]
(let [[_ y] point] ;; x-value of point is unused, so mark it with _
y))
?
和 !
在 Clojure 中作為函式名稱的一部分沒有特別的意義。然而,使用尾隨 ?
來表示謂詞函式和使用尾隨 !
來表示具有副作用的函式是一種慣例。更具體地說,尾隨 ?
表示謂詞嚴格返回布林結果(true
或 false
)。尾隨 !
最初旨在表示函式具有副作用,這會使其在ref 交易(在軟體交易記憶體意義上)中使用不安全。在實際應用中,!
的使用較不一致,有時會更廣泛地用於表示任何類型的副作用行為。
#()
始終擴充為在給定的表達式周圍包含括號,因此在這種情況下,它會產生 (fn [x] ([x]))
,當呼叫向量時會失敗。請改用向量函式 #(vector %)
或僅使用 vector
,這是正在描述的函式。
大多數 Clojure 資料結構操作,包括 conj
(連接),都旨在為使用者提供效能預期。對於 conj
,預期是在此操作有效率的地方進行插入。串列(作為連結串列)只能在前面進行常數時間插入。向量(索引)設計為在後面擴充。在選擇要使用的資料結構時,使用者應考慮這一點。在 Clojure 中,向量的使用頻率高得多。
如果你的目標特別是「新增到集合的前面」,那麼適當使用的函式是 cons
,它會永遠新增到前面。但請注意,這會產生一個序列,而不是原始集合類型的實例。
通常你應該將 Clojure 核心函式分為這兩個類別
資料結構函式 - 採用資料結構並回傳該資料結構的修改版本 (conj、disj、assoc、dissoc 等)。這些函式總是先採用資料結構。
序列函式 - 採用「可序列化」並回傳可序列化。[我們通常會避免承諾回傳值實際上是 ISeq 的實例 - 這在某些情況下允許效能最佳化。] 範例包括 map、filter、remove 等。所有這些函式都將可序列化採用為最後一個。
聽起來你正在使用後者,但卻期待前者的語意 (這是 Clojurists 新手常見的問題!)。如果你想要套用序列函式,但對輸出資料結構有更多控制,有許多方法可以做到這一點。
使用資料結構等效項,例如 mapv 或 filterv 等 - 這是非常有限的集合,它讓你執行這些操作,但回傳資料結構,而不是可序列化。(mapv inc (filterv odd? [1 2 3]))
將序列轉換的結果倒回資料結構中,使用 into:(into [] (map inc (filter odd? [1 2 3])))
使用轉換器 (可能搭配 into
) - 這與 #2 有相同的效果,但轉換組合可以更有效率地套用,而不會建立任何序列 - 只有最終結果會建立:(into [] (comp (filter odd?) (map inc)) [1 2 3])
。當你處理較大的序列或更多轉換時,這會對效能產生顯著的差異。
請注意,所有這些都是急切轉換 - 當你呼叫它們時,它們會產生輸出向量。原始序列版本 (map inc (filter odd? [1 2 3]))
是延遲的,只會在需要時產生值 (在後台使用分塊以提高效能)。這些都不是對或錯,但它們在不同的情況下都很有用。
主要的集合運算元會先出現。這樣一來,就可以撰寫 ->
及其同類,而且它們的位置與它們是否具有可變 arity 參數無關。在物件導向語言和 Common Lisp (slot-value
、aref
、elt
) 中有這樣的傳統。
思考序列的一種方式是,它們從左邊讀取,從右邊饋入
<- [1 2 3 4]
大多數序列函式會使用並產生序列。因此,一種將其視覺化的方式是將其視為鏈條
map <- filter <- [1 2 3 4]
思考許多 seq 函數的一種方式是,它們在某種程度上已參數化
(map f) <- (filter pred) <- [1 2 3 4]
因此,序列函數最後採用其來源,以及它們之前的任何其他參數,而 partial 允許如上所示的直接參數化。這在函數語言和 Lisp 中是一種傳統。
請注意,這與最後採用主要運算元不同。有些序列函數有多個來源 (concat、interleave)。當序列函數是可變參數時,通常在它們的來源中。
改編自 Rich Hickey 的評論。
在執行一系列轉換時,序列會在每次轉換之間建立中間 (快取) 序列。換能器建立單一複合轉換,在輸入上執行一次急切傳遞。這些是不同的模型,兩者都很有用。
換能器的效能優點
來源集合反覆運算 - 在可還原輸入 (集合和其他項目) 上使用時,避免建立不必要的輸入集合序列 - 有助於記憶體和時間。
中間序列和快取值 - 由於轉換發生在單次傳遞中,因此您移除所有中間序列和快取值建立 - 再一次,有助於記憶體和時間。隨著輸入集合大小或轉換次數的增加,前一個項目和這個項目的組合將開始獲得極大的優勢 (但對於其中任何一個小數字,分塊序列可能異常快速,並且會競爭)。
換能器的設計/使用優點
轉換組合 - 如果某些使用案例將轉換組合與轉換應用分開,則設計會更簡潔。換能器支援這一點。
急切性 - 換能器非常適合在急切處理轉換 (並可能遇到任何錯誤) 比延遲更重要的案例
資源控制 - 由於您可以更進一步控制何時橫向移動輸入集合,因此您也知道處理何時完成。因此,更容易釋放或清理輸入資源,因為您知道何時會發生這種情況。
序列的效能優點
延遲 - 如果您只需要部分輸出 (例如使用者正在決定要使用多少),則延遲序列通常可以在延遲處理方面更有效率。特別是,序列可以延遲中間結果,但換能器使用拉取模型,會急切產生所有中間值。
無限串流 - 由於換能器通常會急切使用,因此它們與無限值串流不匹配
序列的設計優點
消費者控制 - 從 API 傳回 seq 讓您可以將輸入 + 轉換組合成賦予消費者控制權的東西。Transducers 不太適用於此(但對於輸入和轉換分開的情況會比較好用)。
在某個時間點,元資料比現在更難使用(私人 defn 的語法為 #^{:private true}
),而 defn-
似乎值得作為「簡單」版本建立。元資料支援改進並變得「可堆疊」,這讓獨立元資料的組合變得更容易。與其為所有 def 形式建立私人變體,不如在需要時在 def
或其他 def 形式上使用 ^:private
元資料。
當使用 partial
(或其他高階函數組合器,例如 comp
、juxt
等)時,在呼叫 partial
之前,會將任何引用的變數評估為函數物件,因此它擷取的是任何函數變數引用的值,而不是變數本身。例如:(partial my-fn 100)
將 my-fn
評估為 #'my-fn
的目前函數值,然後使用它呼叫 partial
。如果在 REPL 中重新繫結 my-fn
變數,先前的 partial
函數將「看不到」這些變更,因為它只有函數,沒有變數。
如果您發現這是互動式開發中的問題,您可以插入一層間接層。一個選項是改用變數參考 #'myfn
,或者您可以使用一個獨立的 fn
或 defn
來重新包含變數解除參考。或者,您可以使用 fn
或匿名函數文字取代 partial。
一般來說,這在執行中的應用程式中不是問題(因為變數通常不會重新繫結),但可能會發生在互動式 REPL 開發中。
spec 處於 alpha 版,表示 API 仍可能變更。spec 從 Clojure 核心程式碼中分離出來,以便 spec 可以獨立於 Clojure 主要版本更新。在某個時間點,spec 的 API 將被視為穩定,屆時將移除 alpha。spec 的下一個版本正在 alpha.spec 中開發。
這個問題沒有單一的正確答案。對於資料規格,通常將它們放在自己的命名空間中會很有用,這可能會與資料規格中使用的限定符號相符或不相符。將限定符號與命名空間相符,可以在規格中和在其他命名空間的別名中使用自動解析的關鍵字,但也會將它們交織在一起,使重構變得更複雜。
對於函數規格,大多數人會將它們放在要套用的函數之前或之後,或放在一個獨立的命名空間中,必要時可以選擇性地需要(用於測試或驗證)。在後一種情況下,Clojure 核心遵循使用 foo.bar.specs 來保存 foo.bar 中函數的函數規格的模式。
正規表示式運算(cat、alt、*、+、? 等)總是描述順序集合中的元素。它們本身不是規格。在規格脈絡中使用時,它們會被強制轉換為規格。巢狀正規表示式運算會結合起來,形成同一個順序集合上的單一正規表示式規格。
要驗證巢狀集合,請使用 s/spec
包住內部正規表示式,在正規表示式運算之間強制一個規格界線。
Instrument 的目的是驗證函數是否根據其參數規格被呼叫。也就是說,函數是否被正確呼叫?這個功能應該在開發期間使用。
檢查函數是否正確運作是一項測試時間活動,這應該使用 check
函數來檢查,它會實際使用產生的參數呼叫函數,並在每次呼叫時驗證回傳值和函數規格。
有,設定 Java 系統屬性 -Dclojure.spec.skip-macros=true
,在巨集展開期間就不會檢查任何巨集規格。
Spec 的一般哲學是「開放」規格,其中地圖可以包含額外的金鑰,超出在 s/keys 規格中指定為必要或可選的內容。達成受限金鑰集的一種方法是 s/and
一個額外的約束
(s/def ::auth
(s/and
(s/keys :req [::user ::password])
#(every? #{::user ::password} (keys %))))
目前不行。這正在考慮中,作為下一個版本的規格。
這些每一個實際上都解決了不同的使用案例。
在對現有的記憶體中資料(在映射或向量中)計算轉換時,reducers 最適合細粒度的資料並行性。通常,當你有數千個小資料項目要計算,並且有許多核心可以執行這項工作時,這是最好的。任何被描述為「令人尷尬的並行」的事情。
期貨最適合將工作推送到背景執行緒,稍後再取用(或用於並行執行 I/O 等待)。它更適合用於大型分塊任務(在背景中提取大量資料)。
core.async 主要用於組織應用程式的子系統或內部結構。它有通道(佇列)可傳遞值,從一個「子程序」(go 區塊)傳遞到另一個。因此,您實際上在如何分解程式時獲得了並行性和架構優點。您只能在 core.async 中獲得的殺手級功能是能夠等待來自多個通道的 I/O 事件,以取得任何通道的第一個回應(透過 alt/alts)。承諾也可以用於傳遞獨立執行緒/子程序之間的單一值,但它們僅限單一傳遞。
pmap、java.util 佇列和執行器等工具,以及 claypoole 等函式庫正在執行粗略層級的「任務」並行性。這裡與 core.async 有些重疊,core.async 具有非常有用的轉換器友善管線功能。
這通常在使用 future
、pmap
、agent-send
或呼叫這些函式的其他函式的程式中提出。當此類程式結束時,會在退出前暫停 60 秒。若要解決此問題,請在程式結束時呼叫 shutdown-agents。
Clojure 使用兩個內部執行緒池來服務期貨和代理函式執行。兩個池都使用非守護執行緒,而且在任何非守護執行緒仍執行時,JVM 都不會退出。特別是,服務期貨和代理傳送呼叫的池使用具有 60 秒逾時時間的執行器快取執行緒池。在上述情況中,程式會等到背景執行緒完成其工作,而且執行緒逾時後才會退出。
如果預設包含讀取,則 STM 會較慢(因為更多交易需要可序列化性)。然而,在許多情況下,不需要包含讀取。因此,使用者可以在必要時選擇接受效能損失,並在不需要時獲得更快的效能。
不會(雖然這很常見)。一個命名空間可以使用 `load` 載入次要檔案,並在這些檔案中使用 `in-ns` 來保留命名空間,從而分割成多個檔案(clojure.core 就是這樣定義的)。此外,也可以在單一檔案中宣告多個命名空間(雖然這很罕見)。
ns 是執行多項任務的巨集
建立新的內部命名空間物件(如果尚未存在)
將該命名空間設為新的目前命名空間(*ns*
)
自動引用 clojure.core 中的所有變數,並匯入 java.lang 中的所有類別
根據指定需求/引用其他命名空間和變數
(以及其他選用事項)
ns 不會傳回函式或任何你可以呼叫的內容,如你所建議的。
雖然 ns 通常會放在 clj 檔案的開頭,但它實際上只是一個常規巨集,並且可以在 REPL 中以相同的方式呼叫。它也可以在單一檔案中使用多次(儘管這會讓大多數 clj 程式設計師感到驚訝,而且在 AOT 中可能無法如預期般運作)。
任何已直接連結的內容都看不到變數的重新定義。例如,如果你在 clojure.core 中重新定義某個內容,則使用該變數的其他核心部分將看不到重新定義(不過,你在 REPL 中新編譯的任何內容都會看到)。在實務上,這通常不是問題。
對於你自己的應用程式部分,你可能希望只在你為生產環境建置和部署時啟用直接連結,而不是在你於 REPL 中開發時使用它。或者,如果你想要總是允許重新定義,則可能需要使用 ^:redef 標記應用程式的部分,或者使用 ^:dynamic 標記動態變數。
使用 $ 來分隔外部類別和內部類別名稱。例如:java.util.Map$Entry
是 Map 內部的 Entry 內部類別。
基本類型可以在封裝類別上找到,作為靜態 TYPE 欄位,例如:Integer/TYPE
。
Java 將尾隨變參數參數視為陣列,並且可以透過傳遞明確陣列從 Clojure 中呼叫它。
範例
;; Invoke static Arrays.asList(T... a)
(java.util.Arrays/asList (object-array [0 1 2]))
;; Invoke static String.format(String format, Object... args)
(String/format "%s %s, %s" (object-array ["March" 1 2016]))
;; For a primitive vararg, use the appropriate primitive array constructor
;; Invoke put(int row, int col, double... data)
(.put o 1 1 (double-array [2.0]))
;; Passing at least an empty array is required if there are no varargs
(.put o 1 1 (double-array []))
;; into-array can be used to create an empty typed array
;; Invoke getMethod(String name, Class... parameterTypes) on a Class instance
(.getMethod String "getBytes" (into-array Class []))
Java 9 新增了模組系統,允許將程式碼分割成模組,其中模組外的程式碼無法呼叫模組內的程式碼,除非模組已將其匯出。Java 中受此變更影響的其中一個領域是反射存取。當 Clojure 遇到 Java 互操作呼叫,但沒有目標物件或函數引數的足夠類型資訊時,它會使用反射。例如
(def fac (javax.xml.stream.XMLInputFactory/newInstance))
(.createXMLStreamReader fac (java.io.StringReader. ""))
這裡的 fac
是 com.sun.xml.internal.stream.XMLInputFactoryImpl
的實例,它是 javax.xml.stream.XMLInputFactory
的延伸。在 java.xml 模組中,javax.xml.stream 是匯出的套件,但 XMLInputFactoryImpl 是該套件中公開抽象類別的內部實作。這裡呼叫 createXMLStreamReader
將會是反射的,而 Reflector 會嘗試根據實作類別呼叫方法,而模組外部無法存取該類別,因此會產生
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by clojure.lang.Reflector (file:/.m2/repository/org/clojure/clojure/1.10.0/clojure-1.10.0.jar) to method com.sun.xml.internal.stream.XMLInputFactoryImpl.createXMLStreamReader(java.io.Reader)
WARNING: Please consider reporting this to the maintainers of clojure.lang.Reflector
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
這裡首先要注意的是,這是一個警告。從 Java 9 到所有目前的版本,都將允許進行呼叫,而程式碼將繼續執行。
有幾個潛在的解決方法
或許最好的方法是提供類型提示給匯出的類型,這樣呼叫就不再是反射的
(.createXMLStreamReader ^javax.xml.stream.XMLInputFactory fac (java.io.StringReader. ""))
從 Clojure 1.10 開始,使用 --illegal-access=deny
關閉非法存取。Java 反射系統將提供必要的回饋給 Clojure,以偵測呼叫透過無法存取的類別並非選項。Clojure 將找到公開的呼叫路徑,而不會發出警告。
使用 JVM 模組系統旗標 (--add-exports
等) 強制匯出內部套件以避免警告。不建議這樣做。
如果從警告中很難判斷反射發生在哪裡,新增旗標可能會有幫助
--illegal-access=debug
例如,透過 Clojure CLI,使用 -J
選項 (或作為 deps.edn
中別名下的 :jvm-opts
的一部分)
clj -J--illegal-access=debug
由於 Clojure 專注於不可變資料,因此通常不會特別重視資料封裝。由於資料是不可變的,因此無需擔心其他人修改值。同樣地,由於 Clojure 資料被設計為可直接操作,因此直接存取資料非常有價值,而不是將其包裝在 API 中。
所有 Clojure 變數都是全域可用的,因此在命名空間中封裝函數的方式並不多。不過,標記變數為私有的功能(使用 defn-
表示函數,或使用 ^:private
搭配 def
表示值)對開發人員來說很方便,可以指出 API 的哪些部分應視為可公開使用,哪些部分屬於實作。
否。Clojure CLI 專注於 a) 建立類別路徑,以及 b) 啟動 Clojure 程式。它不會(而且不會)建立成品、部署成品等,儘管它們可能會透過工具和函式庫來協助這些動作。
tools.deps 旨在提供用於相依性解析和類別路徑建構的程式化建構區塊。clj/clojure 將這些包裝成命令列形式,可用於執行 Clojure 程式。你可以組合這些部分來執行許多其他動作。
否。目前有其他工具可以執行此動作,或可以新增到現有功能中,但這並非最初的目標。
如果你不需要任何額外的相依性,只要將 #!/usr/bin/env clojure
放為第一行即可。請注意,clojure
不會自動呼叫 -main
函數,因此請確保你的檔案不只是定義函數而已。你可以在 *command-line-args*
中找到命令列引數。
如果你需要額外的相依性,請試試以下方法,感謝 Dominic Monroe 提供,將 funcool/tubax
替換成你需要的任何相依性
#!/bin/sh "exec" "clojure" "-Sdeps" '{:deps {funcool/tubax {:mvn/version "0.2.0"}}}' "$0" "$@" ;; Clojure code goes here.
歸結為兩個原因
保護 Clojure 免於未來的法律挑戰,這些挑戰可能會阻止企業採用 Clojure。
讓 Clojure 能夠在有利的情況下重新取得不同開放原始碼授權的授權。
這是 Adobe EchoSign 的一個怪癖,特別會發生在電子郵件帳戶已與 Adobe EchoSign 帳戶關聯的使用者身上。在這些情況下,EchoSign 會在主旨行中使用您現有個人資料中的公司名稱,而不是表單上簽署的個人姓名。別擔心!這不會造成任何影響 - 協議已簽署並附加在電子郵件中。
Rich Hickey 偏好評估附加到 JIRA 票證的修補程式。這並不是為了增加貢獻者的困難度,或出於法律原因,而是因為工作流程偏好。請參閱 開發頁面 以取得更多詳細資訊。
連結 至 Rich Hickey 在 2012 年 10 月於 Clojure Google 群組中發布的訊息,主題為此。
人們經常詢問 Clojure 的「原生」版本,也就是不依賴 JVM 的版本。ClojureScript 自我託管是目前的一種途徑,但可能只適用於部分使用案例。GraalVM 專案可 used to create a standalone binary executable。使用 Graal 產生的原生映像啟動速度極快,但與完整的 JVM 相比,優化效能的機會可能較少。
然而,當人們詢問「Clojure 的原生版本」時,他們可能想像的都不是這些,而是語言的版本,它不是由 JVM 託管,且可直接編譯為原生可執行檔,可能透過類似 LLVM 的東西。Clojure 從 JVM 中獲取大量的效能、可攜性和功能,並高度依賴像世界級垃圾收集器之類的東西。建立「Clojure 原生」將需要大量的工程才能製作出 Clojure 的版本,而這個版本會更慢(可能慢很多)、可攜性更低,且功能大幅減少(因為 Clojure 函式庫高度依賴 JDK)。Clojure 核心團隊沒有計畫進行這項工程,但這會是任何人的絕佳學習專案,我們鼓勵您嘗試看看!
原始作者:Alex Miller