注:本文基于jdk1.6,clojure1.2
比较操作
等于=
clojure中的等于和java中的equals方法类似,但是clojure中的=还能够作用于nil、数字和集合上面。看看例子:
user> (= 3) true user> (= 5 5) true user> (= "a" "a") true user> (= '(7 8.0 9) [7 8.0 9]) true user> (= '(7 8.0 9) [7 8 9]) true user> (= (java.lang.String. "nice") (java.lang.String. "nice")) true
从上面的例子来看,=应该是做的值比较,那我们再来验证一下看看:
user> (= (java.lang.String. "nice") (java.lang.String. "nice"))
true
user> (= 1.25 5/4)
true
user> (= 8.00M 8)
false
user> (= 8.00 8)
true
user> (= 127 0x7f)
true
user> (= (java.lang.Float. 127.0) 127.0)
true
除了声明为BigDecimal的数字之外,其他的都成功了。让我们来看看源码:
user> (source =) (defn = "Equality. Returns true if x equals y, false if not. Same as Java x.equals(y) except it also works for nil, and compares numbers and collections in a type-independent manner. Clojure's immutable data structures define equals() (and thus =) as a value, not an identity, comparison." {:inline (fn [x y] `(. clojure.lang.Util equiv ~x ~y)) :inline-arities #{2} :added "1.0"} ([x] true) ([x y] (clojure.lang.Util/equiv x y)) ([x y & more] (if (= x y) (if (next more) (recur y (first more) (next more)) (= y (first more))) false)))
从上面源码中我们可以看出,如果=函数的传入参数为1个时,会直接返回true; 即使参数是nil也是如此:
user> (= nil) true
传入参数为2个时,会调用clojure.lang.Util类的equiv静态方法,这个方法首先判断两个参数的引用是否相等,
如果引用相等,则返回true;然后判断两个参数是否都是Number的子类,如果是,则调用Number的equiv方法
来判断两个参数的值是否相等,如果相等,则返回true;接着判断其中一个参数是否IPersistentCollection的子类
,如果是,则调用其equiv方法并返回结果;如果此时还没有返回,就会直接调用参数的equals方法来做比较了。
当传入参数大于2时,按惯例,必定是要进行递归了。但是我们注意到,这儿的递归和之前数学运算中的递归
有所不同,这儿采用了recur特殊form,这是个什么东西?实际上,这是clojure的显式尾递归标识。
众所周知,递归调用的一般形式是逐层嵌套调用函数直至满足某个条件后返回结果并逐层向外传递直至到达最
外层得出最终结果,而每一次递归调用时都会往本地方法栈中压入一个栈帧,如果递归的层数够多,毫无疑问,
stackoverflow。但是很多递归实际上只需要最后一次执行的结果,递归过程中产生的结果是无用的,在这种情况
下,就可以采用尾递归。尾递归跟一般递归的不同在于,每次递归执行的方法会替换上次执行的栈帧,换句话说,
每次执行的递归操作除了将参数传递给下一次递归之外,方法体本身是没有在内存中保留的;当执行到最后一次
递归操作时,它会把结果直接返回给最外层的调用者,而不是逐层上报。这在理论上是可以进行“无限”递归的。
让我们看看当前的情况,这是一个典型的尾递归场景,不管中间运行多少次的结果为true,只要某一次运行的
结果为false,就结束递归,将false传回给调用者。如果执行到最后一次递归仍然返回true,那么直接将这个true
返回给调用者就可以了,因为如果递归中途有false就根本执行不到最后一步递归,已经执行到了最后这一步,就代表
前面的所有结果都为true。
我们再看看recur的用法,其实在一般情况下,recur都跟loop配对使用,loop声明递归点并建立临时绑定,recur
进行递归操作;而在当前的例子中,recur调用的尾递归实际上是对应的[x y & more],注意它们的参数匹配。如果
我们采用loop来重构,那么它的形式将是:(省略了说明性文字和元数据)
(defn = ([x] true) ([x y] (clojure.lang.Util/equiv x y)) ([x y & more] (loop [one x two y others more] (if (= one two) (if (next others) (recur two (first others) (next others)) (= two (first others))) false))))
这样子结构是否要清晰一些呢,只要执行到recur,就跳转到loop处开始执行。
数值等于==
==的功能和=相似,不过==只用于比较数字的值:
user> (== 2 2.00000) true
如果我们采用非Number类型的参数,就会抛出类型转换异常:
user> (== '(1 3) [1 3]) clojure.lang.PersistentList cannot be cast to java.lang.Number [Thrown class java.lang.ClassCastException]
让我们看看源码:
user> (source ==) (defn == "Returns non-nil if nums all have the same value, otherwise false" {:inline (fn [x y] `(. clojure.lang.Numbers (equiv ~x ~y))) :inline-arities #{2} :added "1.0"} ([x] true) ([x y] (. clojure.lang.Numbers (equiv x y))) ([x y & more] (if (== x y) (if (next more) (recur y (first more) (next more)) (== y (first more))) false)))
很明显,这个源码的结构和=的几乎是一模一样,唯一的区别是在做二元判断时直接调用了clojure.lang.Numbers
的equiv方法,这就决定了==只能用于数字的比较,因为多元判断同样是递归了二元判断。
不过我们也注意到,当参数只有一个的时候,实际上是没有类型判断的:
user> (== nil) true
但这个应该没什么实际用途,哈哈。
不等于not=
not=的本质其实就是对=判断的结果取反而已,所以=支持什么类型,not=就支持什么类型:
user> (not= 3) false user> (not= 5 3) true user> (not= '(7 8 9) [7 8 9]) false user> (not= 8M 8) false user> (not= 8.0M 8) true
源码一下就暴露出其本质了:
user> (source not=) (defn not= "Same as (not (= obj1 obj2))" {:tag Boolean :added "1.0"} ([x] false) ([x y] (not (= x y))) ([x y & more] (not (apply = x y more))))
不过这儿跟之前不同的地方在于,没有用递归(当然=的源码中还是有递归的),而是直接采用了apply。apply的
作用其实很好理解,它接收两部分的参数,第一部分是一个函数,在当前的例子中是=函数;第二部分是数个参数,这
个参数的数量跟第一部分的参数函数有关,比如:在当前例子中,=函数可以传入三个参数,那么第二部分我们就传入
三个参数x、y和一个集合more。执行时apply会首先把第二部分的参数传入第一部分的函数中,在当前例子,就是把
x、y和集合more中的所有元素传入到=函数中去,=函数执行的结果就作为apply表达式的结果返回。
这时候,我们可能要问了,为什么不能直接采用(not (= x y more))这样的形式来调用呢?
这儿就涉及我们之前没有提及的一个小问题:[x y & more]这种可变长度参数定义的处理方式。采用[x y & more]
这种定义时,比如,我们传入[1 2 3 4 5],1会被绑定到x,2会被绑定到y,而3、4、5则会被绑定到一个叫more的
集合中去,下面我们验证一下:
user> (defn cus+ ([x] x) ([x y] (+ x y)) ([x y & more] (class more))) #'user/cus+ user> (cus+ 1 2 3 4 5) clojure.lang.ArraySeq
这样子就明白了吧,如果我们直接采用(not (+ x y more))这样的形式,那么传入+函数的就是一个集合,从而
产生参数类型不匹配的异常:
user> (defn cus+ ([x] x) ([x y] (+ x y)) ([x y & more] (+ x y more))) #'user/cus+ user> (cus+ 1 2 3 4 5) clojure.lang.ArraySeq cannot be cast to java.lang.Number [Thrown class java.lang.ClassCastException]
而apply的作用是可以将集合中的所有元素取出来传入对应的函数中去,这样就保障了传入函数的必然是集合中
的元素,而非集合本身。
小于<
<直接用来判断数字的数值大小:
user> (< 1 2 3 4) true user> (< 0x4f 2r11111111 54e5) true user> (< 54M 89) true user> (< \a \b) java.lang.Character cannot be cast to java.lang.Number [Thrown class java.lang.ClassCastException]
如上所见,只要是Number类型的数字都可以进行比较;而不是Number类型的参数就会抛出类型转换异常。让我们
看看源码:
user> (source <) (defn < "Returns non-nil if nums are in monotonically increasing order, otherwise false." {:inline (fn [x y] `(. clojure.lang.Numbers (lt ~x ~y))) :inline-arities #{2} :added "1.0"} ([x] true) ([x y] (. clojure.lang.Numbers (lt x y))) ([x y & more] (if (< x y) (if (next more) (recur y (first more) (next more)) (< y (first more))) false)))
结构跟=函数基本上一模一样,最大的不同就在于二元操作时调用的是clojure.lang.Numbers的lt方法,进行数值
大小的比较。
大于>
>也是直接用来判断数字的数值大小,同样支持多个参数:
user> (> 5 4 3 -1) true user> (> 0xf9 8r6 2r111 5e-2) false user> (> 54M 54) false
可以预见,源码跟<基本一致:
user> (source >) (defn > "Returns non-nil if nums are in monotonically decreasing order, otherwise false." {:inline (fn [x y] `(. clojure.lang.Numbers (gt ~x ~y))) :inline-arities #{2} :added "1.0"} ([x] true) ([x y] (. clojure.lang.Numbers (gt x y))) ([x y & more] (if (> x y) (if (next more) (recur y (first more) (next more)) (> y (first more))) false)))
果然如此,除了调用的是clojure.lang.Numbers的gt方法。
小于等于<=
<=同样也是进行数字的数值判断:
user> (<= 1 1 2 4 5.0 5) true user> (<= 0x3 8r6 2r111 13/2 5e2) false user> (<= 3M 3) true
让我们看看源码:
user> (source <=) (defn <= "Returns non-nil if nums are in monotonically non-decreasing order, otherwise false." {:inline (fn [x y] `(. clojure.lang.Numbers (lte ~x ~y))) :inline-arities #{2} :added "1.0"} ([x] true) ([x y] (. clojure.lang.Numbers (lte x y))) ([x y & more] (if (<= x y) (if (next more) (recur y (first more) (next more)) (<= y (first more))) false)))
同样的结构,除了调用的是clojure.lang.Numbers的lte方法。
大于等于>=
>=也是进行数字的数值判断:
user> (>= 30.33 30 18/5 5e-2) true user> (>= 0xff 8r13 2r1111) false user> (>= 5.43M 5.43) true
可以预见,源码是没什么惊喜了:
user> (source >=) (defn >= "Returns non-nil if nums are in monotonically non-increasing order, otherwise false." {:inline (fn [x y] `(. clojure.lang.Numbers (gte ~x ~y))) :inline-arities #{2} :added "1.0"} ([x] true) ([x y] (. clojure.lang.Numbers (gte x y))) ([x y & more] (if (>= x y) (if (next more) (recur y (first more) (next more)) (>= y (first more))) false)))