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