独学 Common Lisp

Chapter 4. 型とクラス

概要

ANSI Common Lispの第4章のうち、型に関するオペレータ型の定義様々な型指定子クラスの概要クラスの定義クラスの継承について説明します。オブジェクト指向(CLOS)については主に第7章「オブジェクト」にて、構造体については第8章「構造体」にて、コンディションについては第9章「コンディション」にて個別の説明がありますので、本章では型に特化して説明します。

  1. 型に関するオペレータ
    1. type-of関数
    2. typep関数
    3. subtypep関数
    4. check-typeマクロ
    5. coerce関数
    6. typeとthe
  2. 型の定義: deftype
  3. 様々な型指定子
    1. 数に関する型
    2. 配列に関する型
    3. 型の定義に関する型
  4. クラスの概要
  5. クラスの定義
  6. クラスの継承

型に関するオペレータ

Common Lispは動的型付けの言語なので初歩的なプログラムを書く際には「型(type)」を意識することなく素早くプロトタイプを作成することができますが、より良いプログラムを書くには必ず型を意識しなければなりません。型を意識することで安全性・効率性・高速性が高まりますし、Common Lisp Object Systemは型システムを前提にした総称関数を基礎に成立しています。

このページでは型に関する説明をしていきますが、抽象的なことを説明する前に、型に関する関数やマクロなどを説明します。

型を調べる: type-of関数

型を調べる基本的な関数の一つがtype-ofです。いくつかの値の型を調べてみます。

(type-of 123)
; => (INTEGER 0 281474976710655)

(type-of 3.14)
; => SINGLE-FLOAT

(type-of 1/3)
; => RATIO

(type-of 1.0d0)
; => DOUBLE-FLOAT

(type-of #\a)
; => STANDARD-CHAR

(type-of #\あ)
; => BASE-CHAR

(type-of 'a)
; => SYMBOL

(type-of :a)
; => KEYWORD

(type-of "Hello")
; => (SIMPLE-BASE-STRING 5)

(type-of '(1 2 3))
; => CONS

(type-of #(1 2 3))
; => (SIMPLE-VECTOR 3)

(type-of #'+)
; => COMPILED-FUNCTION

(type-of #'(lambda (x y) (* x y)))
; => FUNCTION

型を判定する: typep関数

値と型を指定すると適合しているかどうかを判定する関数がtypepです。以下の例のように型の相互関係は包含関係になっており、より特定的な型に合致する場合はより広い型にも合致します。

(typep 1.0d0 'number)
; => T

(typep 1.0d0 'real)
; => T

(typep 1.0d0 'float)
; => T

(typep 1.0d0 'ratio)
; => NIL

(typep #(1 2 3) 'array)
; => T

(typep #(1 2 3) 'vector)
; => T

(typep #(1 2 3) 'simple-array)
; => T

(typep #(1 2 3) 'simple-vector)
; => T

(typep #(1 2 3) '(simple-vector 3))
; => T

(typep #(1 2 3) '(vector fixnum 3))
; => T

型の包含関係を確認する: subtypep関数

型同士の包含関係を確認したければsubtypepを利用するのがシンプルです。これは二つの型を引数に取り、第1引数の型が第2引数の型のサプタイプであるかどうかを確認します。返り値は多値となっており、以下のようにパターン化されています。

1st value 2nd value 意味
T T 第1引数は第2引数のサブタイプである
NIL T 第1引数は第2引数のサブタイプではない
NIL NIL 判定不能

以下に例を示します。

(subtypep 'vector 'array)
; => T; T

(subtypep 'list 'sequence)
; => T; T

(subtypep 'fixnum 'integer)
; => T; T

(subtypep 'single-float 'double-float)
; => NIL; T

型の確認を行い、不適合であればエラーを通知する: check-typeマクロ

型の安全性(確実性)を高めたい場合、check-typeマクロを使用することができます。これは変数に束縛された値が指定の型に合致しない場合、TYPE-ERRORというエラーを通知します。エラーが通知されるとデバッガが起動します。

(defvar *year* '(2015 2016 2017))
; => *YEAR*

(check-type *year* (vector fixnum 3))
; The value of *YEAR* should be of type (VECTOR FIXNUM 3).
; The value is: #1=(2015 2016 2017)
;    [Condition of type SIMPLE-TYPE-ERROR]

マクロは引数の評価を抑制できるため、マクロの場合は型の指定で'(quote)が必要ありません。(vector fixnum 3)と書いているとvector関数のように見えますが、これは型指定子(Type Specifier)としてのvectorです。

型の変換を行う: coerce関数

型の変換を行いたい場合、coerce関数の第1引数に値を指定し、第2引数に目的の型を指定します。型変換はそれほど多く使うわけではありませんが、以下のような場合には積極的に使われます。

以下で例を示します。2つ目は全角スペースを文字として指定したい場合に、文字列から文字へと変換して文字定数を調べています。データに全角スペースが含まれると不都合が多いので、私はこの文字定数を使ってsubstitute関数で半角スペース(#\Space)に置換することがあります(1)。

(coerce '(1 2 3) 'vector)
; => #(1 2 3)

(coerce " " 'character)
; => #\IDEOGRAPHIC_SPACE

(/ 5 2)
; => 5/2

(coerce (/ 5 2) 'double-float)
; => 2.5d0

型を宣言する: typethe

前章(Chapter 3. 評価とコンパイル)でも説明しましたが、引数の型の宣言はdeclareシンボルにおけるtypeの指定を使って行い、返り値の指定はtheスペシャルオペレータを使用します。詳しくはChapter 3を確認してください。

型を定義する: deftypeマクロ

ANSI Common Lispでは様々な型指定子(Type Specifiers)が定められていますが、自分で型を定義したい場合もあります。そのような時はdeftypeマクロを使用します。

deftypeはこれ自体がマクロですが、その定義の仕方もマクロによく似ています。deftypeで定義した型はtypep関数やsubtypep関数で使うことができますが、これらの関数で使用する型指定子をイメージしながら型指定子自体をそのまま(quoteして)定義します。ここでは偶数の型evenを定義していますが、そこでは偶数かどうかを判定するevenp関数を使用し、satisfies型指定子でevenpを満たすことを型の条件にしています。また、偶数は整数が対象なので、integerも型の条件に含め、and型指定子で定義しています(2)。

(deftype even ()
  '(and integer
        (satisfies even-p))) 
; => EVEN

(typep 4 'even)
; => T

(typep 3 'even)
; => NIL

(typep 2.0 'even)
; => NIL

(subtypep 'even 'integer)
; => T; T

様々な型指定子(Type Specifiers)

この節では実際に型をプログラムの中で使うことを意識して、様々な型指定子(Type Specifiers)を紹介します。なお、ここで紹介するのはANSI Common Lispの第4章2節3 "Type Specifiers" に掲載されているものを独自に分類したものです。

数に関する型

実際のプログラムで型を指定するのが最も多いのは数だと思います。数はnumberという型を頂点に様々な型が定義されていますが、頻繁に利用するのはfixnum(固定長整数)とdouble-float(倍精度浮動小数点数)だと思います。また、バイナリデータを扱う場合は(unsigned-byte 8)も必須です。

number
全ての数です。
real
実数です。(real lower upper)で範囲を指定できます。
complex
虚数です。実部と虚部の型を指定することもできます。
rational
有理数です。範囲を指定できます。ただ、一般には整数・浮動小数点数・分数などのより細かい分類を用います。
ratio
分数です。整数は含みません。
integer
整数です。通常は固定長整数のfixnumを使います。
fixnum
固定長整数です。
signed-byte
符号付き2進数です。unsigned-byteはよく使いますが、こちらはあまり使いません。
unsgined-byte
符号なし2進数です。通常は8ビットを指定して(unsgined-byte 8)(1バイト)で使います。1バイトが8ビットではないマシンは現代ではあまりないと思いますので、8まで含めて覚えた方が便利です。
bit
0または1です。(integer 0 1)及び(unsigned-byte 1)と等価です。
float
浮動小数点数です。
double-float
倍精度浮動小数点数です。
short-float, single-float, long-float
それぞれ固定長浮動小数点数ですが、計算ではdouble-floatを使うことが多いので、あまり使いません。

なお、型指定子の話題ではありませんが、ファイルなどから小数点数を読み込む際、全てdouble-floatで読み込みたい場合は以下のように*read-default-float-format*をセットします。

(setq *read-default-float-format* 'double-float)

これを指定しないと通常はsingle-floatで読み込まれます。

配列に関する型

数の次によく使う型が配列に関する型です。配列は型を指定することでメモリの番地が事前に計算できるため、ランダムアクセスがより高速になります。

array
全ての配列の型です。(array type dimension)で要素の型と次元を指定できます。型の指定がない場合はtが使われます。また、例えば2次元配列(行列)の場合の次元指定は(row col)のようにリストで表すこともできますし、単に2というように次元数(Rank)だけを指定することもできます。要素数が不定の場合は(*)を使うこともできます。
simple-array
要素数の変更不可かつフィルポインタ無しの配列に用いる型です。arrayと同様に要素の型と次元を指定できます。
vector
1次元限定の配列、すなわちベクトルに用いる型です。arrayと同様に要素の型と要素数を指定できます。arrayと異なり「次元」という概念はないので、(vector t *)(array t (*))と等価です。また、要素の型にbitを指定するとbit-vectorという個別の型に等しくなります。さらに要素の型に文字型(characterのサブタイプ)を指定すると文字列型(string)になります。
simple-vector
要素数変更不可かつフィルポインタかつ要素の型を指定しないベクトルに用いる型です。上記3つと大きく異なるのは「要素の型を指定しない」点であり、型は常にtで固定です。ベクトルを表すリーダーマクロ(#()で作成された定数ベクトルはこの型になります。また、この型のベクトルに特化した高速アクセス関数svrefを用いることもできます。要素の型を指定する場合はsimple-arrayを使用してください。
simple-bit-vector, simple-string, simple-base-string
要素の型を指定できないsimple-vectorを補うための個別の型です。例えば文字列定数"abc"の型は(simple-base-string 3)のようになります。

型の定義(deftype)で用いる型

型指定子の最後の節として、値の型に用いるのではなく、deftypeによる型の定義に用いる型指定子を紹介します。型に関する関数やマクロはこのページの最初に紹介しましたが、この節で紹介するのはそれ自体が型指定子であるものです。

and
型指定子自体を「かつ」の条件で結びます。指定する型指定子は複数でも構いません。
or
型指定子自体を「または」の条件で結びます。指定する型指定子は複数でも構いません。
not
次に続く1つの型指定子を「ではない」のように否定します。
eql
次に続く1つのオブジェクトに「等しい」ことを示します。
member
次に続く複数のオブジェクトを「または」で結びます。「列挙型」です。
satisfies
次に続く1つの述語関数(3)を満たすことを型の条件にします。
values
関数の型指定における返り値の型に用います。

deftypeの節でsatisfiesの例を示したので、ここでは追加の使用例としてmemberを使います。例えば三択のクイズを行うプログラムを書いている場合、回答の選択肢は'aまたは'bまたは'cのいずれかですから、そのような型は以下のように指定できます。

(deftype three-answer ()
  '(member a b c))
; => THREE-ANSWER

(typep 'a 'three-answer)
; => T

クラスの概要

前節までに型(Type)を説明しましたが、一歩進んでクラス(Class)について説明します。

deftypeによる型の定義はとても柔軟ですが、型の本質には届いていません。そもそも数や文字列、リストやベクトルなどの「型」は多種多様なデータを分類するためのものです。データとは究極的にはメモリ上に構築されているビット(0と1)の羅列に過ぎませんが、抽象的なプログラミング言語はそのデータの意味を明確に分類し、扱いやすくしています。例えば数であれば計算に用いるでしょうし、文字列なら出力に使うかもしれません。個別的には全く異なるビット列のデータを同じ「型」として扱うというのが型の持つ重要な役割です。

もちろんCommon Lispは関数に型を与えることもできますが、関数の型も実質的には引数と返り値というデータの型を指定することになります。つまり型はデータ構造と密接に結びついているのです。しかし、deftypeによる型の定義はそれ自体にデータを持つことはできず、型指定子を使って既存の型を組み合わせた「集合」として新しい型を定義しているのです。よって、型の本質を考えると、それ自体にデータ構造を内包しているような型の定義ができるような手段を用意すべきです。それが「クラス」です。

一般のオブジェクト指向では、クラスとはデータと動作(メソッド)を合わせて保持できるオブジェクトの設計図として説明されますが、Common Lispのクラスは純粋にデータ構造として構築されており、クラスに関する動作(メソッド)はクラスとは別のところで定義します。そのため、一般のオブジェクト指向を学んだことのある人も一旦忘れて、クラスを「データ構造と独自の型を同時に定義できるもの」として捉えましょう。

クラスはCommon Lispの型システムと密接に結びついた言語の根幹を成すものです。前節までに見てきた型も、実際はクラスとして定義されている場合があります。例えば、整数integerなどは型でもありますが、クラスでもあります。

(type-of 10)
; => (INTEGER 0 281474976710655)

(class-of 10)
; => #<BUILT-IN-CLASS INTEGER>

class-of関数はオブジェクトのクラスを調べます。10という値は間違いなく「数」ですが、#<BUILT-IN-CLASS INTEGER>というクラスの情報も持っています(これはつまり、Common Lispにおいては「全てはオブジェクトである」ということです)。このintegerがクラスで、built-in-classというのは「メタクラス」と呼ばれます。integerクラスはユーザーが定義したものではなく、Common Lispですでに定義されているクラスなのでこのメタクラスに属します。

クラスは型と同様に包含関係になっています。

(class-of (class-of 10))
; => #<STANDARD-CLASS BUILT-IN-CLASS>

(class-of 10)のクラスを調べると、きちんと答えが帰ってきます。Common Lispでは、クラスも別のクラスのオブジェクトであることに注意してください。「クラスが設計図で、オブジェクトが実体」という一般的な説明には適合しません。クラスもオブジェクトであり、built-in-classstandard-classに含まれています。この関係では、standard-classがメタクラスになります。

 (class-of (class-of (class-of 10)))
 ; => #<STANDARD-CLASS STANDARD-CLASS>

standard-classオブジェクトのクラスはstandard-class自身です。実はこのstandard-classは全てのクラスの頂点にある最も抽象的なクラスです(4)。class-ofをもう一つ増やして4つにしても上記と同じになります。

standard-classbuilt-in-classなどのメタクラスにはもう一つだけ種類があり、構造体を定義するdefstructで作られたオブジェクトはstructure-classというメタクラスに属することになります。

standard-class
クラス定義の基礎となるメタクラス。
built-in-class
Common Lispにあらかじめ備わっているクラスを示すメタクラス。
structure-class
構造体の基礎となるメタクラス。

クラスの定義: defclass

Common Lispのクラスは前節のようなシンプルな階層構造になっており、元をたどればstandard-classに行き着くようにできています。そのため、自分でクラスを定義する場合も、必ずstandard-classに行き着くように定義されます。

クラスの定義はdefclassマクロで行います。前節で説明した通り、Common Lispのクラスはデータ構造と型の定義を同時に行うものであり、それ以上でもそれ以下でもありません。例えば、非常に簡単なクラスとして、システム上のファイルを表すようなクラスを作ってみます。

(defclass file ()
  ((name :initarg :name
         :reader fname)
   (size :initarg :size
         :reader fsize)))
; => #<STANDARD-CLASS FILE>

クラスはデータ構造を持つと述べましたが、fileクラスではnamesizeという2つのデータを持つように定義しています。クラスで定義されるデータ構造は「スロット」と呼ばれます。

スロットには様々なオプションを付けることができます。

:initargオプション
クラスからオブジェクト(インスタンス)を生成する際に初期値を設定する場合が多いと思いますが、初期値設定値用のキーワード引数を指定します。
:initformオプション
初期値が設定されなかった場合のデフォルト値を指定します。
:accessorオプション
スロットにアクセスするための関数名を指定します。関数はマクロで自動生成されます。
:reader, :writerオプション
読み込み専用または書き込み専用にしたいスロットのアクセス関数名を指定します。関数はマクロで自動生成されます。
:typeオプション
スロットのデータの型をあらかじめ指定します。
:allocationオプション
(slot-name :allocation :class)のように:classキーワードを指定すると、スロットがクラス共有になります。デフォルト値は:instanceで、こちらはスロットがインスタンス(オブジェクト)毎に確保されます。同一クラスから生成された全てのインスタンスで同じ情報を共有したい場合に使います。
:documentationオプション
スロットのドキュメントを指定します。

defclassマクロはアクセス用の関数を自動的に生成しますが、クラスの型も自動的に追加されます。先ほどのfileクラスを実際に使ってみます。クラスからインスタンスを生成するにはmake-instance関数を使います。

(defparameter file1 (make-instance 'file
                                   :name "test.txt"
                                   :size "10KB"))
; => FILE1

(type-of file1)
; => FILE

(fname file1)
; => "test.txt"

(fsize file1)
; => "10KB"

アクセス関数を使うのが便利なのであまり実際に使うことはないかもしれませんが、スロットへのアクセスはslot-value関数によって直接行うこともできます。

(slot-value file1 'name)
; => "test.txt"

また、slot-value関数はsetfマクロと組み合わせて、スロットへの値の書き込みに使用することもできます。

(setf (slot-value file1 'name) "test2.txt")
; => "test2.txt"

Common Lispでは:readerオプションを付けてスロットを定義しても厳密に読み込み専用になる訳ではありません。外部からのアクセスをどのようにコントロールするかはクラスとは別にパッケージの仕組みで設計されています。もちろん、:readerで指定したアクセス関数で書き込みを行うことはできません。

(setf (fname file1) "test.txt")
; FUNCTION: undefined function #1=(SETF FNAME)
;   [Condition of type SYSTEM::SIMPLE-UNDEFINED-FUNCTION]

よって、slot-value関数は極力使わず、:reader, :writer, :accessorの各アクセス関数でアクセスする方が望ましいと言えます。もちろん、外部ではなく内部(同一パッケージ)で使う場合などはプログラマの裁量次第です。

Common Lispのクラスはこのようにデータ構造と独自の型を併せ持つシンプルな設計になっていますが、次節で説明する「継承」を用いることで梃子の力を利用するようにデータ構造を拡張することができます。また、独自の型が素早く定義できるため、型に合わせた振る舞いをする関数(メソッド)を定義することもできるようになります。さらに、Common Lispのクラスは実行中でも定義を変更できたり、すでにインスタンスが生成されていても変更が可能だったり、真に動的なものとして設計されていますが、最も基本的な部分はデータ構造と独自の型のセットと理解しておけばANSI Common Lispの他の部分を理解するのに十分です。

クラスの継承(Inheritance)

前節ではクラスの定義について説明しましたが、クラスが真に力を発揮するのは「継承」が可能であるという点です。

継承とは、すでに定義済みのクラスのスロットと型をそのまま受け継いで、より特定的なサブクラスを定義するという手法です。プログラミングにおいて「関数」は同じような手続きを一括りにして様々な箇所で再利用できるようにするための抽象化の技法ですが、「継承」はデータ構造と型における抽象化の技法です。

例えば、前節で定義したfileクラスを用いて、photoクラスを定義することができます。

(defclass photo (file)
  ((datetime :initarg :datetime
             :accessor photo-datetime)))
; => #<STANDARD-CLASS PHOTO>

photoクラスはfileクラスが持つnamesizeというスロットに加えてdatetimeというスロットを持っています。これは写真の撮影日時を表すのに使えますが、ファイル一般に利用する訳ではないため、fileクラスには定義せず、photoクラスにのみ定義しています。

このような抽象化の技法を取り入れることで、例えばmusicクラスを定義したい時もfileクラスを再利用することができる(おそらくartisttitleなどのスロットを追加で定義するでしょう)でしょうし、ユーザーが自分で新しいfileクラスのサブクラスを定義することもできます。継承は拡張性を高める強力な技法です。

継承はすでに述べた通り、データ構造だけでなく型も引き継ぎます。この例ではphotoクラスはfileクラスの「サブタイプ」として自動的に準備されます。

(subtypep 'photo 'file)
; => T; T

ANSI Common Lispの標準仕様において、型を定義する手法はこのページで紹介したdeftypedefclass以外にも2種類用意されており、defstructdefine-conditionがあります。defstructstructure-classを基礎とする構造体を定義するもので、クラスよりも簡素で高速なデータ構造と型を提供します。構造体もデータ構造を継承する機能はありますが、型を継承する機能はありません。また、define-conditionは「コンディション」と呼ばれる例外処理システム用のものなので、一般のデータ構造と型には適していません。そのため、拡張性を維持したままデータ構造と型を定義したい場合はdefclassを使うのがベストです。拡張性(継承)が必要なければ構造体を利用します。


  1. なお、文字定数の表現はGNU CLISPとSBCLでは例の通りでしたが、CCLでは#\U+3000と出ました。
  2. このandは条件分岐で用いるマクロのandとは別物です。型指定子としてのandは複数の型指定子を「かつ」の条件で結ぶためのものです。また、evenp関数の引数はANSI Common Lispの標準で integer であることが定められており、integer 以外だとtype-errorが発生するため、このような指定を加えておいた方が型の定義としては万能になるでしょう。
  3. 「述語関数」とはtまたはnilを返すだけの条件判定用の関数です。英語ではpredicate functionなので、述語関数には最後にpを付けるのが慣例です。(ただし、nilかどうかを判定するnull関数はpが付いていないですが。)
  4. 真に頂点に君臨するのはtクラスです(混乱するかもしれませんが、シンボルとしてのtではなく、クラスとしてのtです)。Common Lispではnil以外が真として扱われますが、それはあらゆるオブジェクトがtクラスの派生として存在しているためです。standard-classdefclassマクロによって定義できるクラスの頂点ですが、tdefclassで定義されている訳ではありません。

Copyright (c) 2017-, satoshiweb.net. All rights reserved.