/home/matstani/weblog

programming log.

[Clojure] core.typedで静的型チェック

Clojureは動的型付けの言語ですが、core.typedライブラリを利用すると、静的型チェックを行うことができます。
バージョン0.2.0リリースで、Production Readyとアナウンスされました。
単純な例を試してみたのでメモします。

テスト用プロジェクト作成

1
2
$ lein new try-typed
$ cd try-typed

project.clj[org.clojure/core.typed "0.2.3"]を追記します。

1
$ lein deps

静的型チェックの実行

core.typedを利用した静的型チェックは、プログラムのコンパイルとは別に、明示的に実行します。
REPL内でclojure.core.typed/check-ns関数を実行することにより、指定したnamespaceをチェックします。

1. REPL起動

1
$ lein repl

2. 静的型チェック実行

先ほど作成したテスト用プロジェクトに対し、型チェックを行ってみます。

1
2
user=> (use 'clojure.core.typed)
user=> (check-ns 'try-typed.core)

上記を実行すると、以下のようにエラーになります。

1
2
3
Type Error (try-typed.core:3:1) Found untyped var definition: try-typed.core/foo
Hint: Add the annotation for try-typed.core/foo via check-ns or cf
in: (def foo (fn* ([x] (clojure.core/println x Hello, World!))))

core.typedによる静的型チェックでは、全てのvarが明示的に型付けされている必要があります。
上記では、try-typed.core/foo関数に型付けがされていないため、エラーになっています。

varに型付けを行う

clojure.core.typed/annで、varに型付けを行うことができます。
上記でエラーになったtry-typed.core/fooに型付けしてみます。
core.clj

上記の例では、try-typed.core/fooに対し「String型の引数をとりnilを返す」という型情報を付与しています。
この状態で型チェックを行うと、:okとなります。

1
2
3
4
5
6
7
8
user=> (check-ns 'try-typed.core)
Start collecting try-typed.core
Finished collecting try-typed.core
Collected 1 namespaces in 16.039067 msecs
Start checking try-typed.core
Checked try-typed.core in 65.01378 msecs
Checked 1 namespaces (approx. 8 lines) in 84.795137 msecs
:ok

型不一致エラー

clojure.core.typed/annで指定した型付けと整合しない呼び出しがあった場合、エラーとして検出されます。
例) core2.clj

1
2
3
4
5
6
7
8
9
10
11
12
user=> (check-ns 'try-typed.core)
Start collecting try-typed.core
Finished collecting try-typed.core
Collected 1 namespaces in 15.624342 msecs
Start checking try-typed.core
Checked try-typed.core in 33.318364 msecs
Type Error (try-typed.core:13:3) Type mismatch:

Expected:       String

Actual:         (Value 1.5)
in: (try-typed.core/foo 1.5)

型情報の参照

clojure.core.typed/cfで型情報を参照できます。

1
2
user=> (cf try-typed.core/foo)
(Fn [String -> nil])

標準関数についても、ほとんどがライブラリ内で型付けされています。

1
2
user=> (cf inc)
(Fn [AnyInteger -> AnyInteger] [Number -> Number])

自作関数と、標準関数の間で型の不整合があった場合には、エラーとして検出されます。
例) core3.clj

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
user=> (check-ns 'try-typed.core)
Start collecting try-typed.core
Finished collecting try-typed.core
Collected 1 namespaces in 22.306404 msecs
Start checking try-typed.core
Checked try-typed.core in 36.899735 msecs
Type Error (try-typed.core:8:3) Static method clojure.lang.Numbers/inc could not be applied to arguments:


Domains:
        AnyInteger
        Number
                
Arguments:
        String
        
Ranges:
        AnyInteger
        Number
                
with expected type:
        AnyInteger
        
in: (clojure.lang.Numbers/inc s)

関数内で正しく型変換していると、チェックは:okになります。
例) core4.clj

1
2
3
4
5
6
7
8
user=> (check-ns 'try-typed.core)
Start collecting try-typed.core
Finished collecting try-typed.core
Collected 1 namespaces in 36.006706 msecs
Start checking try-typed.core
Checked try-typed.core in 38.304219 msecs
Checked 1 namespaces (approx. 9 lines) in 78.396396 msecs
:ok

型付けの例

引数が複数

1
2
3
(ann my-add [Number, Number -> Number])
(defn my-add [n1 n2]
  (+ n1 n2))

可変長引数

1
2
3
(ann my-add [Number * -> Number])
(defn my-add [& n]
  (apply + n))

シーケンス

1
2
3
(ann my-add [(Seqable Number) -> Number])
(defn my-add [n]
  (apply + n))

Set

1
2
3
(ann my-filter [(Set AnyInteger) -> (Seqable AnyInteger)])
(defn my-filter [n]
  (filter odd? n))

Map

1
2
3
(ann get-count [(Map clojure.lang.Symbol Number) -> (U nil Number)])
(defn get-count [m]
  (m :count))

無名関数

高階関数に無名関数を渡す場合、型付けが必要になる場合があるようです。 clojure.core.typed/fn>による型付け

1
2
3
4
5
6
7
(ann inc-seq [Number, (Seqable Number) -> (Seqable Number)])
(defn inc-seq
  [i s]
    (map
       ; clojure.core.typed/fn> は無名関数のパラメータに型情報を付加できる
       (fn> [e :- Number] (+ i e))
       s))

clojure.core.typed/ann-formによる型付け

1
2
3
4
5
6
7
(ann inc-seq [Number, (Seqable Number) -> (Seqable Number)])
(defn inc-seq
  [i s]
    (map
       ; clojure.core.typed/ann-form> は任意のフォームに型情報を付加できる
       (ann-form #(+ i %) [Number -> Number])
       s))

その他

Clojureには標準で型ヒント(type hinting)と呼ばれる機能がありますが、Javaを呼び出す際のリフレクションを防ぐためのもので、core.typedのような、静的型チェックの機能ではありません。

Comments