Clojure The Mini Book
;; please using cider version of ob-clojure
(require 'ob-clojure)
(require 'ob-js)
;;(setq org-babel-clojure-backend 'cider)
cider
我每天用括号当早饭
为什么要学习全是括号的语言
选择Clojure是因为
- 专门为多线程并发编程设计
- 跑在JVM上,使部署变得简单
- lisp语法太简单了,函数,函数,都是函数
- 动态类型,更灵活
- 数据结构都是Immutable,mutable is evil
- 与Java交互
- 丰富的第三方库
lisp 是好东西
上世纪50年代的就有了lisp语言,都不能说它不是一门语言,因为他太多种方言了。虽然一直都不温不火,但是随着系统逻辑和计算越来越复杂,再加上分布式和并行计算。人们突然发现函数式是一个好东西,state is evil。目前比较流行的lisp方言是Clojure,Scheme。
函数式是好东西
OO并没有想象中的好,带状态和mutable的代码特别难推理,非常难读。需要特别多的上下文才能推理当前属于哪种状态,有哪些行为。如果再加上多线程,那就更难推理代码的行为了。
多态是好东西
OO的多态的概念倒是有趣的好东西。一个函数在不同类型的参数能有不同的行为,使得我们的能够更灵活的建立抽象。
多线程是好东西如果用的对
- Immutablility 减少了很多多线程带来的问题
- 加锁只会阻塞并使事情更复杂,Clojure用更妙的方式解决资源共享问题。
TODO 搭建环境
首先得有一个管理依赖的玩意,如Ruby的bundler,python的pip,js的npm。clojure用leiningen。
如果你用mac,简单的用brew安装leiningen
brew install leiningen
clojure的编辑器我推荐使用emacs,如果你觉得emacs学习曲线太陡峭,那么light table是个不错的选择。
来试试不一样的Clojure数据结构
Number
Cojure支持全面的数字类型,甚至包括分数。
1/2
=> 1/2
String
字符串只能用双引号定义哦,字符串的连接不再是加号,而是str
(str "What's your name? " "I'm fine! " "thank you! " "and you?")
=> "What's your name? I'm fine! thank you! and you?"
Vector
向量是indexed的集合,用方括号初始化
[1 2 3 4]
(vector 1 2 3 4)
=> [1 2 3 4]
由于动态类型,还支持向量内的元素可以是任何类型
(get [1 "2" {3 "4"}] 2)
=> {3 "4"}
List
和vector类似,但是却稍微不同
'(1 2 3 4)
(list 1 2 3 4)
=> (1 2 3 4)(1 2 3 4)
但是取元素的时候就和vector有所不同了
(nth '(1 2 3 4) 2)
=> 3
Set
集合也一样,元素类型可以随意
#{"1" 2 :3}
(set ["1" 2 :3 :3])
=> #{2 "1" :3}
Keyword
慢着,刚刚的 :3
是个什么玩意
没错,如果你用过ruby,基本上时一个东西,但是可以是任何字母,数字,符号,甚至包括unicode,比如emoji
没有错了,那么我们其实是可以用中文和可爱的emoji编程的,虽然有点杀马特
:abc
:34
:>_<b
:你好
:😱
=> :abc:34:>_<b:你好:😱
Map
map 非常简单,就像将键值对写在list里,不过需要用花括号
跟其他语言不一样的是key可以是任何东西,甚至是list都可以作为key
{:smile 😀}
(get-in {:first-name "NiMa" :last-name "Wang" :属性 {:颜值 0 :吐槽能量 100 }} [:属性 :颜值])
=> 0
get-in
通过一个path数组来找到深度的某个值。
lisp专用的 ' 引号
如果你觉得前面这些其实其他语言都有的话,那么你可能没有注意到在介绍list时有这样一个不起眼的玩意 '。
这是什么啊?具体是什么可能需要专门的篇幅来介绍,但是这里我可以解释它大概是神马。
如果在lisp里面见到单引号,那么你完全可以理解成literally后面那个东西,什么意思呢。
(let [男神 '(王尼玛 王大锤 张全蛋) 女神 '(孔连顺)]
(first 男神);=> 王尼玛
(first 女神);=> 孔连顺
(first ['男神 '女神]);=>男神
)
可以看到 男神
女神
都是list,但是如果在他们前面加个单引号后,他们就变成了字面的值,他们符号本身,而不会被eval成一个list。
所以由于lisp里面所有的 ()
括号都是list,但是他们是会被eval的list,他们的会返回eval后的值,但是如果在前面加上单引号,他们返回他们本身,list,不会被eval。
反引号 `
Special Forms
def
def
创建一个全局的绑定
(def a-symbol 'init)
=> #'user/a-symbol
不管是在哪里(甚至是thread里)调用 def
都会创建成全局绑定
let
let关键字非常有意思,在其他语言如js里虽然没有这个关键字,但是功能大致可以翻译成
(function(男神,女神){
男神[0]
女神[0]
}).call(this, ['王尼玛','王大锤','张全蛋'],['孔连顺'])
但是js里面很少这么干,不是么。我们通常会直接。
var 男神=['王尼玛','王大锤','张全蛋'],女神=['孔连顺'];
男神[0];
女神[0];
用 var
有什么区别。当然就是scope不一样,前例中函数内部的 男神
女神
两个值的绑定不会受到函数外的影响,同样也不会对外界造成任何影响。
比如
var 男神='葫芦娃';
(function(男神,女神){
男神[0]; //=> 王尼玛
}).call(this, ['王尼玛','王大锤','张全蛋'],['孔连顺'])
男神; //=> 葫芦娃
所以 let
理解成一个函数, binding其实就是参数
do
clojure没有statement, 全是表达式, 有了do, 可以像statement一样按顺序 eval 表达式, 返回最后一个.
loop recur
clojure的数据结构都是immutable的,意味着你(如果不用macro的话)不能像其他语言一样写for循环,也不能像其他语言这样这样的…
var 男神=['王尼玛','王大锤','张全蛋']
男神[0]='葫芦娃'
男神 // => ['葫芦娃','王大锤','张全蛋']
后一种好解决,大不了创建一个新的 男神
但是for循环怎么搞?我又不能改变一个值.
var sum=0;
for(var i=0; i<10;i++)
sum+=i
在函数式语言中,循环和遍历都必须要通过递归来实现呢。也就是我不能改变值,但是我能利用函数递归调用重新绑定参数
而在clojure中,写一个递归是如此的简单。
(do
(defn sum-to-10 [sum i]
(if (> i 10)
sum
(recur (+ sum i) (inc i))))
(sum-to-10 0 0))
=> 55
还有更简单的, 不需要定义函数的递归, 更像for循环
(loop [sum 0 i 0]
(if (> i 10)
sum
(recur (+ sum i) (inc i))))
=> 55
recur总是会递归到离它最近的 loop
或者函数
完全可以吧 loop
理解成递归版本的 let
函数, 用起来跟 let
一模一样
code? data?
list 是数据, 但是他是可以eval的数据, eval的过程中第一个元素就变成了函数, 啊哈哈哈, 甚至是加减乘除. 比如 (+ 1 2)
, 你可能觉得读着别扭. 但是如果
(+ 1 2 3 4 5)
所以list是可以执行的, list 也是代码, 因此 lisp 叫做 list processing 语言.
因此在 lisp 语言里, 数据即代码, 代码也即数据. 而这样的 list 也就是著名的 s-expression
是不是感觉到头晕了, 来看看 clojure 到底是怎么做到的.
- expand macro
- eval list 中的每一个元素
用第一个元素作为函数, 后边所有元素作为参数
((or nil +) 1 2 (+ 3 4)) ; => (+ 1 2 7)
=> 10
第一部 expand macro 我们到后面macro的时候讨论
Reader
还记得搭建环境是提到的 REPL 吗? 也就是 Read Eval Print Loop
正常的Clojure程序的运行只经过前两个步骤, Read 和 Eval. 因此我们可以理解
- 有一个Reader去读取list
- 生成对应的clojure数据结构
- 扔给Evaluator
- Evaluator对其求值
Reader的工作有些像JavaScript的 JSON.parse
, 读取json, 转换成JavaScript对象.
Macro
有了Reader, 在eval之前clojure还可以再作一些工作 – macro
macro 可以扩展一个 form 成另一种 form, 比如 when
macro
(macroexpand '(when (> 1 2) (println "you suck")))
=> (if (> 1 2) (do (println "you suck")))