引数の型チェックの有無を使用者に選択させる(sbcl)

関数を書いていると、引数の型チェックを有効にするかどうかで悩むことがたまにある。

;;;; sbcl-1.0.37

;; 足し算を行う関数
;; 引数が適切な前提としている
(defun plus-impl (x y)
  (declare ((integer 0 1000) x y)
           (optimize (speed 3) (safety 0)))
  (+ x y))

(plus-impl 1 2)  ; 正しい引数
--> 3

(plus-impl 1 'a) ; 不正な引数
--> (0)          ; どんな処理が行われるかは不定。この場合は不正な結果が返されている。

上の例の場合は、型チェック(安全性)よりも実行速度を優先している。
この関数に渡される引数が常に適切であることが確信できるならばこれでも特に問題はないが、二番目の実行例のように想定外の引数が渡される可能性があるなら、何らかのチェックを追加する必要が出てくる。

;; ユーザ(?)が実際に使用する関数
;;  - インライン
;;  - 引数の型宣言のみで、最適化宣言はない
(declaim (inline plus))
(defun plus (x y)
  (declare ((integer 0 1000) x y))
  (plus-impl x y))  ; 実際の処理はplus-impl関数に任せる

(plus 1 2)  ; 正しい引数
--> 3

(plus 1 'a) ; 不正な引数
;; ↓ちゃんとチェックして、適切なエラーを出してくれる
debugger invoked on a TYPE-ERROR in thread #<THREAD "initial thread" RUNNING
                                             {A9F2831}>:
  The value A is not of type (MOD 1001).  ; 型が違う
Type HELP for debugger help, or (SB-EXT:QUIT) to exit from SBCL.
restarts (invokable by number or by possibly-abbreviated name):
  0: [ABORT] Exit debugger, returning to top level.
(PLUS 1 A)[:EXTERNAL]

plus関数のように引数の型が宣言されており、かつsafetyが0ではない場合は、実行時(関数呼び出し時)に引数の型がチェックされるので、plus-impl関数の時のような問題はおこらなくなる*1
ただし当たり前のことだが、この場合、型のチェックという余分の処理が加わるので、実行速度自体は遅くなってしまう(はず)


理想的には、関数の使用者が(その使用状況によって)実行速度を優先するか、それとも安全性を優先するかを選択できるのが望ましい*2
で、そのために上でplus関数につけているようなインライン宣言が使えないか、というのが今回の趣旨。
関数にインライン宣言がついている場合、その関数の本体のコードは、展開先の最適化宣言の影響を受けることになるので、関数の呼出側がsafetyのレベルを操作することで、型チェックの有無も制御できる。

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; 引数の型チェックを行いたい場合
(defun 2* (x)
  (declare (optimize (safety 1)))  ; 最適化宣言そのものを省略してしまっても良い
  (plus x x))

(2* 10)  ; 適切な引数
--> 20

(2* 'a)  ; 不正な引数
debugger invoked on a TYPE-ERROR in thread #<THREAD "initial thread" RUNNING {A9F2831}>:
  The value A is not of type (MOD 1001).  ; 不正な引数

(disassemble #'2*)  
;; いろいろチェックしている
; disassembly for 2*
; 0B5EE969:       F7C603000000     TEST ESI, 3                ; no-arg-parsing entry point
;       6F:       7525             JNE L0
;       71:       8BC6             MOV EAX, ESI
;       73:       83F800           CMP EAX, 0
;       76:       7C1E             JL L0
;       78:       8BC6             MOV EAX, ESI
;       7A:       3DA00F0000       CMP EAX, 4000
;       7F:       7F15             JNLE L0
;       81:       8BD6             MOV EDX, ESI
;       83:       8BFE             MOV EDI, ESI
;       85:       8B0538E95E0B     MOV EAX, [#xB5EE938]       ; #<FDEFINITION object for PLUS-IMPL>
;       8B:       B908000000       MOV ECX, 8
;       90:       FF7504           PUSH DWORD PTR [EBP+4]
;       93:       FF6005           JMP DWORD PTR [EAX+5]
;       96: L0:   8B053CE95E0B     MOV EAX, [#xB5EE93C]       ; '(MOD 1001)
;       9C:       CC0A             BREAK 10                   ; error trap
;       9E:       05               BYTE #X05
;       9F:       1F               BYTE #X1F                  ; OBJECT-NOT-TYPE-ERROR
;       A0:       FE9001           BYTE #XFE, #X90, #X01      ; ESI
;       A3:       10               BYTE #X10                  ; EAX
;       A4:       CC0A             BREAK 10                   ; error trap
;       A6:       02               BYTE #X02
;       A7:       18               BYTE #X18                  ; INVALID-ARG-COUNT-ERROR
;       A8:       4F               BYTE #X4F                  ; ECX


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; 引数の型チェックがいらない(外したい)場合
(defun 2* (x)
  (declare (optimize (safety 0)))  ; safetyを0に
  (plus x x))

(2* 10)  ; 適切な引数
--> 20

(2* 'a)  ; 不正な引数
--> #<unknown immediate object, lowtag=#b110, widetag=#xE {1635750E}>  ; よく分からないけど、不正な値が返ってきた

(disassemble #'2*)
;; 引数を設定した後、plus-impl関数にジャンプしているだけ(?)
; disassembly for 2*
; 0B5BC24C:       8BD6             MOV EDX, ESI               ; no-arg-parsing entry point
;       4E:       8BFE             MOV EDI, ESI
;       50:       8B0520C25B0B     MOV EAX, [#xB5BC220]       ; #<FDEFINITION object for PLUS-IMPL>
;       56:       B908000000       MOV ECX, 8
;       5B:       FF7504           PUSH DWORD PTR [EBP+4]
;       5E:       FF6005           JMP DWORD PTR [EAX+5]

使用者側は、呼び出す関数を変える必要もなく、最適化宣言を状況に応じて変更するだけで良いので、少し便利。
まあ、型チェックのコストくらい気にせず、(パッケージ外にエクスポートする関数)全部につけてしまっても良いような気もするけど。

*1:ちなみに、plus関数の定義から引数の型宣言を外すと、受け取った引数をそのままplus-impl関数に渡すことしかしないので、plus-impl関数をそのまま使うのと同じ問題が起こる

*2:その点、HaskellOCamlなどでは、コンパイル時の型チェックによって、安全性と実行速度を両立させることができる(型チェックのために実行速度を犠牲にせずに済む)ので、羨ましい。