【Common Lisp】
型指定子の基本の使い方
(type, ftype)

  • Common Lispの型指定子はアトム型と複合型の2種類に分かれ、複合型はリスト形式でパラメータを渡す
  • 関数型は ftype を使って関数の引数と戻り値を一括宣言する
  • REPLでは type-oftypepsubtypep で型の確認と包含関係の検証ができる
;; vecの範囲[a, b) で xs に含まれるものの合計を計算する
(declaim
  (ftype (function 
      ((simple-array fixnum (*)) fixnum fixnum list)
                   fixnum)
         sum-selected-range-in))
(defun sum-selected-range-in (vec a b xs)
  (declare
    (type (simple-array fixnum (*)) vec))
    (type fixnum a b)
    (type list xs)
  (loop for i from a below b
        for x = (aref vec i)
        when (member x xs)
          sum x))Code language: PHP (php)

関連記事

1. 型指定子の基本構造(declare type)

型指定子には、シンボルひとつで書くアトム型と、リスト形式で書く複合型があります。

型指定子の基本構造(type) アトム型 シンボル1つで書く (declare (type fixnum n)) (declare (type boolean f)) (declare (type string s)) 複合型 リスト形式でパラメータを渡す (integer 0 100) (simple-array fixnum (*)) * = 任意(省略) (integer 0 *) → 0以上上限なし type の直後が括弧で始まるかどうかで区別できる (declare (type fixnum n)) vs (declare (type (integer 0 100) n))
;; アトム型:シンボルだけ
(declare (type fixnum n))
(declare (type boolean flag))
(declare (type string s))

;; 複合型:リスト形式
(declare (type (integer 0 100) n))
(declare (type (simple-array fixnum (*)) v))Code language: Lisp (lisp)

type の直後が括弧で始まるかどうかで区別できます。

1.1. * による範囲の省略

範囲や次元を省略するときは * を書きます。
「任意」という意味です。

(integer 0 *)     ; 0以上、上限なし
(integer * 100)   ; 100以下、下限なし
(integer * *)     ; integer と同義Code language: Lisp (lisp)

配列の次元でも同様です。

(simple-array fixnum (*))     ; 長さ任意の1次元配列
(simple-array fixnum (* *))   ; サイズ任意の2次元配列
(simple-array fixnum (10 *))  ; 行数10、列数任意Code language: Lisp (lisp)

2. REPLでの型確認

REPLでの型確認 type-of 値の具体的な型を返す (type-of 42) → FIXNUM (type-of “hi”) → (SIMPLE-ARRAY…) (type-of nil) → NULL 実装依存に注意 typep 特定の型に属するか確認 (typep 42 ‘fixnum) → T (typep 42 ‘integer) → T (typep nil ‘list) → T スーパータイプもT subtypep 包含関係を2値で返す (判定値, 確定フラグ) (subtypep ‘fixnum ‘integer) → T, T (subtypep ‘null ‘list) → T, T 2値目NIL=判定不能 declare の型が意図通りか subtypep で事前チェックできる

2.1. 値の型を調べる(type-of, typep)

type-of は値の具体的な型を返します。

(type-of 42)        ;=> FIXNUM
(type-of 3.14d0)    ;=> DOUBLE-FLOAT
(type-of "hello")   ;=> (SIMPLE-ARRAY CHARACTER (5))
(type-of '(1 2 3))  ;=> CONS
(type-of nil)       ;=> NULLCode language: Lisp (lisp)

type-of の返す型は実装依存で、最も具体的な型を返すとは限りません。
特定の型に属するかを確認するには typep を使います。

(typep 42 'fixnum)        ;=> T
(typep 42 'integer)       ;=> T
(typep 42 'double-float)  ;=> NIL
(typep nil 'null)         ;=> T
(typep nil 'list)         ;=> TCode language: Lisp (lisp)

2.2. 型の包含関係を調べる(subtypep)

subtypep は「型Aは型Bのサブタイプか」を2値で返します。
1つ目が真偽、2つ目が「処理系が確定できたか」です。

(subtypep 'fixnum 'integer)         ;=> T, T
(subtypep 'null 'list)              ;=> T, T
(subtypep 'string 'sequence)        ;=> T, T
(subtypep '(integer 0 10) 'fixnum)  ;=> T, T
(subtypep 'double-float 'fixnum)    ;=> NIL, TCode language: Lisp (lisp)

2つ目が NIL のときは、処理系が判断できなかったことを意味します。
deftype で定義した型や複雑な and 条件では稀に起きます。

declare で書いた型が意図通りかを確認するとき、subtypep で「宣言した型が fixnum に収まるか」を事前チェックする使い方が実用的です。

3. アトム型(declare type)

3.1. 数値の型(fixnum, integer, real)

(declare (type fixnum n))                    ; プラットフォームネイティブの整数
(declare (type integer n))                   ; 任意精度整数(bignum含む)
(declare (type (integer 0 100) n))           ; 範囲付き整数
(declare (type (integer 0 *) n))             ; 0以上、上限なし
(declare (type double-float x))              ; 倍精度浮動小数点
(declare (type single-float x))              ; 単精度浮動小数点
(declare (type float x))                     ; 精度問わず浮動小数点
(declare (type (double-float 0.0d0 *) x))   ; 0.0以上のdouble-float
(declare (type real x))                      ; 整数・浮動小数点どちらもCode language: Lisp (lisp)

パフォーマンス目的では fixnum が最も効果的です。
64bit環境のSBCLでは -2^62 から 2^62-1 の範囲で、ループカウンタや配列インデックスに宣言するだけで速度が変わります。

3.2. 文字・文字列の型(string)

(declare (type character c))      ; 1文字
(declare (type string s))         ; 文字列
(declare (type simple-string s))  ; adjustableでない文字列Code language: Lisp (lisp)

simple-stringsimple-array と同様に、文字アクセスの最適化が効きやすくなります。

4. 複合型

複合型 配列型 (simple-array fixnum (*)) 1次元・長さ任意 (simple-array fixnum (* *)) 2次元 (simple-array fixnum (10 *)) 行数固定・列数任意 aref アクセスが高速化 リスト・コンス型 (declare (type list xs)) nil を含む (declare (type cons xs)) 非空のコンスセル (declare (type null x)) nil のみ list = (or cons null) or / and / not or:どちらか (or fixnum null) and:両方満たす (and fixnum (integer 0 *)) not:その型以外 (not null)

4.1. 配列の型(simple-array)

;; 1次元、長さ任意
(declare (type (simple-array fixnum (*)) v))

;; 1次元、長さ固定
(declare (type (simple-array fixnum (500001)) v))

;; 2次元
(declare (type (simple-array fixnum (* *)) matrix))

;; 要素型不問
(declare (type (simple-array * (*)) v))Code language: Lisp (lisp)

simple-arrayadjustablefill-pointer がない単純な配列です。
aref によるアクセスが速くなります。

4.2. リスト・コンセルの型(list, cons)

(declare (type list xs))   ; リスト(nil含む)
(declare (type cons xs))   ; 非空のコンスセル
(declare (type null x))    ; nil のみCode language: Lisp (lisp)

list(or cons null) と同義で、空リストを含みます。
空でないことが確定しているなら cons の方がコンパイラへの情報量が増えます。

4.3. 型の組み合わせ(or・and・not)

orandnot で型を組み合わせられます。

;; or:いずれかの型
(declare (type (or fixnum null) result))     ; fixnum か nil
(declare (type (or fixnum double-float) x))  ; 整数か浮動小数点

;; and:すべての条件を満たす
(declare (type (and fixnum (integer 0 *)) n))  ; 0以上のfixnum

;; not:その型以外
(declare (type (not null) x))  ; nil以外Code language: Lisp (lisp)

or null の形は、見つからなかった場合に nil を返す関数の戻り値によく使います。
ftype の戻り値にも書けます。

(declaim (ftype (function (fixnum) (or null fixnum)) find-index))Code language: Lisp (lisp)

4.4. 構造体・クラスの型(defstruct, defclass)

defstructdefclass で定義した型名はそのまま使えます。

(defstruct point x y)

(defun distance (p)
  (declare (type point p))
  (sqrt (+ (* (point-x p) (point-x p))
           (* (point-y p) (point-y p)))))Code language: Lisp (lisp)

パフォーマンス最適化よりも、コンパイル時の型チェックと可読性のために使うことが多いです。

5. 型エイリアス(deftype)

よく使う型の組み合わせには deftype で名前をつけられます。

(deftype index () '(integer 0 #.array-dimension-limit))
(deftype probability () '(double-float 0.0d0 1.0d0))
(deftype nullable-fixnum () '(or fixnum null))Code language: Lisp (lisp)

#. はコンパイル時に評価されます。
定義した型名はそのまま declare に使えます。

(declare (type index i))
(declare (type probability p))Code language: Lisp (lisp)

競技プログラミングではファイル冒頭に deftype をまとめておくと、宣言の意図が読みやすくなります。

6. 関数全体の型宣言(ftype)

ftype は関数の引数と戻り値をまとめて宣言します。
関数定義の前に declaim で書きます。

(declaim (ftype 
   (function (引数の型...) 戻り値の型) 
   関数名))Code language: Lisp (lisp)
ftype:関数全体の型宣言 (declaim (ftype (function (引数の型…) 戻り値の型) 関数名)) 関数定義の前に書く 基本パターン (function (fixnum fixnum) fixnum) 引数2つ→fixnum を返す 多値を返す (function (fixnum fixnum) (values fixnum fixnum)) 戻り値を values で列挙 nil を返す可能性 (function (fixnum) (or null fixnum)) 見つからない場合に nil 高階関数 (function ((function (fixnum) fixnum) fixnum) 引数の型も function で書く declare type との使い分け declare → 内部関数・短い関数 ftype → 外部公開・引数と戻り値を ドキュメントとして残したい場合

基本パターンは、

(declaim (ftype 
  (function (fixnum fixnum) fixnum) add))
(defun add (a b)
  (+ a b))Code language: Lisp (lisp)

6.1. 多値を返す関数(values)

values で複数の値を返す場合は、戻り値を (values ...) で書きます。

(declaim (ftype (function (fixnum fixnum) (values fixnum fixnum)) divide-and-rem))
(defun divide-and-rem (a b)
  (values (floor a b) (mod a b)))Code language: Lisp (lisp)

副作用だけを目的にした関数で戻り値がない場合は (values) と書きます。

(declaim (ftype (function (fixnum) (values)) print-n))Code language: Lisp (lisp)

6.2. オプション引数・rest引数(&optional, &rest)

;; &optional
(declaim (ftype (function (fixnum &optional fixnum) fixnum) clamp-or-abs))

;; &rest(残りの引数の型をひとつ書く)
(declaim (ftype (function (fixnum &rest fixnum) fixnum) sum-all))Code language: Lisp (lisp)

6.3. 高階関数

関数を引数に取る場合は、その引数の型を (function ...) で書きます。

(declaim (ftype (function ((function (fixnum) fixnum) fixnum) fixnum) apply-twice))
(defun apply-twice (f n)
  (funcall f (funcall f n)))Code language: Lisp (lisp)

(function (fixnum) fixnum) は「fixnum を受け取り fixnum を返す関数」という型です。

6.4. declare type と declaim ftype の使い分け

関数が短く、ファイル内でしか使わないなら declare (type ...) で十分です。
ftype が有効なのは、他ファイルから呼ばれる関数、let+defun で定義している関数、引数と戻り値をまとめてドキュメントとして残したい場合です。

;; 内部関数なら declare で十分
(defun add (a b)
  (declare (type fixnum a b))
  (+ a b))

;; 外部公開・let+defun 構造なら ftype
(declaim (ftype (function (fixnum fixnum) fixnum) add))Code language: Lisp (lisp)