Clojure

閱讀 Clojure 字元

( …​ ) - 清單

清單是作為連結清單實作的順序異質集合。

包含三個值的清單

(1 "two" 3.0)

[ …​ ] - 向量

向量是順序、索引、異質集合。索引從 0 開始。

從包含三個值的向量中擷取索引 1 處值的範例

user=> (get ["a" 13.7 :foo] 1)
13.7

{ …​ } - 對應

對應是使用交替鍵和值指定的異質集合

user=> (keys {:a 1 :b 2})
(:a :b)

# - 分派字元

您會看到此字元出現在其他字元旁邊,例如 #(#"

# 是特殊字元,會告訴 Clojure 讀取器(將 Clojure 來源視為 Clojure 資料並「讀取」它的元件)如何使用讀取表來詮釋下一個字元。儘管有些 Lisp 允許使用者擴充讀取表,但 Clojure 不允許

在語法引用中建立 已產生符號 時,# 也會用於符號的結尾

#{ …​ } - 集合

請參閱 # 以取得更多詳細資料。

#{…​} 定義一個集合(一個唯一值集合),特別是一個 hash-set。下列為等效的

user=> #{1 2 3 4}
#{1 2 3 4}
user=> (hash-set 1 2 3 4)
#{1 2 3 4}

集合不能包含重複項,因此在這種情況下,set 讀取器會擲回例外,因為它是一個無效的文字。當項目新增到集合時,如果值已存在,它們只會被捨棄。

user=> #{1 2 3 4 1}
Syntax error reading source at (REPL:83:13).
Duplicate key: 1

#_ - 捨棄

請參閱 # 以取得更多詳細資料。

#_ 告訴讀取器完全忽略下一個形式。

user=> [1 2 3 #_ 4 5]
[1 2 3 5]

請注意,#_ 後面的空格是可選的,所以

user=> [1 2 3 #_4 5]
[1 2 3 5]

也行得通。另外請注意,捨棄字元在 edn 中有效。

一個巧妙的技巧是,可以堆疊多個 #_ 來省略多個形式

user=> {:a 1, #_#_ :b 2, :c 3}
{:a 1, :c 3}

文件建議「#_ 後面的形式會被讀取器完全略過(這比 comment 巨集更徹底地移除,後者會產生 nil)。這對於除錯情況或多行註解很有用。

#"…​" - 正規表示式

請參閱 # 以取得更多詳細資料。

#" 表示正規表示式的開頭

user=> (re-matches #"^test$" "test")
"test"

此形式會在 讀取時間 編譯成特定主機的 regex 機制,但它在 edn 中不可用。請注意,在 Clojure 中使用 regex 時,不需要 Java 字串跳脫

#(…​) - 匿名函式

請參閱 # 以取得更多詳細資料。

#( 開始內嵌函式定義的簡寫語法。下列兩個程式碼片段類似

; anonymous function taking a single argument and printing it
(fn [line] (println line))

; anonymous function taking a single argument and printing it - shorthand
#(println %)

讀取器會將匿名函式擴充為函式定義,其元數(它所接收的引數數量)由 % 佔位符的宣告方式定義。請參閱 % 字元以了解有關元數的討論。

user=> (macroexpand `#(println %))
(fn* [arg] (clojure.core/println arg)) ; argument names shortened for clarity

#' - 變數引號

#' 是變數引號,會擴充為對 var 函式的呼叫

user=> (read-string "#'foo")
(var foo)
user=> (def nine 9)
#'user/nine
user=> nine
9
user=> (var nine)
#'user/nine
user=> #'nine
#'user/nine

使用時,它會嘗試傳回所引用的變數。當您想要討論參考/宣告而不是它所代表的值時,這很有用。請參閱元資料 (^) 討論中的 meta 使用方式。

請注意,變數引號在 edn 中不可用。

## - 符號值

Clojure 可以讀取和列印符號值 ##Inf##-Inf##NaN。這些值在 edn 中也可用。

user=> (/ 1.0 0.0)
##Inf
user=> (/ -1.0 0.0)
##-Inf
user=> (Math/sqrt -1.0)
##NaN

#inst#uuid#js 等 - 標記文字

標記文字在 edn 中定義,並由 Clojure 和 ClojureScript 讀取器原生支援。#inst#uuid 標籤由 edn 定義,而 #js 標籤由 ClojureScript 定義。

我們可以使用 Clojure 的 read-string 來讀取標記文字(或直接使用它)

user=> (type #inst "2014-05-19T19:12:37.925-00:00")
java.util.Date ;; this is host dependent
user=> (read-string "#inst \"2014-05-19T19:12:37.925-00:00\"")
#inst "2014-05-19T19:12:37.925-00:00"

標記文字會告訴讀者如何解析文字值。其他常見用法包括用於表示 UUID 的 #uuid,以及在 ClojureScript 世界中極為常見的標記文字用法 #js,可用於將 ClojureScript 資料結構直接轉換為 JavaScript 結構。請注意,#js 沒有遞迴轉換,因此如果您有巢狀資料結構,請使用 clj->js

請注意,儘管 #inst#uuid 在 edn 中可用,但 #js 不可用。

%%n%& - 匿名函數參數

% 是匿名函數 #(...) 中的參數,如 #(* % %)

當匿名函數展開時,它會變成 fn 形式,而 % 參數會被 gensym 化的名稱取代(這裡我們使用 arg1 等以提高可讀性)

user=> (macroexpand `#(println %))
(fn* [arg1] (clojure.core/println arg1))

數字可以放在 % 之後,以表示參數位置(從 1 開始)。匿名函數的元數會根據最高的數字 % 參數來決定。

user=> (#(println %1 %2) "Hello " "Clojure")
Hello Clojure ; takes 2 args
user=> (macroexpand `#(println %1 %2))
(fn* [arg1 arg2] (clojure.core/println arg1 arg2)) ; takes 2 args

user=> (#(println %4) "Hello " "Clojure " ", Thank " "You!!")
You!! ; takes 4 args, doesn't use first 3 args
user=> (macroexpand `#(println %4))
(fn* [arg1 arg2 arg3 arg4] (clojure.core/println arg4)) ; takes 4 args doesn't use 3

您不必使用參數,但您需要按照您預期外部呼叫者傳遞參數的順序來宣告它們。

%%1 可以互換使用

user=> (macroexpand `#(println % %1)) ; use both % and %1
(fn* [arg1] (clojure.core/println arg1 arg1)) ; still only takes 1 argument

另外還有 %&,這是變參匿名函數中用於表示「其餘」參數(在最高命名的匿名參數之後)的符號。

user=> (#(println %&) "Hello " "Clojure " ", Thank " "You!!")
(Hello Clojure , Thank You!! ) ; takes n args
user=> (macroexpand '#(println %&))
(fn* [& rest__11#] (println rest__11#))

匿名函數和 % 不是 edn 的一部分。

@ - 解引用

@ 會展開成對 deref 函數的呼叫,因此這兩個形式是相同的

user=> (def x (atom 1))
#'user/x
user=> @x
1
user=> (deref x)
1
user=>

@ 用於取得參考的目前值。上述範例使用 @ 來取得 原子 的目前值,但 @ 可以應用於其他事物,例如 futuredelaypromises 等,以強制計算並可能封鎖。

請注意,@ 在 edn 中不可用。

^(和 #^) - 元資料

^ 是元資料標記。元資料是 Clojure 中可以附加到各種形式的值的映射(帶有簡寫選項)。這為這些形式提供了額外的資訊,可用於文件、編譯警告、類型提示和其他功能。

user=> (def ^{:debug true} five 5) ; meta map with single boolean value
#'user/five

我們可以使用 meta 函數存取元資料,該函數應針對宣告本身執行(而不是針對傳回值執行)

user=> (def ^{:debug true} five 5)
#'user/five
user=> (meta #'five)
{:ns #<Namespace user>, :name five, :column 1, :debug true, :line 1, :file "NO_SOURCE_PATH"}

由於我們這裡只有一個值,因此我們可以使用宣告元資料 ^:name 的簡寫符號,這對於旗標很有用,因為值將設定為 true。

user=> (def ^:debug five 5)
#'user/five
user=> (meta #'five)
{:ns #<Namespace user>, :name five, :column 1, :debug true, :line 1, :file "NO_SOURCE_PATH"}

^ 的另一個用途是類型提示。這些提示用於告訴編譯器值將是什麼類型,並允許它執行特定於類型的最佳化,從而可能使結果代碼更快

user=> (def ^Integer five 5)
#'user/five
user=> (meta #'five)
{:ns #<Namespace user>, :name five, :column 1, :line 1, :file "NO_SOURCE_PATH", :tag java.lang.Integer}

我們可以在該範例中看到設定了 :tag 屬性。

您也可以堆疊簡寫符號

user=> (def ^Integer ^:debug ^:private five 5)
#'user/five
user=> (meta #'five)
{:ns #<Namespace user>, :name five, :column 1, :private true, :debug true, :line 1, :file "NO_SOURCE_PATH", :tag java.lang.Integer}

最初,meta 宣告為 #^,現在已棄用(但仍然有效)。稍後,這簡化為僅 ^,而這是在大多數 Clojure 中會看到的,但偶爾會在較舊的代碼中遇到 #^ 語法。

請注意,edn 中有元資料,但沒有類型提示。

' - 引號

引號用於指示應讀取但不要評估下一個形式。讀取器將 ' 展開為對 quote 特殊形式的呼叫。

user=> (1 3 4) ; fails as it tries to invoke 1 as a function

Execution error (ClassCastException) at myproject.person-names/eval230 (REPL:1).
class java.lang.Long cannot be cast to class clojure.lang.IFn

user=> '(1 3 4) ; quote
(1 3 4)

user=> (quote (1 2 3)) ; using the longer quote method
(1 2 3)
user=>

; - 註解

; 開始一行註解,並忽略從其起始點到行尾的所有輸入。

user=> (def x "x") ; this is a comment
#'user/x
user=> ; this is a comment too
<returns nothing>

在 Clojure 中,通常使用多個分號來提高可讀性或強調,但這些對 Clojure 來說都是一樣的

;; This is probably more important than

; this

: - 關鍵字

: 是關鍵字的指示符。關鍵字通常用作映射中的鍵,它們提供比字串更快的比較和更低的記憶體開銷(因為實例會被快取並重複使用)。

user=> (type :test)
clojure.lang.Keyword

或者,您可以使用 keyword 函數從字串建立關鍵字

user=> (keyword "test")
:test

關鍵字也可以作為函數呼叫,以在映射中將它們自己視為鍵來查詢

user=> (def my-map {:one 1 :two 2})
#'user/my-map
user=> (:one my-map) ; get the value for :one by invoking it as function
1
user=> (:three my-map) ; it can safely check for missing keys
nil
user=> (:three my-map 3) ; it can return a default if specified
3
user => (get my-map :three 3) ; same as above, but using get
3

:: - 自動解析關鍵字

:: 用於在目前的命名空間中自動解析關鍵字。如果未指定限定符,它將自動解析為目前的命名空間。如果指定了限定符,它可以使用目前的命名空間中的別名

user=> :my-keyword
:my-keyword
user=> ::my-keyword
:user/my-keyword
user=> (= ::my-keyword :my-keyword)
false

在建立巨集時,這很有用。如果您要確保呼叫巨集命名空間中另一個函數的巨集正確擴展為呼叫該函數,則可以使用 ::my-function 來指稱完全限定的名稱。

請注意,edn 中沒有 ::

#:#:: - 命名空間映射語法

命名空間對應語法在 Clojure 1.9 中新增,用於在對應中具有共用命名空間的鍵或符號時指定預設命名空間內容。

#:ns 語法指定命名空間對應前綴 n 別名在命名空間對應前綴中,其中 ns 是命名空間的名稱,前綴在對應的開啟大括號 { 之前。

例如,下列具有命名空間語法的對應文字

#:person{:first "Han"
         :last "Solo"
         :ship #:ship{:name "Millennium Falcon"
                      :model "YT-1300f light freighter"}}

讀取為

{:person/first "Han"
 :person/last "Solo"
 :person/ship {:ship/name "Millennium Falcon"
               :ship/model "YT-1300f light freighter"}}

請注意,這些對應表示相同的物件 - 這些只是交替語法。

#:: 可用於使用目前的命名空間自動解析對應中關鍵字或符號鍵的命名空間。

這兩個範例是等效的

user=> (keys {:user/a 1, :user/b 2})
(:user/a :user/b)
user=> (keys #::{:a 1, :b 2})
(:user/a :user/b)

類似於 自動解析關鍵字,您也可以使用 #::alias 來使用在 ns 形式中定義的命名空間別名自動解析

(ns rebel.core
  (:require
    [rebel.person :as p]
    [rebel.ship   :as s] ))

#::p{:first "Han"
     :last "Solo"
     :ship #::s{:name "Millennium Falcon"
                :model "YT-1300f light freighter"}}

讀取與

{:rebel.person/first "Han"
 :rebel.person/last "Solo"
 :rebel.person/ship {:rebel.ship/name "Millennium Falcon"
                     :rebel.ship/model "YT-1300f light freighter"}}

相同/ - 命名空間分隔符

/ 可以是除法函數 clojure.core//,但也可以作為符號名稱中的分隔符,用於分隔符號的名稱和命名空間限定詞,例如 my-namespace/utils。因此,命名空間限定詞可以防止簡單名稱的命名衝突。

\ - 字元文字

\ 表示文字字元,如下所示

user=> (str \h \i)
"hi"

還有一些少數特殊字元用於命名特殊 ASCII 字元:\newline\space\tab\formfeed\backspace\return

\ 後面也可以接續 \uNNNN 形式的 Unicode 文字。例如,\u03A9 是 Ω 的文字。

$ - 內部類別參考

用於在 Java 中參考內部類別和介面。分隔容器類別名稱和內部類別名稱。

(import (basex.core BaseXClient$EventNotifier)

(defn- build-notifier [notifier-action]
  (reify BaseXClient$EventNotifier
    (notify [this value]
      (notifier-action value))))

EventNotifierBaseXClient 類別的內部介面,而 BaseXClient 類別是已匯入的 Java 類別

->->>some->cond->as-> 等 - 執行緒巨集

這些是執行緒巨集。請參閱 官方 Clojure 文件

` - 語法引號

` 是語法引號。語法引號類似於引號(用於延遲評估),但有一些額外效果。

基本語法引號可能看起來類似於一般引號

user=> (1 2 3)
Execution error (ClassCastException) at myproject.person-names/eval232 (REPL:1).
class java.lang.Long cannot be cast to class clojure.lang.IFn
user=> `(1 2 3)
(1 2 3)

然而,在語法引號中使用的符號會根據目前的命名空間完全解析

user=> (def five 5)
#'user/five
user=> `five
user/five

語法引號最常在巨集中用作「範本」機制。我們現在可以寫一個

user=> (defmacro debug [body]
  #_=>   `(let [val# ~body]
  #_=>      (println "DEBUG: " val#)
  #_=>      val#))
#'user/debug
user=> (debug (+ 2 2))
DEBUG:  4
4

巨集是由編譯器呼叫並以程式碼作為資料的函式。它們預期會傳回程式碼(作為資料),以便進一步編譯和評估。此巨集會取得單一主體表達式,並傳回一個 let 形式,它會評估主體、列印其值,然後傳回該值。這裡,語法引號會建立一個清單,但不會評估它。該清單實際上是程式碼。

請參閱 ~@~ 以取得僅在語法引號中允許的額外語法。

~ - 取消引號

請參閱 ` 以取得更多資訊。

~ 是取消引號。語法引號就像引號一樣,表示評估不會在語法引號形式中發生。取消引號會關閉引號,並評估語法引號表達式中的表達式。

user=> (def five 5) ; create a named var with the value 5
#'user/five
user=> five ; the symbol five is evaluated to its value
5
user=> `five ; syntax quoting five will avoid evaluating the symbol, and fully resolve it
user/five
user=> `~five ; within a syntax quoted block, ~ will turn evaluation back on just for the next form
5
user=> `[inc ~(+ 1 five)]
[clojure.core/inc 6]

語法引號和取消引號是撰寫巨集的基本工具,巨集是在編譯期間呼叫並取得程式碼和傳回程式碼的函式。

~@ - 取消引號拼接

請參閱 `~ 以取得更多資訊。

~@ 是取消引號拼接。取消引號 (~) 會評估一個形式,並將結果放入引號結果中,~@ 預期評估值為集合,並將該集合的內容拼接至引號結果中。

user=> (def three-and-four (list 3 4))
#'user/three-and-four
user=> `(1 ~three-and-four) ; evaluates `three-and-four` and places it in the result
(1 (3 4))
user=> `(1 ~@three-and-four) ;  evaluates `three-and-four` and places its contents in the result
(1 3 4)

同樣地,這是一個撰寫巨集的強大工具。

<符號># - Gensym

符號結尾# 用於自動產生新的符號。這在巨集中很有用,可以防止巨集的細節外洩到使用者空間。一般的 let 會在巨集定義中失敗

user=> (defmacro m [] `(let [x 1] x))
#'user/m
user=> (m)
Syntax error macroexpanding clojure.core/let at (REPL:1:1).
myproject.person-names/x - failed: simple-symbol? at: [:bindings :form :local-symbol]
  spec: :clojure.core.specs.alpha/local-name
myproject.person-names/x - failed: vector? at: [:bindings :form :seq-destructure]
  spec: :clojure.core.specs.alpha/seq-binding-form
myproject.person-names/x - failed: map? at: [:bindings :form :map-destructure]
  spec: :clojure.core.specs.alpha/map-bindings
myproject.person-names/x - failed: map? at: [:bindings :form :map-destructure]
  spec: :clojure.core.specs.alpha/map-special-binding

這是因為語法引號中的符號會完全解析,包括這裡的區域繫結 x

相反地,你可以將 # 附加到變數名稱的結尾,讓 Clojure 產生唯一的(非限定)符號

user=> (defmacro m [] `(let [x# 1] x#))
#'user/m
user=> (m)
1
user=>

重要的是,每次在單一語法引號中使用特定的 x# 時,會使用相同的產生的名稱。

如果我們展開這個巨集,可以看到 gensym 'd 名稱

user=> (macroexpand '(m))
(let* [x__681__auto__ 1] x__681__auto__)

#? - 讀取條件

讀取條件的設計目的是讓 Clojure 的不同方言可以共用共用程式碼。讀取條件的行為類似傳統的 cond。使用語法為 #?,如下所示

#?(:clj  (Clojure expression)
   :cljs (ClojureScript expression)
   :cljr (Clojure CLR expression)
   :default (fallthrough expression))

#?@ - 拼接讀取條件

拼接讀取條件的語法為 #?@。它用於將清單拼接進包含的表單中。因此 Clojure 讀取器會將這讀成

(defn build-list []
  (list #?@(:clj  [5 6 7 8]
            :cljs [1 2 3 4])))

如下所示

(defn build-list []
  (list 5 6 7 8))

*var-name* - 「耳罩」

耳罩(一對星號包圍變數名稱)是許多 LISP 中的命名慣例,用於表示特殊變數。在 Clojure 中最常見的用法是表示動態變數,也就是說,會根據動態範圍而變更的變數。耳罩的作用是警告「此處有危險」,並且永遠不要假設變數的狀態。請記住,這是一個慣例,而不是規則。

Clojure 核心範例包括 *out**in*,它們分別代表 Clojure 的標準輸入和輸出串流。

>!!<!!>!<! - core.async 通道巨集

這些符號是 core.async 中的通道操作,core.async 是 Clojure/ClojureScript 的通道式非同步程式設計函式庫(特別是 CSP - Communicating Sequential Processes)。

如果你想像一下,為了論證,通道有點像一個佇列,東西可以放進去和拿出來,那麼這些符號支援這個簡單的 API。

  • >!!<!! 分別是阻塞式放

  • >!<! 簡單來說就是 放入取出

不同之處在於,封鎖版本在 go 區塊外運作,並封鎖它們運作的執行緒。

user=> (def my-channel (chan 10)) ; create a channel
user=> (>!! my-channel "hello")   ; put stuff on the channel
user=> (println (<!! my-channel)) ; take stuff off the channel
hello

非封鎖版本需要在 go 區塊內執行,否則它們會擲回例外狀況。

user=> (def c (chan))
#'user/c
user=> (>! c "nope")
AssertionError Assert failed: >! used not in (go ...) block
nil  clojure.core.async/>! (async.clj:123)

雖然這些差異遠超出本指南的範圍,但基本上 go 區塊會運作並管理它們自己的資源,暫停程式碼的執行而不封鎖執行緒。這使得非同步執行的程式碼看起來是同步的,消除了從程式碼庫管理非同步程式碼的麻煩。

<符號>? - 謂詞後綴

在符號的結尾加上 ? 是許多支援符號名稱中特殊字元的語言中常見的命名慣例。它用來表示該事物是一個謂詞,亦即它提出一個問題。例如,想像使用一個處理緩衝區操作的 API

(def my-buffer (buffers/create-buffer [1 2 3]))
(buffers/empty my-buffer)

乍看之下,你如何知道這個範例中的 empty 函式

  • 如果傳入的緩衝區為空,則傳回 true,或

  • 清除緩衝區

雖然作者可以將 empty 重新命名為 is-empty,但 Clojure 中豐富的符號命名讓我們能夠更具象徵性地表達意圖。

(def my-buffer (buffers/create-buffer [1 2 3]))
(buffers/empty? my-buffer)
false

這只是一個建議的慣例,而不是一個要求。

<符號>! - 不安全的運算

在 STM 交易中不安全的函式/巨集的名稱應該以驚嘆號結尾(例如 reset!)。

你最常會看到它附加在函式名稱上,其目的是要變更狀態,例如連線至資料儲存、更新原子或關閉檔案串流

user=> (def my-stateful-thing (atom 0))
#'user/my-stateful-thing
user=> (swap! my-stateful-thing inc)
1
user=> @my-stateful-thing
1

這只是一個建議的慣例,而不是一個要求。

請注意,驚嘆號通常發音為 bang。

_ - 未使用的引數

當你看到底線字元用作函式引數或 let 繫結時,_ 是表示你不會使用此引數的常見命名慣例。

這是使用 add-watch 函式的範例,可用於在原子變更值時新增回呼式行為。想像一下,給定一個原子,我們希望在每次它變更時列印新的值

(def value (atom 0))

(add-watch value nil (fn [_ _ _ new-value]
                       (println new-value))

(reset! value 6)
; prints 6
(reset! value 9)
; prints 9

add-watch 需要四個引數,但在我們的範例中,我們只真正關心最後一個引數 - 原子的新值,所以我們對其他引數使用 _

, - 空白字元

在 Clojure 中,, 被視為空白,與空格、標籤或換行符號完全相同。因此,在字面集合中從不需要逗號,但通常用於增強可讀性

user=>(def m {:a 1, :b 2, :c 3}
{:a 1, :b 2, :c 3}

非常感謝所有提供想法和 [大量] 拼寫更正的人(天啊,我的拼字很爛 - 所以謝謝 Michael R. Mayne、lobsang_ludd)。我試著找尋特別要求某些事物的人。如果你沒被我找到,請見諒。

原作者:James Hughes