自己所学过的编程语言基本是 C 风格的, 给自己定下的目标是要学习下 Python, Swift 和 Clojure. 正如之前的 我的 Python 快速入门 那样的几分钟入门, 这里记录下 Clojure 的快速上手过程.
为什么是 Clojure, 因为它是 Lisp 的一个方言, 个人觉得有必要拓展一下不同的语言风格与思维方式, 就像当初接触 Objective-C 的 [person sayHello] 的方法调用有点不好理解一样, 其实把它还原为面向对象的本质是向 person 发送 sayHello 消息就简单了.
编程语方不仅仅是一种技术, 它更是一种思维习惯
希望通过 Clojure 这样的语言来感受另样的思维方式. Clojure 是运行在 JVM 之上的函数式 List 方言. Clojure 乍一看, 基本就是一个括号语言, 它的语法更能体现操作/函数为中心. Clojure 的圆括号兼具 C 风格语言的圆括号(参数列表), 分号(分隔语句), 以及大括号(限定作用域) 的功能. (1 + 2 + 3 +4) 只用写成 (+ 1 2 3 4)。
因为 Clojure 是构筑在 JVM 之上, 所以可以从 http://clojure.org/community/downloads 下载 clojure 的 jar 包, 然后
java -jar clojure-1.8.0.jar
就能进到 Clojure 的 REPL(read-eval-print-loop) 控制台了, 就可以开始体验 Clojure 的代码 user=> (+ 1 2 3)
,如果要运行一个已经写好的 Clojure 文件, 如 hello.clj, 就要用 java -jar clojure-1.8.0.jar hello.clj
来执行. 为方便可以建立一个脚本 clj
, 内容为
java -jar /path/clojure.jar $1
在 Mac 下我一般首先会尝试 brew install clojure
, 结果它会告诉我说 Clojure 只是一个库, 需要用 brew 来安装 leiningen, 于是就 brew install leiningen
, 安装完 leiningen 后提示依赖会安装到 $HOME/.m2/repository, 用命令 lein repl
进到 Clojure 的控制台, Leiningen 是一个用 Clojure 写的像 Maven/sbt 那样的构建工具, Leininge 和 Clojure 的关系就像是 sbt 与 Scala.
现在真正开始来学习这门语言了, 主要根据在线的 Clojure 入门教程 来整理的.
Clojure 的名字包含了 C(C#), L(List) 和 J(Java). Clojure 以操作为中心(操作前置, 更能体现计算机的行为), 它实现成三种形式: 函数(function), 宏(macro) 或者 special form(非 Clojure 代码, 基本就是关键字, 像 def, catch 之类, 不是我们要考虑的).
有人觉得 Lisp 方言很简洁, 很美; 数据和代码的表达形式是一致的. 就像是 Vim 或 Emacs 中的快捷键都有对应的命令一样, Clojure 的语法糖一般也都有相对应的函数或宏, 例如:
注释用 ; _text_, 对应的宏是 (comment _text_), 如 (comment 这一行是干什么的, 不需要引号括起来的)
正则表达式 #"_pattern_", 对应的函数是 (re-pattern _pattern_)
List 是 `(_items_), 对应函数 (list _items_)
Vector [_items_], 对应函数 (vector _items_)
Set #{_items_}, 对应函数 (hash-set _items_) 或 (sorted-set _items_)
Map {_key-value-pairs_}, 对应函数 (hash-map _key-value-pairs_) 或 (sorted-map _key-value-pairs_)
匿名函数 #(_single-expression_), 用 %1, %2 来表示参数, 对应函数 (fn [_arg-names_] _expressions_)
等等等等
怎么看起变得像 Perl 的那些约定了, Perl 的标量, 数组, 哈希分别用 $, @, % 作为变量名前缀, 以及一堆的 $_. $$, $! 这样的规定.
变量/Binding
Clojure 是函数式的, 它本质是不支持变量的, 它包括全局binding, 线程本地binding, 函数内本地binding, 以及表达式内部binding. 定义方式为
(def v 1) 或 (def ^:dynamic v 1) 在所有线程是可见的. 函数(defn foo [a b] ...) 的参数是在这个函数内的本地binding. (let [v 2]), (binding [v 3]). 宏 binding 与 let 的不同之处是在它的作用域内会暂时覆盖全局binding
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
(def ^:dynamic v 1) (defn f1 [] (println "f1: v = " v)) (let [v 3] (println v) ;3 (f1)) ;f1: v = 1, let 像 java 的方法内声明的同名局部变量一样,不对全局变量产生任何影响 (println v) ;1 (binding [v 3] (println v) ;3 (f1)) ;f1: v = 3, 用 binding 时全局变量被临时覆盖, 离开作用域全局变量重新生效 (println v) ;1 |
不说别的, Clojure 光一个变量就能把一半想学它的人给吓跑. Clojure 还有这样的操作 (var-set #'v 6), (def ^:dynamic *shiro-response*). 语法糖的增多未必让语方简洁, 可能更晦涩难懂, 云里雾里.
集合
和 Scala 一样, Clojure 自带集合实现, 然而与 Scala 不同的是 Scala 有分可变与不可变化集合, Clojure 更彻底, 它的函数式特性决定只提供不可变的集合, 所以对集合的任何插入元素, 排序等操作都会生成一个新的集合, 不影响老的集合. 例如下面的语句, 老集合在 conj, reverse 操作后都不会变的.
1 2 3 4 5 6 7 8 |
user=> (def a [1 2 3]) #'user/a user=> (def b (conj a 4)) #'user/b user=> (def c (reverse b)) #'user/c user=> (println a b c) [1 2 3] [1 2 3 4] (4 3 2 1) |
Clojure 集合分为 list, vector, set 和 map. 看集合的几个基本操作, 像 Java 的 map, reduce(collect), filter 对应是 Clojure 有 map, apply 和 filter 方法
1 2 3 4 |
(map #(+ % 3) [2 4 7]) ; -> (5 7 10), #() 是匿名函数的语法糖, % 或 %1 表示第一个参数, %2, %3 ... 为第二第三个参数 (map + [2 4 7] [5 6] [1 2 3 4]) ;同位上相加, 次数由短板决定, 即 [(+ 2 5 1) (+ 4 5 2)], 得到 (8 10) (apply + [2 4 7]) ; -> 13, apply 返回一个值, 而 map 的结果仍然是集合 (filter #(> % 3) [2 4 5]) ; -> (4 5) |
其他还有一些常用集合函数, 如 first, second, last, nth, next, butlast, drop-last, nthnext, 和相当于 java 的 && 或 or 的谓词测试函数, 如 every?, instance?, not-every?, some, not-any? 等. (every? #(instance? String %) ["I'm string" 2 4))
.
List - 有序列表, 它更像是 Java 的堆栈, 适合操作顶端元素, 有三种方式来创建 List
1 2 3 |
(def stooges (list "Moe" "Larry" "Curly")) ;list 函数 (def numbers (quote (1 2 3)) ;quote 组成的是一个 special form (def fruits '("Apple" "Banana")) ;' 是 list 函数的语法糖 |
像 Python 的 dir() 一样, 在 Clojure 里可以用 (doc list), (doc quote) 那样的方式来查看函数或宏的用法. 搜索 list 是线性的, 转成 set 会更高效, 如
1 2 |
(contains? (set stooges) "Moe") ; -> true (remove #(= % "Curry") stooges) ; 按条件移除元素 |
Vector - 也是一种有序集合, 适于从后面操作, 或用索引(nth) 进行操作, 所以凡无特别需求, 尽量用 vector 而不是 list, 而且 [...]
比 (...)
更自然. vector 的声明方式和索引取值
1 2 3 4 5 |
(def stooges (vector "Moe" "Larry" "Curly")) (def numbers [1 2 3]) ;这种语法糖的方式更简洁, 注意啦, 函数参数就是用这种方式 (get stooges 1) (get stooges 3 "unknown") ;第二个参数为数组越界时的默认返回值, 不写的话为 nil, nth 函数越界会有异常 |
Set - 它的概念与 Java 的 Set 是一样的, 并且也分有序与无序的. set 的声明与基本用法
1 2 3 4 5 6 |
(def stooges (hash-set "Moe" "Larry" "Curly")) (def numbers #{1 2 3}) (def stooges (sorted-set "Moe" "Larry" "Curly")) ;-> #{"Curly", "Larry", "Moe"}, 前两种方式顺序不可预知 (stooges "Moe") ;-> "Moe" (stooges "Mark") ;-> nil, set 变量可当作函数来使用, 返回参数值或 nil |
Map - map 的 key 一般会用一个 keyword (内部字符串, 有点像 Scala 的 Symbol), 声明方式如下
1 2 3 |
(def popsicle-map (hash-map :red :cherry, :green "apple")) ; value 想是什么都行, Key 一般是 keyword, 用冒号开始 (def popsicle-map {:red "cherry", (keyword "purple") grape}) ; 如果不用冒号, 或者 key 来自于变量可以用 keyword 函数生成一个 keyword (def popsicle-map (sorted-map :red :cherry "green" :apple)) ; 这里说明一下上面的逗号是非必需的, 而且 key 也可是任何类型 |
类似于 Set, map 变量名可以作为它的 key 的函数, 方便获取值, 如果 key 是一个 keyword 的话也可作为函数用, 如
1 2 3 |
(get popsicle-map :green) (popsicle-map "cherry") ; (popsicle-map :red) (:green popsicle-map) |
其他的 map 操作有 contains? 是否包含 key; keys 和 vals 分别返回所有 key 或 value 的集合(Vector), select-keys 选择出子 map.
Clojure 有一种与生俱来的接近于 JSON 结构的定义方法, 如下面这样的结构
1 2 3 4 5 6 7 8 9 10 11 |
(def person { :name "Yanbin" :address { :city "Chicago" :state "IL"} :language [ "Java" "Scala" "Clojure" "Python" ]}) ;->{:name Yanbin, :address {:city Chicago, :state IL}, :language [Java Scala Clojure Python]} |
其余以后实际了解 get-in, reduce, accoc-in 和 update-in 等函数.
->
和 -?>
这两个宏很有意思, 它是一个管道操作, 把前面函数的返回值作为后一个函数的参数
1 2 |
(f3 (f2 (f1 x))) (-> x f1 f2 f3) ;与上面是一样的效果, -?> 的不同之处是调用链上任何一个函数返回 nil, 整个链立即返回 nil (短路操作) |
还有一种特殊的 Map 是 StructMap, 用 create-stuct 函数或 defstruct 宏来创建, 不深入.
本文链接 https://yanbin.blog/clojure-get-started/, 来自 隔叶黄莺 Yanbin Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。