(defn add-elem [#^java.util.List java-list elem]
(.add java-list elem)
java-list)
等效于
(defn add-elem [#^{:tag java.util.List} java-list elem]
(.add java-list elem)
java-list)
如果不告诉编译器这个类型
(defn naive-add-elem [java-list elem]
(.add java-list elem)
java-list)
Clojure 一样可以顺利执行,只不过 Clojure 是通过反射去调用 add 方法的。因为编译器没办法知道运行时的参数可能会是什么类型。所以,两种方法的执行时间差距还是有点大的:
(def java-list (java.util.ArrayList.))
(time (dotimes [_ 100000]
(naive-add-elem java-list 1))) ==> "Elapsed time: 342.663873 msecs"
(time (dotimes [_ 100000]
(add-elem java-list 1))) ==> "Elapsed time: 18.652259 msecs"
有一个数量级的差距,跟反射与普通方法调用的差距差不多。但是不带类型提示的版本会在重用性上取胜。比如:
(def java-list (java.util.ArrayList.))
(def java-set (java.util.HashSet.))
(naive-add-elem java-list 1) ==> #<ArrayList [1]>
(naive-add-elem java-set 1) ==> #<Hashset [1]>
(add-elem java-list 1) ==> #<ArrayList [1]>
(add-elem java-set 1) ==> ClassCastException!
与类型提示有点关系的就是 :inline。为了加快 Clojure 在 JVM 上的数字运算,Clojure 提供了内联。因为 Clojure 与 C/C++ 一样具有宏的功能(只不过强大了太多),所以 Clojure 也可以做到源代码展开。比如 num 的实现:
(defn num
{:tag Number
:inline (fn [x] `(. clojure.lang.Numbers (num ~x)))}
[x] (. clojure.lang.Numbers (num x)))
编译器何时会选择内联我不知道,只是它的确有这种优化。因为在频繁数字运算时 HotSpot 会将包装类型优化成原始类型,所以 Clojure 依赖 JVM 的这个特性,通过内联将优化的任务交给 JVM 去做。Clojure 的官网说下面的两段代码在 JVM 的 -server 属性开启时运行速度一样:
static public float asum(float[] xs) {
float ret = 0;
for (int i = 0; i < xs.length; i++)
ret += xs[i];
return ret;
}
(defn asum [#^floats xs]
(areduce xs i ret (float 0)
(+ ret (aget xs i))))
Clojure 在给数字运算型函数做内联的时候与普通宏不一样的地方是,内联的宏都返回一个函数。这样它们就可以被扔到 map, reduce 这样的函数去执行了。对于原始类型数组,Clojure 特别添加了支持:#^ints, #^floats, #^longs, #^doubles。关于内联具体请参看:http://clojure.org/news
对于那些无法在函数声明时在参数列表就加上类型提示的,可以在 Java 调用的时候再指明参数类型。比如:
(defn bigint
{:tag BigInteger}
[x] (cond
(instance? BigInteger x) x
(decimal? x) (.toBigInteger #^BigDecimal x)
(number? x) (BigInteger/valueOf (long x))
:else (BigInteger. x)))
这个函数在声明参数 x 的时候并没有指明类型,但是通过 decimal? 测试后 x 就应该是 BigDecimal 类型的了,这个时候就可以告诉编译 x 的类型是 BigDecimal。
没有评论:
发表评论