文章目录
一、 概述
- Clojure中大量使用各种符号,初学时经常容易看晕,因此将学习过程中接触到的符号总结到一起,便于理解;
- Clojure中的符号本质上都是用于简化代码的语法糖,编译时会用Reader进行解析;Clojure官方文档专门有一章用于介绍Reader,里面大部分都是关于符号的说明,详见 The Reader
- Clojure官方的CheatSheet中也有对符号的总结,非常便于检索,详见Cheatsheet
二、数据结构相关符号
-
Clojure使用了大量符号来区分不同的数据类型,且引入了 解构 概念来拆分数据结构,导致符号之中还会嵌套更多的符号;
-
如需了解常见数据类型及使用方法,建议参阅 Clojure数据类型
-
如需了解集合及常用方法,建议参阅 Clojure集合
-
如需了解解构方法,详见官方说明 Destructuring
1. 数据定义
- 【String】"Hello"
- 【Char】\a
- 【Keyword】:key
- 【List】’(1 2 3)
- 【Vector】[1 2 3]
- 【Map】{key word key word}
- 【Set】{1 2 3}
- 【BigInt】100N
- 【BigDecimal】0.01M
- 使用 class obj 可以输出数据类型,使用 map?、list? 等函数可以判断类型对不对
2. 数据解构
(let [[x y] [1 2 3]] ;x=1 y=2 (let [[x y z] [1 2]] ;x=1 y=2 z=nil (let [[item1 _ item3 & remaining :as all] [1 2 3 4 5]]) ;item1=1, item3=3, remaining=[4 5] all=[1 2 3 4 5] (let [[a b :as A] [c d :as B]] [[1 2] [3 4]]) ;a=1 b=2 c=3 d=4 A=[1 2] B=[3 4] (let [{A :a B :b D :d :or {D "None"} :as all} {:a 1 :b 2 :c 3}]) ; A=1 B=2 D="None" all={:a 1 :b 2 :c 3}
二、命名规范及元数据
-
在Clojure中定义一个函数或变量/符号时,除了采用合适的单词解释该函数/变量/符号以外,还会在命名中加入一些符号或元数据来包含更多信息;
-
请注意下述规范和元数据不是强制性的,也不会影响到数据计算,但多了解一些有利于理解源码;
1. 函数命名规范
- 【?】有判断功能的函数以 ? 结束,如 fn? number?
- 【*】有时会在一个命名空间中定义两个同名函数,其中一个函数用于对外提供接口并调用第二函数去实际执行(有时也把第二个函数放在第一个函数内部),此时实际运行的函数以 * 结尾
如 metabase中,runf 函数用于从 context 中解析出一个函数并运行,这个被运行的内部函数可命名为 runf*
2. 参数命名规范
- Clojure中定义函数入参时不必指定数据类型,但可以用参数名来解释数据类型,例如
-【 a 】Java数组
-【 agt 】代办
-【 coll 】容器
-【 expr 】表达式
-【 f 】函数
-【 idx 】索引
-【 r 】引用
-【 v 】向量
-【 val 】值
3. 变量/符号命名规范
- 会改变的参数以 * 开头和结尾,如 *data*
def ^:dynamic *data* ;定义一个变量,一般会在下文中用 binding 改变值
4. 元数据使用
- 元数据是对数据的附加说明,本身不参与任何计算,除非主动提取出来使用 参考说明
(defn function ;函数添加元数据,查询指令(meta #'function) "this is a note" {:added "1.2"} ;元数据 [coll](expr)) (def name ;变量添加元数据,查询指令 (meta name) (with-meta (expr) {:creator "tim"}))
三、常用符号
-
部分常用符号在不同语境下可以表达不同的意思,部分示例包括:
1. Dot (.)
- 主要用于使用Java标准库,注意Clojure只会自动引入Java.lang,其它库都要手动引入
(Date.) ;类名字后加个”."用来创建一个对象 (. (Date.) getTime) ;创建一个实例并调用示例方法 (.getTime (Date.)) ;同上 (def rnd (new java.util.Random)) ;同样可以创建一个Java实例 (. rnd nextInt 10) ;调用实例方法 (.replace (.toUpperCase "a b c d") "A" "X") ;调用字符串方法 (System/currentTimeMillis) ;调用Java静态方法时使用 /
2. Quote (’)
(= 'form (quote form)) ;用于防止表达式被解析 (= '(1 2 3) (list 1 2 3)) ;同上 (eval '(+ 1 1)) ;返过来,解析表达式 (eval (read-string "(println 1)")) ;解析字符串表达式 (+' 1 2) ;用于支持任意精度,不会出现 integer overflow 报错,同样适用于 -' *' (+ 1N 2N) ;与上述效果一样,突破精度限制
3. Character (\)
- 用于表示字节 \a,特殊字符 \newline , 或Unicode \u03A9
4. Unquote (~)
- 主要用在宏中,示例参考 宏 macro
5. More (&)
- 用于函数的入参中,会将后续参数组装为一个list,list中数据类型可以不一致
(defn date [person & stores]) ;stores将所有后续参数组装到一个list中
6. Dispatch (#)
- 调用宏来读取其参数,有多种用途
#{} ;定义一个Set #"\s*\d+" ;定义一个正则表达式 #'x ;等效于(var x),可以用 (meta #'x) 读取元数据 #(println %) ;匿名函数
四、常用关键字
-
Clojure中可以通过使用预先定义的 :key 来实现一些方法或功能
1. 断言 :pre & :pos
- :pre 在函数调用之前检查参数是否符合条件,:post 在函数调用之后检查返回值是否符合条件;
- 断言一般写在 [参数] 和 (代码) 之间,断言仅在开发环境起作用,正式环境会忽略,详见文章 :pre & :post
2.私有化 ^:private
- 可以标注在函数或变量上将其私有化,被 ^:private 标注后需要通过 #’ 访问
- defn- 和 defn ^:private 的效果是一样的,但后者应用更灵活更广泛
;; 将一个变量私有化,需要使用 #' 才能读取该变量 (def ^:private a {}) (meta a) ;=> nil (meta #'a) ;=> {:private true} ;; 注意,下述写法是 {} 私有化了,不能起到预期的效果 (def a ^:private {}) (meta a) ;=> {:private true} (meta #'a) ;=> nil ;; 灵活运用 private 定义一个私有的def- (defmacro def- [name & decls] (list* `def (with-meta name (assoc (meta name) :private true)) decls))
3. 变量 ^:dynamic
- 定义一个可以被改变的变量,一般名称要用 *name* 的格式,且下文会用 binding 改变值;
- 本质上这还是常量,只是在binding的作用域中会被临时绑定到一个新的值上,且仅对当前线程有效
4. 常量 ^:const
- 定义一个常量,加上这个注释之后,JVM会在编译时直接用值替换符号,否则JVM不会进行编译优化,而是在运行该函数时通过寻址去加载符号绑定的值,参考解答 stackoverflow
;; 下述两种表达方式等价,编译后代码一致 (def ^:const pi 3.14) (defn circ2 [r] (* 2 pi r)) (defn circ2 [r] (* 2 3.14 r)) ;; 下述表达方式运行会慢一些,因为运行时会查找pi的值 (def pi 3.14) (defn circ2 [r] (* 2 pi r))
5. 数据类型说明 ^String
- 在定义函数/入参时将数据类型写入元数据的tag键中,等效于 ^{:tag String}
- 元数据仅仅记录说明,不会在运行中真正检查数据类型,即输入其它类型数据也不会有影响
(defn ^String testB [^Number s] (println s)) ;可以添加到函数或入参上 (defn ^String test) ;meta记录为 :tag java.lang.String (defn ^:String test) ;meta记录为 :String true
6. 提高性能 ^:static
- 可以提高运行性能,原理待补充