与君共勉:生命不息,学习不止,切忌浮躁,静下心来,每天进步一点点。
Clojure简介
Clojure是一门运行在JVM上面的Lisp方言,其它的Lisp方言还有Scheme、Common Lisp等。Lisp相关的著名书籍有《计算机程序的构造和解释》(简称SICP)、《The Little Schemer》、《黑客与画家》。
Clojure可以和Java代码互操作,它会编译成字节码运行在JVM上面,所有的Java生态都可以为Clojure所用。
Clojure的语法比较怪异,采用前缀表达式,比如一般编程语言中的3+4、5-3、add(4,6),在Clojure中统一写成(+ 3 4)、(- 5 3)、(add 4 6),对编译器很友好(如果你了解抽象语法树的形式会很好理解),且因为这种统一,无需记忆所谓的操作符优先级,而且语法元素也很少,正是因为这种简洁,Lisp系的语言的数据和代码被统一了起来,即所谓的代码即数据(代码本身就是它的数据结构,比如(+ 3 4)是代码,但是它作为读取器处理的对象,可以将它看作list类型的数据,因为list类型的数据就是(1 2 3)或(+ 4 5)这种形式
)。
普遍存在的数据结构
大部分编程语言都有集合类型,比如array、list、set、map等等,它们都有自己的具体实现。
比如在静态类型语言中,如Java、Rust,数组的元素都是连在一起的,数组的元素类型都是一致的,这样根据下标访问数组元素,可以简单地通过数组指针加上数组元素占用的字节大小乘以下标快速定位到特定元素。动态语言中,如JavaScript中,数组的元素类型不要求是一致的,比如可以这样声明一个数组:
let arr = [2,"花无缺"]; // 数组元素可以是Number和String类型混在一起的
比如list,在Java中分ArrayList和LinkedList,LinkedList是通过链表来实现的,Clojure中的list也是通过链表来实现的(链表元素在内存中可能相隔很远),链表实现的优点是,访问链表头特别快,在链表头追加一个元素特别快,但是访问链表中其它的元素比较慢,需要一个一个地跳转。
Clojure中的集合类
Clojure中常用的集合类型的数据结构有:list、vector、set、map等,它们有字面量表示方式(即在代码中写死的表示),也可以通过函数创建它们。
例如:
;;list的字面量需要有一个单引号在小括号前面,如果不加,Clojure会认为(1 2 3)中的1是一个函数调用,从而报错
(def this-is-list '(1 2 3)) ;;将包含1、2、3这三个数字的list绑定到this-is-list变量上
(def this-is-list2 (list 1 3 4)) ;;通过list函数创建一个list
;;vector的字面量是中括号包起来的
(def this-is-vector ["江苏" "南京" "江宁"]) ;;通过字面量创建一个vector,其中元素是三个字符串
(def this-is-vector2 (vector :name :sex :age)) ;;通过vector函数创建一个vector,其中元素是三个关键字(keyword)
;;set的字面量要用#{}包起来
(def this-is-set #{2 3 4}) ;;字面量方式
(def this-is-set2 (set [4 5 6]));;通过set函数创建,注意set函数后面的参数必须是集合类型,这儿用一个vector作为参数
(def this-is-set3 (set '(4 5 6)));;通过set函数创建,注意set函数后面的参数必须是集合类型,这儿用一个list作为参数
;;map的字面量要用大括号{}包起来,Clojure中的map的各个key-value之间可以不用逗号分割
(def this-is-map {:name "明月" :address "无双城"}) ;;字面量方式创建map,key的类型是关键字(keyword),value的类型是字符串
(def this-is-map2 (hash-map :name "第二梦" :address "绝情谷"));;通过hash-map函数创建map
Clojure的哲学
Clojure是一门特别推崇函数式编程范式的编程语言,它有一个设计哲学:
It is better to have 100 functions operate on one data structure
than 10 functions on 10 data structures.
—Alan Perlis
即在1种数据结构上定义100中操作方法,比在10种数据结构上定义10种方法更好。
也就是说,Clojure编程语言,不像面向对象编程语言那样提倡多创建自己的类型,然后再在每个类型上添加方法,通过方法之间的调用来构造复杂的系统,比如在Java中,你可以自定义User类、Employee类,每个类都有自己的方法。然而,这些自定义类的字段仍不可免俗地是String、Integer、Boolean这些类型。所以,Clojure的设计者认为,提供map类型的数据结构,里面可以放各种类型,然后通过给map提供丰富多彩的函数操作,一样可以构造复杂系统。
比如在Java中定义的User类可能是这样的:
public class User {
private String name;
// 通过给User类定义name方法来访问name字段
public String getName(){
return this.name;
}
private int age;
// 通过给User类定义getAge方法来访问age字段
public int getAge(){
return this.age;
}
}
在Clojure中,我们可以将用户信息放到map中,然后通过map的key来访问用户信息:
(def user {:name "江别鹤" :age 50})
;; 通过 :name字段来获取姓名
(user :name)
;; 或者通过 :name关键字作为函数来使用,没错,Clojure中的关键字可以作为函数来使用
(:name user)
;; => "江别鹤"
;; 通过 :age字段来获取年龄
(user :age)
;; 或者通过 :age关键字作为函数来使用
(:age user)
;; => 50
上面我们介绍过Clojure中的几种常见的集合类型,在Clojure的设计哲学的指导下,Clojure中的集合类型上面有大量的函数可以使用,比如:
;; 提到的函数有点多,耐心点,为了后面讲的抽象做铺垫
;; empty?函数,没错,Clojure中的函数或变量中可以包含问号、中划线、感叹号等
(empty? [1 3 4]) ;; 判断vector是否为空
(empty? '("慕容仙" "江玉凤" "苏樱")) ;; 判断list是否为空
(empty? #{23 45 "呵呵"}) ;; 判断vector是否为空
(empty? {:name "花无缺" :age 20}) ;; 判断map是否为空
;; seq函数,返回序列
(seq [23 4 5]) ;; 返回(23 4 5)
(seq '(43 5 66)) ;; 返回(43 5 66)
(seq #{45 23 2}) ;; 返回(2 23 45)
(seq {:name "怜星" :sister "邀月"}) ;; 返回([:name "怜星"] [:sister "邀月"])
;; first函数
(first [23 4 5]) ;; 获取vector的第一个元素,返回23
(first '(43 5 66)) ;; 获取list的第一个元素,返回43
(first #{45 23 2}) ;; 获取set的第一个元素,返回2
(first {:name "怜星" :sister "邀月"}) ;; 获取map的第一个key-value对,返回[:name "怜星"]
;; rest函数,获取集合第一个元素之外的元素列表
(rest [23 4 5]) ;; 获取vector的第一个元素之外的元素,(4 5)
(rest '(43 5 66)) ;; 获取list的第一个元素之外的元素,返回(5 66)
(rest #{45 23 2}) ;; 获取set的第一个元素之外的元素,返回(23 45)
(rest {:name "怜星" :sister "邀月"}) ;; 获取map的第一个key-value对之外的元素,返回 ([:sister "邀月"])
;; map函数,类似Java中stream上面的map函数,对每一个元素应用一次函数
(defn say-hello [x] (str "Hello," x)) ;; 定义一个say-hello函数,对入参拼上一个"Hello,"字符串前缀
(map say-hello ["南京" "兰州"]) ;; 返回("Hello,南京" "Hello,兰州")
(map say-hello #{"江宁" "百家湖"}) ;; 返回("Hello,江宁" "Hello,百家湖")
;; reduce函数,在Clojure中+-*/这些符号都是函数
(reduce + [2 3 4]) ;; 对vector中的所有元素求和,返回9
(reduce * '(4 6 7)) ;; 对list中的所有元素求乘积,返回168
;; conj函数,往集合中追加元素
(conj [2 3 4] 5 6 7) ;; 返回 [2 3 4 5 6 7]
(conj [1 2] 3) ;; 返回[1 2 3]
(conj '(1 2 3) 4) ;; Clojure中的list底层实现为链表,追加元素的时候,放在表头最快,所以返回(4 1 2 3)
(conj '(1 2 3) 4 5) ;; Clojure中的list底层实现为链表,追加元素的时候,放在表头最快,返回(5 4 1 2 3)
(conj {:name "张无忌" :age 25} [:first-girl-friend "周芷若"]) ;; 返回{:name "张无忌", :age 25, :first-girl-friend "周芷若"}
;; cons函数,往序列中添加元素
(cons 3 [1 2 3]) ;; 返回(3 1 2 3)
(cons 5 '(2 3)) ;; 返回(5 2 3)
(cons 6 #{5 6}) ;; 返回(6 6 5)
介绍这么多函数,有的函数返回的是集合本来的类型。比如:
对于conj
函数。入参是vector,返回的还是vector,入参是list,返回的还是list。
有的函数返回的和入参的集合类型不同,比如:
cons
函数不管入参是什么类型,返回的都是小括号包起来的输出。
是否有点凌乱?
抽象的力量
让我们站在更高的角度来审视这个问题。我们知道对于Java的List、Set、Map,一些工具类,如CollectionUtils提供了isEmpty、isNotEmpty来判断集合是否为空。对于List、Set、Map中的元素想挨个处理,可以通过这些集合上的stream方法转换成流以后,使用map、filter等方法来处理。
也就是说,我们通过提供更高层次的抽象,来屏蔽掉了底层数据结构的不同,让上层可以用统一的方式来处理底层的数据。
所谓的抽象,不过是一组操作的集合,满足了这些操作的集合的数据类型,就是这种抽象的一种具体实现
。比如汽车是一种抽象,它有前进、转弯、后退、加油、开窗等操作,具体的类型可以是大巴车、小轿车、公交车。
然而,对一组类型进行抽象,只能提取它们共同部分的信息,比如集合都有获取第一个元素、获取第n个元素、遍历元素这些操作,所以这些操作都可以提取成一种更高层次的抽象,在Clojure中表现为first、nth、map这些函数,然而真正落实到具体的数据结构上的时候,它们又不同,所以需要将它们转换成中间的一层统一的抽象,在Clojure中就是所谓的seq(或者叫sequence,即序列),当各种各样的数据结构转换成seq,就可以对seq进行first、second、rest、map等操作。就像Java中的各种集合调用了stream之后转换成了统一的抽象后,才可以使用map、filter、collect等操作。
Clojure中的两大抽象
Clojure有两个重要的抽象,collection和seq,即集合和序列,Clojure的list、vector、map、set都实现了这两大抽象。前面我们说过,所谓的抽象就是一组操作的集合,符合某一个抽象的类型必定都具备这个抽象中的所有操作,而满足一个抽象中的所有操作的类型也可以实现为这个抽象,进而可以被针对这个抽象实现的所有的函数使用。
collection的抽象中的主要操作有empty?、contains?、every等,它是将数据作为一个整体进行处理的;
seq的抽象中的主要操作有first、second、rest、map等,它是将数据作为一个序列,从而可以一个一个地处理。
当我们将list、vector、set、map数据类型传递到形参是seq类型的函数式,会发生隐式转换,会先调用seq函数将其转换成seq抽象,比如:
;; 下面两个是等价的,返回的都是4,因为(seq #{2 3 4})返回的是(4 3 2)这个seq
(first #{2 3 4})
(first (seq #{2 3 4}))
参考资料
1.《Clojure for the brave and true》