Weitere ähnliche Inhalte Ähnlich wie JVM上的实用Lisp方言:Clojure (15) JVM上的实用Lisp方言:Clojure1. .
JVM 上的实用 Lisp 方言:Clojure
Jerry Peng
2012-12-18
1/
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
1/90
.
2. .
Outline
今天的主题
Clojure 简介
揭开 Lisp 的神秘面纱
Clojure 语言特性一览
函数式编程
并发支持
2/
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
2/90
.
3. .
Topic
今天的主题
Clojure 简介
揭开 Lisp 的神秘面纱
Clojure 语言特性一览
函数式编程
并发支持
3/
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
3/90
.
4. .
思维方式的引导
• Clojure 语言的简要介绍
◦ Lisp 的特别之处
• 一窥函数式编程的奇妙
• 重在打开一扇思维方式的门
◦ 私下的学习和实践最重要
4/
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
4/90
.
5. .
Topic
今天的主题
Clojure 简介
揭开 Lisp 的神秘面纱
Clojure 语言特性一览
函数式编程
并发支持
5/
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
5/90
.
6. .
About Clojure
• Clojure 发音和 Closure 一样
• 作者是 Rich Hickey
• 最新稳定版本是 1.4
◦ 1.5RC1 也已经 release 了
6/
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
6/90
.
7. .
About Rich Hickey
• 十分有想法的一个人
• 强烈推荐看看他关于 Time、Identity 和
Value 的那场演讲
• E2E 上和 Brian Beckman 的讨论
7/
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
7/90
.
8. .
Hello World
(defn hello [name]
(println "Hello," name))
(hello "John")
; ==> Hello, John
8/
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
8/90
.
9. .
Smarter Hello World
(defn say-hello [& names]
(println "Hello to"
(apply str
(concat (interpose ", " (drop-last names))
[" and " (last names)]))))
(say-hello "John" "Peter" "Brad" "Jerry")
; ==> Hello to John, Peter, Brad and Jerry
9/
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
9/90
.
10. .
Clojure 语言简介
• Clojure 是一种 Lisp 方言
• Clojure 是函数式编程语言
• Clojure 支持强大的并发机制
• 强大的 REPL 支持(Read-Evaluation-Print-Loop)
◦ 极其高效、舒适的开发体验
10 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
10/90
.
11. .
Clojure 是一种 Lisp 方言
• 100% 的 Lisp
◦ REPL 支持
◦ 强大的宏
• 更现代
◦ 支持 vector 和 map 的字面量
◦ 括号尽可能少
• 平台更强大——JVM/CLR
◦ 性能强大、工业标准
◦ 高质量的库支持
◦ 无缝的 Java 互操作
◦ ClojureScript,Clojure-Py
11 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
11/90
.
12. .
Clojure 是函数式编程语言
• 强调不可变性(Immutability)
• 性能优秀的 Persistent Data Structure
◦ 做到 immutable 的同时兼顾性能
• 强大的 sequence 支持
◦ 类似 Python 的 iterator,但更强大
• Laziness
◦ Lazy sequences
◦ 大量的基本操作都是 lazy 的
◦ 自由使用函数式编程风格又无需担心性能
12 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
12/90
.
13. .
Clojure 对并发有良好的支持
• Immutability 本身就对并发友好
◦ 可变状态是并发的天敌
• 简单的原子操作支持(atoms)
• 软件事务内存(STM,refs)
• 异步编程(agents)
13 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
13/90
.
14. .
Topic
今天的主题
Clojure 简介
揭开 Lisp 的神秘面纱
Clojure 语言特性一览
函数式编程
并发支持
14 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
14/90
.
15. .
大牛们怎么说 Lisp 的
.
Eric Raymond
.
“Lisp is worth learning for the profound enlightenment
experience you will have when you finally get it; that experience
will make you a better programmer for the rest of your days, even
if you never actually use Lisp itself a lot.”
.
15 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
15/90
.
16. .
大牛们怎么说 Lisp 的
.
Paul Graham
.
“Within a couple weeks of learning Lisp I found programming in
any other language unbearably constraining.”
.
.
Alan Kay
.
“The greatest single programming language ever designed.”
.
16 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
16/90
.
17. .
个人的体验(1)
• 曾经对 Lisp 几乎一无所知
◦ 只听闻它可读性很不好,到处都是括号
• 读《黑客与画家》后,开始有所了解并决定学习
• 以 Common Lisp 开始 Lisp 学习之路
◦ 《Practical Common Lisp》
◦ 对 Lisp 独特之处有所体会
◦ 因为种种原因没能持续
17 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
17/90
.
18. .
个人的体验(2)
• 从 Clojure 开始一发不可收拾
◦ 很短的时间就上手了
• 熟悉 Lisp 就是熟悉了所有的 Lisp 方言
◦ 更好、更实用的现代 Lisp 方言
◦ 已经在项目中实际使用
• Emacs Lisp
◦ 因为 Clojure 而开始用 Emacs
◦ 不可避免地要 Hack 一下 Emacs Lisp :-)
• SICP 和 Scheme 学习中
◦ SICP——《计算机程序的解释与构造》
◦ 学习编程思想,尤其是函数式思维的绝佳教材
◦ 为什么没能在编程入门阶段阅读到它?
18 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
18/90
.
19. .
括号!括号!
• Lisp is …
◦ Lost In Stupid Parentheses
◦ Lots of Irritating Superfluous Parentheses
.
Lisp Cycles
.
.
19 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
19/90
.
20. .
S-expression
• 可以说是括号造就了 Lisp 的强大
• Lisp 代码的这种括号套括号的表达法被称作 S-expression
◦ Lisp 被发明时,还有个 M-expression 的表达方式
◦ 但 M-expression 不受欢迎,被 S-expression 取代
• S-expression 本质上是任意树形结构的一个表达法
• 让我们先 hold 一会
◦ 记住 Lisp 代码就是一棵用 S-expression 表达的树
◦ 看看其他语言是怎么做的
20 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
20/90
.
21. .
其他语言怎么做的
• 具备某种特定语法
• 往往区分 Expression 和 Statement
• 词法分析 -> 语法分析 -> AST -> 编译/解释
21 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
21/90
.
22. .
Grammer == Restrictions
• 必须在语法框架下去写程序
• 无法跳出语法限制下的条条框框
.
在 C 或者 Java 里永远写不出这样的程序
.
printf("Result is %s", switch (input) {
case 1: "A"; break;
case 2: "B"; break;
case 3: "C"; break;
default: "Unknown";
. })
22 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
22/90
.
23. .
Lisp == No Grammer
• Lisp 没有语法
◦ Lisp 代码只是用 S-expression 表达出来树形结构
◦ Lisp 求值规则决定了 S-expression 的语义
◦ 不区分 Expression 和 Statement
• 换言之,Lisp 代码其实就相当于其他语言的 AST
• Lisp 是真正的“代码即数据”
.
没有语法 == 没有限制
.
(println "Result is" (case input
1 "A"
2 "B"
3 "C"
. "Unknown"))
23 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
23/90
.
24. .
代码即数据
• 应该将 Lisp 代码看作递归的树形结构
• Lisp 语言仅仅定义了如何运行这些数据
24 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
24/90
.
25. .
Lisp 求值规则(1)
• 先提出几个概念
◦ atom:原子
• 符号、数字、字符串、关键字等等
◦ form:可被 Lisp 解释器解释的合法 S-expression
• 第一个元素必须是 symbol
25 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
25/90
.
26. .
Lisp 求值规则(2)
• atom 求值规则
◦ symbol 求值得到 symbol 上绑定的值
◦ 其他 atom 求值得到本身
• form 求值规则
◦ 根据 form 的第一个 symbol 决定语义
26 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
26/90
.
27. .
form 的语义由第一个 symbol 决定
• symbol 对应一个函数:函数调用
◦ 要先将函数所有的参数求值出来才能调用函数
• symbol 对应一个 special form
◦ 根据 special form 来决定语义,且子 form 代表的含义是固定
的
• symbol 对应一个宏
◦ 调用宏函数来展开宏
◦ 这一步一般发生在编译阶段,到运行时所有的宏都应该被完
全展开了
27 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
27/90
.
28. .
以上就是 Lisp 的所有求值规则
• 短短几句话就可以总结 Lisp 的运行规则
• Lisp 的核心只有 7、8 个核心原语操作
◦ 其他的函数、special form 都在此之上实现
◦ if,while,for,case,defun 等等…
• 这些意味着什么?
28 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
28/90
.
29. .
Lisp 是最高级的语言
• 用户可以任意定义自己需要的语言功能
◦ if-not,for-even,defprivate…
• 终极 DSL
◦ 直接通过扩展语言来以最自然的方式来表达问题域的概念
• 通过宏来实现
◦ 和 C/C++ 里的宏完全是两个概念
• C/C++ 里的宏是简单的字符串替换
• Lisp 的宏是以 Lisp 代码为输入/输出的函数
• Lisp 语言在任何阶段都是可用的
◦ Read -> Compile -> Eval
◦ Reader Macro
◦ Macro
29 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
29/90
.
30. .
你可以任意发明自己的语言构造
• 不如发明个 while-not 玩玩?
(defmacro while-not [test & body]
`(while (not ~test)
~@body))
(let [a (atom 10)]
(while-not (< @a 0)
(println @a)
(swap! a - 2)))
; ==> 打印 10 8 6 4 2 0
30 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
30/90
.
31. .
示例——clojure-control
(defcluster :mycluster
:clients [{:host "a.domain.com" :user "alogin"}
{:host "b.domain.com" :user "blogin"}])
(deftask :deploy "scp files to remote machines"
[file1 file2]
(scp [file1 file2] "/home/alogin/")
(ssh (str "tar zxvf " file1))
(ssh (str "tar zxvf " file2)))
; control run CLUSTER TASK <args>
31 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
31/90
.
32. .
示例——clojure.java.jdbc
(def mysql-db {:subprotocol "mysql"
:subname "//127.0.0.1:3306/clojure_test"
:user "clojure_test"
:password "clojure_test"})
(sql/with-connection mysql-db
(sql/insert-records :fruit
{:name "Apple" :appearance "rosy" :cost 24}
{:name "Orange" :appearance "round" :cost 49}))
32 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
32/90
.
33. .
示例——clojure web 开发
(defpage [:post "/"] {:keys [url]}
(layout
[:h3 "Here is your shortened URL"]
[:h3 (short-url-link (shorten-url! url))]
[:p (link-to "/" "Back")]))
33 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
33/90
.
34. .
个人实践——clj.tr069
(deftr069type Inform
(device-id :child :DeviceId)
(events :child-array :Event
:EventStruct)
(parameter-list :child-array :ParameterList
:ParameterValueStruct)
(retry-count :int :RetryCount)
(current-time :dateTime :CurrentTime)
(max-envelopes :int :MaxEnvelopes))
34 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
34/90
.
35. .
Topic
今天的主题
Clojure 简介
揭开 Lisp 的神秘面纱
Clojure 语言特性一览
函数式编程
并发支持
35 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
35/90
.
36. .
基本数据类型
variable1 ;Symbols(可理解成别的语言的变量)
"string" ;String
:keyword ;Keyword
i ;Character
true
false ;Booleans
nil ; 等同 Java 的 null
1234 ;long
12.4 ;double
42N ;BitInt
0.01M ;BigDecimal
22/7 ;clojure.lang.Ratio (分数)
#"(d+)-(d+)" ;Regex
36 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
36/90
.
37. .
复合数据类型
'(1 2 3 4 5) ;List
[1 2 3 4 5] ;Vector
{:name "John" :age 22} ;Map
#{"a" "b" "c"} ;Set
• Clojure 直接支持 Map,Vector 和 Set 的字面量
◦ 其他 Lisp 方言可能要借助函数来创建
◦ 对开发者更友好
37 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
37/90
.
38. .
命名空间
• 类似 Java 的 package,但 Java 的 package 中不允许有数据
◦ 更像 Python、Ruby 中的 Module
• 把代码和数据组织到不同的命名空间下
• 可以互相引用
• 默认命名空间是 user
• 通过 ns 宏来声明一个模块所处的命名空间
38 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
38/90
.
39. .
定义全局变量
(def my-name "Foobar")
my-name
; ==> "Foobar"
user/my-name
; ==> "Foobar"
• 将指定的值绑定到指定的 Symbol 上
• 通常用作全局数据,初始化后不再变动
39 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
39/90
.
40. .
定义函数
(def hello-func (fn [name] (println "Hello, " name)))
; 把函数理解成数据
(hello-func "World")
(defn hello-func [name] (println "Hello, " name))
; 和上面的定义方式等价
• Lisp 语言核心很小,很多在别的语言里是语言级别功能的,
在 Lisp 里都是通过更基础的操作组合的
◦ defn 其实就是 def 后面跟着一个 fn 作为值
40 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
40/90
.
41. .
函数的返回值
• 没有显式的 return
◦ Ruby 等语言也学到了这一思想
• 逻辑上最后一条表达式的结果即函数的返回值
◦ 不区分语句和表达式,一切皆有值
• 任何 Lisp form 求值后一定会得到一个值
◦ 比如 if ,满足条件返回 then 求值的结果,否则是 else 求值
的结果
◦ 比如 do ,返回最后一个表达式的值
◦ 递归
• 有一些操作返回的是 nil ,这类 form 主要是为了产生副
作用而不是值
◦ 比如在 doseq 里写文件、打印日志等
41 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
41/90
.
42. .
匿名函数
• fn 创建的函数可以是匿名的
◦ 如果用 def 赋值给一个 symbol,相当于为其命名
• 可以在需要函数的地方直接用 fn 创建匿名函数
◦ 如果需要递归,可以在 fn 后面给它一个名字,但这个名字只
在函数体中有效
42 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
42/90
.
43. .
用 #(...) 来创建匿名函数
• 可以用 #(...) 来创建更简洁的匿名函数
◦ 可以用函数体只有一个 form 的地方
◦ 使用 % 以及 %n 来代表参数
(fn [x] (.toUpperCase x))
#(.toUpperCase %)
(fn [a b] (* (+ a b) (- a b))
#(* (+ %1 %2) (- %1 %2))
43 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
43/90
.
44. .
局部变量的使用——let
(let [a 3
b 4
c (* a b)] ; 引用已定义的 symbol a 和 b
(println a b c))
; Body 中可以引用 let 中绑定的所有 symbol
• 定义局部变量
• 将值绑定到指定的 symbol 上,在 let 的 body 部分可以引
用这些 symbol
◦ 可一次绑定多个 symbol
◦ 后面的 binding form 中可以引用前面已经定义过的 symbol
• 绑定是只读的,一旦绑定无法变更
◦ Clojure 强调“不可变性”的体现
44 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
44/90
.
45. .
let 可以任意嵌套
(let [a 3
b 4]
(let [a 5
b (+ b 1)
c (* a b)]
(println a b c)))
• Clojure 采用的是词法作用域,symbol 引用的是最近的
scope 里的值
• let 的 [..] 部分被成为 binding form
◦ 其他的 binding form 还有函数参数
◦ binding 语句
• 嵌套的 binding form 都采用上述规则
45 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
45/90
.
46. .
Destructuring 介绍
• Destructuring 是 Lisp 语言的一个十分强大的功能
• 用来将“解构”复合数据结构并绑定到 symbol 上
• 可以用在任意 binding form 上
◦ fn、defn、let、binding 等等
• 支持所有的数据结构
◦ list、vector、map、set
• Destructuring 还支持嵌套
46 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
46/90
.
47. .
Destructuring 示例
(let [data [:a :b :c]
a (first data) (let [data [:a :b :c]
b (second data) V.S [a b c] data]
c (last data)] (println a b c))
(println a b c))
47 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
47/90
.
48. .
Map 的 Destructuring
(defn print-info [{:keys [name gender age]}]
(println name gender age))
(print-info {:name "John" :gender "Male" :age 32})
; ==> John Male 32
• Destructuring 让 Map 十分适合作为数据的承载对象
48 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
48/90
.
49. .
条件操作符 if
(defn test-value [x]
(if (> x 0)
(println "Yeah")
(println "No")))
(test-value 1)
(test-value 0)
49 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
49/90
.
50. .
then/else 支持多个表达式
(defn test-value2 [x]
(if (> x 0)
(do (println "Yeah")
(println "Right"))
(println "No")))
(test-value2 1)
(test-value2 0)
• 如果 then/else 部分有多个表达式,则需要用 do 包装一下
• do 就类似 C/Java 中的大括号
50 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
50/90
.
51. .
条件操作符 when
(defn test-value3 [x]
(when (> x 0)
(println "Yeah")
(println "Right")))
(test-value3 1)
• 相当于没有 else 的 if
• 可以直接写多条表达式
51 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
51/90
.
52. .
循环——loop/recur
(defn print-1-to-10 []
(loop [x 1]
(when (<= x 10)
(println x)
(recur (+ x 1)))))
(print-1-to-10) ; ==> 打印 1 到 10
• loop 定义递归点以及 symbol 初始值
• recur 跳转到 loop 处,并以参数值来更新 loop 处的
symbol 绑定
• recur 必须是逻辑上的最后一句语句,后面没有任何别的
表达式
52 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
52/90
.
53. .
在函数中使用 recur
(defn print-x-to-10 [x]
(when (<= x 10)
(println x)
(recur (+ x 1))))
(print-x-to-10 5)
• 函数入口也可以作为一个递归点(相当于有个隐藏的 loop
)
53 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
53/90
.
54. .
loop/recur 可读性问题
• loop/recur 的可读性不好
• 是为了解决 JVM 不支持 TCO(尾递归优化 Tail Call
Optimization)的问题
• 实践中往往有更高层次的选择,doseq , for, map 等
• 很少有直接使用 loop/recur 的需要
(defn print-y-to-10 [y]
(doseq [x (range y 11)]
(println x)))
(print-y-to-10 5)
54 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
54/90
.
55. .
和 Java 互操作
• java.lang 包以外的类需要 import 才能用
• 通过 new 操作符来创建 Java 对象
• 也可以使用 ClassName. 宏,它往往更简洁
• 使用 .methodName 来调用实例方法,对象作为第一个参数
• 使用 Class/methodName 来调用静态方法
• 特殊的内部类——无法像在 Java 中那样直接使用
◦ 需要使用 ParentClass$ChildClass 的形式使用
◦ 事实上这才是内部类的真实面目
55 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
55/90
.
56. .
和 Java 互操作——示例
(import 'java.util.Calendar
'java.util.Date
'java.text.SimpleDateFormat)
(let [date (new Date)
cal (Calendar/getInstance)
fmt (SimpleDateFormat. "yyyy-MM-dd HH:mm:ss")]
(println (.toString date))
(doto cal
(.set Calendar/YEAR 2011)
(.clear Calendar/SECOND))
(println (.format fmt (.getTime cal))))
56 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
56/90
.
57. .
Topic
今天的主题
Clojure 简介
揭开 Lisp 的神秘面纱
Clojure 语言特性一览
函数式编程
并发支持
57 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
57/90
.
58. .
高阶函数
• 能使用高阶函数的条件是语言支
. 持 First-class Function
什么是高阶函数? ◦ 函数也是“一类对象”
. ◦ 可以作为变量的值
参数或者返回值是函数 ◦ 可以作为参数传递
的函数就是高阶函数 ◦ 可以作为返回值
(Higher-Order • 支持 First-class Function 的语言:
Function)。map , filter
◦ 所有 Lisp 方言
, reduce 都是典型的高 ◦ JavaScript
阶函数。
. ◦ Groovy,Scala
• 高阶函数是函数式编程的基石
58 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
58/90
.
59. .
map
(map count
["Foo" "Hello" "World"])
; ==> (3 5 5)
(map #(.toUpperCase %)
["a" "B" "c" "d"])
; ==> ("A" "B" "C" "D")
.
map 函数的语义
.
• map 对输入的集合作变换操作
◦ 对输入集合的每个元素应用给定的函数
◦ 得到的集合包含变换后的元素
◦ 给定函数支持一个参数
.
59 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
59/90
.
60. .
filter
(filter even? (range 10))
; ==> (0 2 4 6 8)
(filter #(> (count %) 0)
["" "Foobar" "" nil
"Hello" "World"])
; ==> ("Foobar" "Hello" "World")
.
filter 函数的语义
.
• filter 对输入的集合作过滤操作
◦ 对输入集合的每个元素应用给定函数
◦ 返回逻辑真的元素会保留在结果集合中
◦ 给定函数支持一个参数
.
60 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
60/90
.
61. .
reduce
• 使用给定的函数”折叠“输入集合的所有元素
◦ 即将多个元素”合并“成一个
◦ 也常被称作 fold
◦ 可选择给定初值
• reduce 的工作过程
◦ 对集合的第一、二个元素或者初值、第一个元素应用给定函
数
• 取决于有没有提供初始值
◦ 不断用得到的中间结果和下一个元素调用给定函数
◦ 集合遍历结束后返回最后得到的中间结果作为最终结果
• reduce 可以表达很多强大逻辑
61 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
61/90
.
62. .
reduce 示例
(reduce + (range 10))
; ==> 45
(reduce max
(map count
["a" "aa" "abc" "ef"]))
; ==> 3
(reduce (fn [m e]
(assoc m e (count e)))
{}
["america" "china" "japan"
"england" "korea"])
; ==> {korea 5, england 7, japan 5, china 5, america 7}
62 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
62/90
.
63. .
闭包(Closure)
(def counter
.
什么是闭包? (let [x (atom 0)]
. (fn []
闭包是指绑定了外围局部变量 (swap! x inc))))
环境的函数,闭包是一种将数
据和行为绑定的方式(对象是 (counter) ; ==> 1
另一种方式)
. 。 (counter) ; ==> 2
(counter) ; ==> 3
63 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
63/90
.
64. .
闭包让高阶函数变得实用
• 闭包对函数的使用者是透明的
◦ 对于调用者来说都是个可调用的“函数”
◦ 例如上面的 counter 调用者对计数器一无所知
• 闭包让函数的组合变得更简单
◦ 数据和行为的绑定对外界是透明的
◦ 可以方便地通过部分调用来适配接口
• comp/partial
(map (partial * 10) (range 10))
; ==> (0 10 20 30 40 50 60 70 80 90)
(map (comp (partial + 10)
(partial * 10))
(range 10))
; ==> (10 20 30 40 50 60 70 80 90 100)
64 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
64/90
.
65. .
没有闭包的语言怎么办?
• 没有闭包的函数需要用户自己来考虑数据传递问题
◦ C 的函数指针一定程度上也类似类似 First-class Function
• 对象是穷人的闭包;闭包是穷人的对象。
◦ Java 只能通过难看的内部类加 final 引用来模拟闭包
65 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
65/90
.
66. .
函数是更高级的抽象
• map/reduce/filter 三个操作即可组合表达各种复杂逻辑
◦ 函数式是比面向对象更高级的抽象
• 函数式编程风格不应该看到太多 for 循环之类的构造
◦ 事实上,Clojure 中的 for 和 Java/C/C++ 等语言的 for 是
完全两码事
• 函数式风格以数据为中心
◦ 函数无副作用,以数据为输入、以数据为输出
◦ 通过对数据进行变换、过滤和折叠来表达复杂逻辑
◦ map/reduce/filter 只是最常用的三个数据操作函数,是最
“通用”的,但实际上有很多更加具体的实用函数
• 排序,分组,多个数据结构的组合
• 查看源码就能发现,它们大多也是用 map/reduce/filter 实现
的
66 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
66/90
.
67. .
数据驱动编程
• 示例:查找给定目录中尺寸最大的文件
(defn find-largest-file [dir]
(reduce (fn [f1 f2]
(if (> (second f1) (second f2))
f1
f2))
(map (fn [f]
[(.getName f) (.length f)])
(filter #(.isFile %)
(.listFiles (io/file dir))))))
67 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
67/90
.
68. .
-> 和 ->> 宏
• 使用了 Threading 宏(-> 和 ->> )后,可读性提升很多
• 很好反映出数据在计算步骤之间的流动
(defn find-largest-file-ex [dir]
(->> (io/file dir)
.listFiles
(filter #(.isFile %))
(map (fn [f]
[(.getName f) (.length f)]))
(reduce (fn [f1 f2]
(if (> (second f1) (second f2))
f1
f2)))))
68 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
68/90
.
69. .
继续增强——寻找 Top-N 个最大的文件
(defn find-top-n-files [dir n]
(->> (io/file dir) .listFiles
(filter #(.isFile %))
(map (fn [f] [(.getName f) (.length f)]))
(sort-by (comp - second))
(take n)
(map (fn [[name size]]
[name (cond
(> size 1048576)
(str (quot size 1048576) "MB")
(> size 1024)
(str (quot size 1024) "KB")
true
(str size "B"))]))))
69 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
69/90
.
70. .
自定义高阶函数
• map/filter/reduce 已经能解决很多问题
• 但当发现代码中有一些可复用的操作时
◦ 进一步抽象
◦ 定义自己的高阶函数
70 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
70/90
.
71. .
继续增强——抽取 take-top 函数
(defn take-top [keyfn n coll]
(take n (sort-by keyfn coll)))
(defn find-top-n-files-ex [dir n]
(->> (io/file dir) .listFiles
(filter #(.isFile %))
(map (fn [f] [(.getName f) (.length f)]))
(take-top (comp - second) n)
(map (fn [[name size]]
[name (cond
(> size 1048576)
(str (quot size 1048576) "MB")
(> size 1024)
(str (quot size 1024) "KB")
true (str size "B"))]))))
71 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
71/90
.
72. .
Sequence 抽象
• 类似 Java/Python 里的迭代器概念
◦ map, vector, list, set 等都可以被当作 seq 使用
◦ 应该理解为对顺序产生数据的抽象
◦ 不一定 和某个数据结构关联
• sequence 是 Clojure 的核心之一
◦ 设计优雅
◦ 功能强大
◦ 想玩好 Clojure,必须深入理解 sequence
• lazy sequence
◦ 让函数式编程在表达力强大的同时性能也很好
◦ 针对数据的操作如 map, filter 等返回的都是 lazy seq
72 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
72/90
.
73. .
Laziness
• 通过 lazy-seq 函数可以构造 Lazy Sequence
• 只有在需要 sequence 的值的时候才会运算
• Clojure 中针对 sequence 的大部分操作都是 lazy 的
• 通过 Lazy 运算可以支持无限长度的 sequence
◦ range/repeat/iterate 等函数返回的都可能是无限 sequence
◦ 在代码中通过 take 等函数来获取其中的一部分
73 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
73/90
.
74. .
例子:解析 nginx 日志获取访问排名数据
• 截取字段
(ns nginx
(:require [clojure.java.io :as io]))
(def ^:private nginx-log-regex
#"^([0-9.]+) - - [(.*)] "([A-Z]+) (.*) HTTP/(1.[01
(defn extract-log-fields
"Extract fields from a line of nginx log"
[line]
(vec (->> line
(re-seq nginx-log-regex)
first
rest)))
74 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
74/90
.
75. .
解析 nginx 日志获取访问排名数据
• 核心逻辑
(defn parse-ip-freq
[file black-list n]
(with-open [rdr (io/reader file)]
(let [black-list (set black-list)]
(->> (line-seq rdr)
(map (comp first extract-log-fields))
(remove #(or (nil? %) (black-list %)))
frequencies
(take-top (comp - second) n)))))
75 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
75/90
.
76. .
函数式编程——一点总结
• 代码逻辑体现为数据在计算步骤之间的流动
• 每个计算步骤都是一个函数调用
◦ 这些函数是没有副作用的
◦ 可以组合、变换这些函数以获得需要的功能
• 数据以 sequence 为载体
◦ 不一定是内存中具体数据结构
◦ 计算/IO 过程的抽象
◦ 甚至是将更复杂的结构表达为序列
• 树的遍历
• 计算是 Lazy 的
◦ 只有当数据被消费的时候才会触发计算步骤的执行
◦ 没有额外的内存/GC 开销
76 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
76/90
.
77. .
Topic
今天的主题
Clojure 简介
揭开 Lisp 的神秘面纱
Clojure 语言特性一览
函数式编程
并发支持
77 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
77/90
.
78. .
简单的异步支持——future
• 将 body 部分的代码放到异步线程池里执行,并返回一个对
应的 Future 对象
◦ 通过 deref 来引用 future 的返回值
◦ 方便的 reader macro @ 和 deref 等价
(defn async-hello []
(future
(Thread/sleep 5000)
(println "Hello")
(* 10 10)))
(def a (async-hello))
(deref a)
@a
78 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
78/90
.
79. .
简单利用多核来做并行计算——pmap
• 和 map 语义是一致的,但在异步线程池中半 lazy 地执行
• 数据在线程之间的交换是由 Clojure 来解决的,无需用户干
预
◦ 线程安全无需自己考虑
• 在多核机器上能获得很大性能提升
◦ 一般需要将输入划分成合适的 chunk 来并行执行
◦ chunk size 的调整也很重要
79 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
79/90
.
80. .
访问排名示例的 pmap 强化版
(defn parse-ip-freq-pmap
[file black-list n]
(with-open [rdr (io/reader file)]
(let [lines (line-seq rdr)
chunks (partition-all 5000 lines)
black-list (set black-list)]
(->> chunks
(pmap
(fn [chunk]
(->> chunk
(map (comp first extract-log-fields))
(remove #(or (nil? %) (black-list %)))
frequencies)))
(reduce (partial merge-with +))
(take-top (comp - second) n)))))
80 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
80/90
.
81. .
原子类型——atoms
• 类似 Java 中的 AtomicReference
• 可以引用任意数据类型
• 通过 swap!/compare-and-set!/reset! 来操作
(def counter
(let [x (atom 0)]
(fn []
(swap! x inc))))
81 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
81/90
.
82. .
STM 支持——refs
• 软件事务内存
• 通过类似数据库事务的机制来操作内存数据
• 数据事务可以保证 ACID,软件事务内存可以保证 ACI,D
无法保证
• ref 代表一个需要通过事务访问的引用
• 所有对 ref 的访问都需要包装在事务中
◦ 通过 dosync 包装事务操作
• 事务提交时发现冲突会导致事务被重试
◦ 因此 dosync 中的代码可能被执行多次
◦ 必须是无副作用的,否则会有逻辑问题
82 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
82/90
.
83. .
示例:银行转帐
(defn transfer-money [from to amount]
(dosync
(alter from - amount)
(alter to + amount)))
(def my-account (ref 1000))
(def your-account (ref 1000))
(transfer-money my-account your-account 400)
@my-account ;==> 600
@your-account ;==> 1400
83 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
83/90
.
84. .
异步支持——agents
• 通过异步操作的原子引用
• 使用 send 和 send-off 来发起异步的 agent action
• agent action 会排队在 agent thread pool 里执行
• agent action 执行的结果会被原子地更新到 agent 里
• 支持 STM,事务中执行的 send 和 send-off 会保证不会
重复提交 agent action
84 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
84/90
.
85. .
Clojure 并发编程——一点总结
• 这里只是大概介绍一些最基本的内容
◦ 最常用的还是 atom
◦ 需要用到 STM 的场景其实不多(我没遇到过)
◦ 坚持用无副作用的函数式风格本身对并发就有很大帮助,不
一定非要引入这些机制
• 想用好需要花功夫深入探索
◦ Concurrency is hard!
◦ STM 也有其自身的问题,不是万能的
85 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
85/90
.
86. .
今天没有谈到的话题
• 数据结构的更多操作
◦ 重在介绍思想而不是具体的 API
◦ 可以查文档来进一步学习
◦ 一定要查文档,标准库替你做的比你想象的多很多
• 多态机制
◦ defprotocol 和 defrecord ,接口和自定义数据类型
◦ Multi-method,更加灵活的动态派发机制,完爆 OO 几条街
• 宏
◦ 上面提到的各种
deftask/defcluster/defpage/deftr069type 等等都需要通
过宏来实现
◦ 不需要宏也能走很远
◦ 用在需要 DSL 的场合
86 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
86/90
.
87. .
今天没有谈到的话题
• 工具、环境
◦ 构建工具:leiningen
◦ Eclipse IDE:Counterclockwise
◦ Emacs clojure-mode
• 开始探索的时候自己学习就好
◦ 网络上遍布各种教程
87 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
87/90
.
88. .
Clojure 书籍
• 基础阶段
◦ Programming Clojure 2nd Edition
◦ Clojure Programming
• 进阶阶段
◦ The Joy of Clojure
• 都有人在翻译,其中前两本应该快要出版了
88 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
88/90
.
89. .
Clojure 网络资源
• 官网:http://clojure.org
• CN-Clojure:https://groups.google.com/group/cn-clojure/
◦ 国内的 Clojure 牛人都活跃在此
• Clojure 中文维基:http://wiki.clojure.cn/
• ClojureDocs:http://clojuredocs.org/
• CDS Project:http://clojure-doc.org/
◦ 各种教程
• Clojure Handbook:
http://qiujj.com/static/clojure-handbook.html
◦ 少有的完整中文手册,强烈推荐
89 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
89/90
.
90. .
Happy Hacking Lisp
90 /
Jerry Peng JVM 上的实用 Lisp 方言:Clojure 90
90/90
.