長さの情報を持つ配列型とか

Common Lispでは変数の型を明示的に指定することができます。

(defun a3 (x)
  (declare (type (array * (3)) x))
  x)

こう書くと、関数a3の引数xの型は「長さ3の配列」という意味なります。
これをSBCLで動かしてやると

(a3 123)  ;=> ERROR
; The value 123 is not of type (VECTOR * 3).

このように型の違うものでa3を呼び出すとエラーを検出できます。
このエラーの検出は実行時に行われています。
そのため、関数を挟むと、その関数を呼び出すまでエラーを検出できません。

;;; a3に整数を渡す関数
(defun f1 (x)
  (declare (type integer x))
  (a3 x))  ;=> 問題なく動作 (エラーを検出できない)

こうなってしまう原因は「xの型は長さ3の配列である」という情報が、
関数a3の内側にしか伝えられないためです。
declaimを使ってa3の型を外側に伝えてやることで、この問題は解決できます。

(declaim (ftype (function ((array * (3))) t) a3)
(defun a3 (x)
  (declare (type (array * (3)) x))
  x)

(defun f1 (x)
  (declare (type integer x))
  (a3 x))  ;=>
; caught WARNING:
;   Asserted type (VECTOR * 3) conflicts with derived type
;   (VALUES INTEGER &OPTIONAL).

コンパイラが型がおかしいことをちゃんと伝えてくれました。
これで「コンパイル時に型チェックが入る言語以外でプログラムを書きたくない」
という人も安心してCommon Lispが使えます。

安心したところでもう少しプログラムを書いてみます。

(declaim (ftype (function (array) t) a*))
(defun a* (x)
  (declare (type array x))
  (a3 x))

引数に配列をとる関数a*を定義しました。
a*の引数は「配列」というだけで、長さは指定していません。
しかし、a3の呼び出しに対して(SBCLでは)警告が出されることはありません。
それでは、a*を使ってみます。

(a* (make-array 3 :initial-element 999))  ;=> #(999 999 999)
(a* 123)  ;=> ERROR
; The value 123 is not of type ARRAY.
(a* (make-array 4 :initial-element 999))  ;=> ERROR
; The value #(999 999 999 999) is not of type (VECTOR * 3).

a*に配列以外を渡すと、a*に怒られ、
a*に長さ3以外の配列を渡すと、a3に怒られます。
しかし、(SBCLでは)コンパイル時に前者しか検出できません。

;;; a*に整数を渡す関数
(defun f2 (x)
  (declare (type integer x))
  (a* x))  ;=>
; caught WARNING:
;   Asserted type ARRAY conflicts with derived type (VALUES INTEGER &OPTIONAL).

;;; a*に長さ4の配列を渡す関数
(defun f3 (x)
  (declare (type (array * (4)) x))
  (a* x))  ;=> 問題なく動作 (エラーを検出できない)

なんということでしょう。ちょとガッカリな結果です。
この結果を利用して、非常に残念な関数を定義できます。

(declaim (ftype (function ((array * (4))) t) a4)
(defun a4 (x)
  (declare (type (array * (4)) x))
  (a* x))

この関数a4は「長さ4の配列」を受け取る関数なので、
長さ4の配列以外を渡すと怒られます。
しかし、a4は最終的にa3を呼び出すので、
長さ3の配列を渡さないと怒られます。
つまり、どんなものを渡しても怒られるというわけです。

C言語(GCC)も似たような動きをします。

int a[3], (*p)[3], (*q)[], (*r)[4];
p = &a;
r = q = p;

長さ不定の配列へのポインタを中継することで、
長さ3の配列のアドレスを長さ4の配列へのポインタに代入できてしまいます。
ちなみに、C++(G++)だとコンパイルエラーになります。
G++といえば、

class C1 {} c1;
class C2 { int a[0]; } c2;

このようなクラスを作ったときに、
sizeof(c1) > sizeof(c2)が成立するおもしろ言語なのに生意気です。

Leave a Reply