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 Reader(将 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 Reader 会抛出异常,因为这是一个无效的文本。将项添加到集合时,如果值已经存在,那么这些项就会被直接跳过。

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

#_ - 丢弃

有关更多详情,请参见 #

#_ 告诉 Reader 完全忽略后面的形式。

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

请注意,#_ 后面的空格为可选项,因此

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

也可以起作用。还要注意,discard 字符在 edn 中起作用。

一个小窍门是可以堆叠多个 #_ 来省略多种形式

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

文档建议“#_ 之后的表单将完全被读者跳过(这比生成 nilcomment 宏提供了更完整的删除机制)。在调试情况下或在多行注释时,这可证明其有用性。

#"…​" - 正则表达式

有关更多详情,请参见 #

#" 表示正则表达式的开始

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

此表单在 读取时间编译成主机特定的正则表达式机制,但在 edn 中不可用。请注意,在 Clojure 中使用正则表达式时,不需要进行 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 引用

#' 是 var 引用,它展开为对 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

使用它时它将尝试返回所引用的 var。当你想要了解引用/声明而不是它所表示的值时,这非常有用。请参阅元数据中 meta 的用法(^)。

请注意,edn 中没有 var 引用。

## - 符号值

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’ed 名字替换(为了可读性,我们在此使用 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=>

@ 用于获取引用的当前值。上面的示例使用 @ 获取 atom 的当前值,但 @ 可应用于其他事物,如 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}

最初,元数据是通过 #^ 声明的,现在这已被弃用(但仍然可以使用)。后来,此符号被简化为 ^,大多数 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)

类似于 自动解析的关键字,还可以使用 #::别名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 类(一个导入的 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)

同样,这是一个编写宏的强大工具。

<symbol># - 符号生成

符号末尾的 # 用于自动生成一个新符号。这对于在宏内部保持宏的具体内容不会渗入用户空间非常有用。普通 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中的通道操作,此 Clojure/ClojureScript 库可进行基于通道的异步编程(特别是CSP - 通信顺序进程)。

如果出于辩论目的,您将通道想象成类似于队列的东西,人们可以在此队列中存放或取出物品,那么这些符号会支持这样的简单 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 块会执行并管理自己的资源,暂停代码的执行而不会阻塞线程。这样一来,异步执行的代码看起来就像同步执行的代码,从而无需管理异步代码中恼人的代码库。

<symbol>? - 谓词后缀

在符号后缀加上 ? 是许多支持符号名称中使用特殊字符的语言中通用的命名约定。它用来表示对象是谓词,即它提出一个问题。例如,假设您使用涉及缓冲区操作的 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

这仅仅是推荐的约定,而不是要求。

<symbol>! - 不安全的操作

不适用于 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