HOME | EDIT | RSS | INDEX | ABOUT | GITHUB

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 是好东西

lisp_cycles.png

上世纪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 到底是怎么做到的.

  1. expand macro
  2. eval list 中的每一个元素
  3. 用第一个元素作为函数, 后边所有元素作为参数

    ((or nil +) 1 2 (+ 3 4))
    ; => (+ 1 2 7)
    
    => 10
    

第一部 expand macro 我们到后面macro的时候讨论

Reader

还记得搭建环境是提到的 REPL 吗? 也就是 Read Eval Print Loop

正常的Clojure程序的运行只经过前两个步骤, Read 和 Eval. 因此我们可以理解

  1. 有一个Reader去读取list
  2. 生成对应的clojure数据结构
  3. 扔给Evaluator
  4. 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")))

TODO Functional Programming

TODO Collection

TODO Concurrency

TODO 多态