ftype型宣言(sbcl) : 戻り値の型指定

前回と重複するところがあるが、ftype型宣言の有用な使い方について書いておく。

関数の戻り値の型指定

ftype型宣言を使うと、関数の戻り値の型を指定することができる。
これが何故有用かと云うと、sbclは関数の戻り値の型が指定されている場合、それに基づいて型推論を行ってくれるので、(前回の例でも挙げたように)呼び出し側での型宣言が不要となる。


以下、例。
まずは、ftype型宣言がない場合。

;; バイナリファイル読み込み関数
;; 戻り値の型指定無し
(defun read-binary-file (path)
  (with-open-file (in path :element-type '(unsigned-byte 8))
    (let ((as (make-array (file-length in) :element-type '(unsigned-byte 8))))
      (read-sequence as in)
      as)))

;; read-binary-fileの呼び出し側
(defun fn (path)
  (declare (optimize (speed 3))
           (sb-ext:unmuffle-conditions sb-ext:compiler-note))
  (loop FOR c ACROSS (read-binary-file path) COLLECT c))
;;;;
;; ループ対象の配列の型(ランク)が不明なため、最適化が行えないと云われる
;
; in: LAMBDA NIL
;     (LOOP FOR C ACROSS (READ-BINARY-FIL PATH)
;           COLLECT C)
; --> BLOCK LET SB-LOOP::WITH-LOOP-LIST-COLLECTION-HEAD LET* SB-LOOP::LOOP-BODY 
; --> TAGBODY SB-LOOP::LOOP-REALLY-DESETQ SETQ THE AREF 
; ==>
;   (SB-KERNEL:HAIRY-DATA-VECTOR-REF/CHECK-BOUNDS ARRAY SB-INT:INDEX)
; 
; note: unable to
;   optimize
; because:
;   Upgraded element type of array is not known at compile time.
; 
; compilation unit finished
;   printed 1 note
--> FN

;; 型を明示する
(defun fn (path)
  (declare (optimize (speed 3))
           (sb-ext:unmuffle-conditions sb-ext:compiler-note))
  (loop FOR c ACROSS (the (simple-array (unsigned-byte 8)) 
                          (read-binary-file path)) COLLECT c))
--> FN  ; 警告はでない


次が、ftype型宣言をつけた場合。

;; 分かりやすさのために、型の別名を定義する
(deftype simple-octets () '(simple-array (unsigned-byte 8)))

;; バイナリファイル読み込み関数
;; simple-octets型の値を返すことを宣言する
(declaim (ftype (function (t) simple-octets) read-binary-file))  ; XXX: 関数の引数の型は、本当は(or stream string pathname)とかが適切
(defun read-binary-file (path)
  (with-open-file (in path :element-type '(unsigned-byte 8))
    (let ((as (make-array (file-length in) :element-type '(unsigned-byte 8))))
      (read-sequence as in)
      as)))

;; read-binary-fileの呼び出し側
(defun fn (path)
  (declare (optimize (speed 3))
           (sb-ext:unmuffle-conditions sb-ext:compiler-note))
  (loop FOR c ACROSS (read-binary-fil path) COLLECT c))
--> FN  ; 呼び出し側で、型を明示しなくても警告はでない


また、ftypeを使って関数の型を宣言しておいた場合、呼び出し側の関数で特にoptimize宣言などを行なっていない場合でも、コンパイラが勝手に型を推論して最適化を行ってくれる。
※ ただし、ftypeを使わなくても、コンパイラが(関数の定義から)戻り値の型を推論してくれる場合が結構ある*1

;; まだread-binary-fileの型宣言は行っていない
> (defun fn (path)
    (loop FOR c ACROSS (read-binary-fil path) COLLECT c))
--> FN  ; (optimize ...)及び(unmuffle ...)宣言をつけていないので、警告はでない。最適化も行わない。

;; bashコマンドの読み込み
> (time (progn (fn "/bin/bash") 'done))
Evaluation took:
  0.023 seconds of real time  ; 0.023s
  0.024002 seconds of total run time (0.024002 user, 0.000000 system)
  104.35% CPU
  63,412,396 processor cycles
  6,321,280 bytes consed
--> DONE

;;
;; 型宣言を付与
> (declaim (ftype (function (t) simple-octets) read-binary-file))

;; 中身は変えずに再定義
> (defun fn (path)
    (loop FOR c ACROSS (read-binary-fil path) COLLECT c))
--> FN

;; bashコマンドの読み込み
> (time (progn (fn "/bin/bash") 'done))
Evaluation took:
  0.015 seconds of real time  ; 0.015s  ->  1.5倍くらい速くなる
  0.016001 seconds of total run time (0.016001 user, 0.000000 system)
  106.67% CPU
  41,930,544 processor cycles
  6,321,528 bytes consed
--> DONE

このため、ユーティリティ関数などで戻り値の型があらかじめ決まっている場合は、ftypeを使って型を宣言しておくのが良いのではないかと思う*2 *3
そうすれば、使用側は特に意識せずとも、型情報がついた関数を呼び出すだけで可能な最適化をコンパイラが行ってくれるし、明示的な最適化を行いたい場合も、自分で書く宣言の量を減らすことができる。

*1:特定の関数に十分な型情報がついているかどうかは、describe関数を使って確認できる

*2:実際には、上でも書いたようにftypeを使わなくても、既に十分な型情報がコンパイラにより付与されている場合がある(むしろそっちの方が多い。本文では(declaim (ftype ...) )の必要性を強く主張しすぎ )ので、一度試してみて必要なものにだけ宣言を行えば良いと思う。

*3:関数を適切に定義しておけば、呼び出し側でその型情報が暗黙の内に利用される、ということの方が重要。(declaim (ftype ...) )は、そのための方法の一つ。