Clojure 快速突击

自己所学过的编程语言基本是 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 来安装 leininge, 于是就  brew install leiningen , 安装完 leiningen 后提示依赖会安装到 $HOME/.m2/repository, 用命令  lein repl 进到 Clojure 的控制台, Leininge 是一个用 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

(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 操作后都不会变的.

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, filter 对应是 Clojure 有 map, apply 和  filter 方法

(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

(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 会更高效, 如

(contains? (set stooges) "Moe")    ; -> true
(remove #(= % "Curry") stooges)    ; 按条件移除元素

Vector - 也是一种有序集合, 适于从后面操作, 或用索引(nth) 进行操作, 所以凡无特别需求, 尽量用  vector 而不是 list, 而且 [...] 比 (...) 更自然. vector 的声明方式和索引取值

(def stooges (vector "Moe" "Larry" "Curly"))
(def numbers [1 2 3])       ;这种语法糖的方式更简洁, 注意啦, 函数参数就是用这种方式

(get stooges 1)
(get stooges 3 "unknown")  ;第二个参数为数组越界时的默认返回值, 不写的话为 nil, nth 函数越界会有异常

Set - 它的概念与 Java 的 Set 是一样的, 并且也分有序与无序的. set 的声明与基本用法

(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), 声明方式如下

(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  的话也可作为函数用, 如

(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 结构的定义方法, 如下面这样的结构

(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 等函数.

-> 和 -?> 这两个宏很有意思, 它是一个管道操作, 把前面函数的返回值作为后一个函数的参数

(f3 (f2 (f1 x)))
(-> x f1 f2 f3)  ;与上面是一样的效果, -?> 的不同之处是调用链上任何一个函数返回 nil, 整个链立即返回 nil (短路操作)

还有一种特殊的 Map 是 StructMap, 用 create-stuct 函数或 defstruct  宏来创建, 不深入.

 

类别: Clojure. 标签: . 阅读(74). 订阅评论. TrackBack.

Leave a Reply

Be the First to Comment!

avatar
wpDiscuz