F.1 Clojure语言的DSL相关特性

Clojure是一种植根于JVM的函数式编程语言,以通用编程语言为定位。它属于动态类型的语言,具有类型推断的特性,还可根据需要向编译器提供类型提示(type hint)以提高效率。Clojure是一种Lisp方言,直接编译成JVM字节码执行。Clojure语言具有同像(homoiconic)的特征,且内建了丰富而强大的并发控制结构。

除了表F-1所列的项目,Clojure还有很多值得了解的语言特性,包括并发和状态管理方面的特性、各种缓求值序列(lazy sequence)、序列推导(sequence-comprehension)和循环,以及众多高级数据结构。详情可以查阅第F.2节文献[1]。

表F-1 Clojure语言特性汇总

函数式——围绕函数来组织DSL
我们的整套DSL全部由函数构成 函数是Clojure首要的语言构造。Clojure的函数支持高阶函数和闭包特性,也支持匿名函数 Clojure虽然建立在Java之上,但它的函数式特征完全占据主导地位。尽管Clojure允许我们下降到Java层面去调用对象语义,但就语言习惯来说,Clojure是函数式的
  1. (str "hello" " " "world")
  2. => "hello world"
  3. (count [1 2 3 4 5])
  4. => 5
  5. (+ 12 20)
  6. => 32
前缀语法现在已经不会让人感到陌生 注意: Clojure具有“同像(homoiconic)”的性质。函数调用操作本身也是一个列表,以函数的名字作为列表的第一个元素。
  1. (filter even? [1 2 3 4])
  2. => (2 4)
even?是一个Clojure函数,其作用是当输入为偶数时返回true。我们在上面例子中将even?函数作为参数之一传递给filterfilter会用它来逐一筛选输入列表的每一个元素。
  1. (filter #(or (zero? (mod % 3))
  2. (zero? (mod % 5)))
  3. [1 3 5 7 9 10 15])
  4. => (3 5 9 10 15)
我们还可以把匿名函数传递给filter
函数式——函数定义
我们通过defn宏来定义函数。如右边的例子所示,其语法非常纯粹。函数本身也是数据,只不过开头的位置添加了一个有语义意义的符号,这是Clojure(以及其他Lisp变体)最为引人注目的特点
  1. (defn ^String greet
  2. "Greet your friend"
  3. [name]
  4. (str "hello, " name))
函数定义以defn开头。然后是文档字符串(docstring),也就是函数的说明文档。接着是放在一个vector里的参数列表。最后是函数体 我们可以根据需要插入元数据,元数据以^前缀开头。例中的^String标明了函数的返回类型 这段函数定义本身也是一个Clojure列表结构,定义的每一个构成部分都是列表中的元素
抽象设计
Clojure方式传统OO方式
- 字段公开; - 对象不可变; - 由多路方法(multimethod)和协议(protocol)实现多态; - 不存在实现继承 - 数据都作为私有成员隐藏在类中; - 对象是可变的; - 由层级化的继承关系形成多态,继承关系可以是接口继承,也可以是实现继承; - 允许实现继承
序列
Clojure语言的任何一种集合数据类型都是一个序列。我们可以一视同仁地对待所有种类的序列,通过相同的API来操作它们。另外,所有的Java集合类型都可以当做Clojure序列来对待,如右边的例子所示
  1. (first '(10, 20, 30))
  2. => 10
  3. (rest [10, 20, 30])
  4. => (20, 30)
  5. (first {:fname "rich" :lname "hickey"})
  6. => [:fname "rich"]
在这段代码中: - 第一个例子在一个List上调用first; - 第二个例子在一个Vector上调用rest; - 第三个例子在一个Map上调用first
序列同时也是函数
Clojure把所有的序列类型都当成函数看待。这种观点源自序列的数学定义 Clojure的各种序列都可以从数学的角度去表述,下面是几个例子: - Vector是它的位置的函数; - Map是它的键的函数; - Set是它的成员关系的函数
  1. (def colors [:red :blue :green])
  2. (colors 0)
  3. => :red
  4. (def room {:len 100 :wd 50 :ht 10})
  5. (room :len)
  6. => 100
  7. (def names #{"rich hickey" "martin odersky"
  8. "james strachan"})
  9. (names "rich hickey")
  10. => "rich hickey"
  11. (names "dennis Ritchie")
  12. => nil
创建序列
Clojure提供了一系列的序列创建函数。其中很大一部分函数返回的序列是缓求值序列,因此可以用于创建无限长的序列
  1. (range 0 10 2)
  2. => (0 2 4 6 8)
  3. (repeat 5 3)
  4. => (3 3 3 3 3)
  5. (take 10 (iterate inc 1))
  6. => (1 2 3 4 5 6 7 8 9 10)
对序列进行筛选
Clojure为序列的筛选操作提供了若干组合子。我们应该优先使用这些组合子,尽量避免程序中出现显式的递归
  1. (filter even? [1 2 3 4 5 6 7 8 9])
  2. => [2 4 6 8]
  3. (take 10 (filter even? (iterate inc 1)))
  4. => [2 4 6 8 10 12 14 16 18 20]
  5. (split-at 5 (range 10))
  6. => [(0 1 2 3 4) (5 6 7 8 9)]
对序列进行变换
Clojure提供了大量对已存在序列进行变换操作的组合子。它们以一个序列作为输入,输出变换后产生另一个序列或值
  1. (map inc [1 2 3 4])
  2. => (2 3 4 5)
  3. (reduce + [1 2 3 4])
  4. => 10
数据结构的持久性和不可变性
Clojure语言中所有的数据结构都具有不可变(immutable)和持久(persistent)的性质。也就是说,即使一个对象发生了改变,我们仍然可以访问它的所有历史版本。而且Clojure的实现方式并不需要额外占用大量的存储空间
  1. (def a [1 2 3 4])
  2. (def b (conj a 5))
  3. a
  4. => [1 2 3 4]
  5. b
  6. => [1 2 3 4 5]
Clojure进行DSL设计的秘诀是宏。宏是一种在宏展开阶段被展开替换为有效Clojure语言成分的程序结构。 宏是我们为DSL量身定做各种语法结构的利器
  1. (defmacro unless [expr form]
  2. (list if expr nil form))
我们在这个宏里定义了一种类似于ifwhile的控制结构。它在使用的时候和一般的Clojure语言成分并没有什么两样