2010年5月30日星期日

Clojure 的类型提示

为了提高与 Java 类库交互时的性能,Clojure 提供了元数据用以定义某个给定的变量的类型。比如在函数定义的时候可以指定参数与返回值的类型:

(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。

没有评论: