unmatch: パターンマッチングライブラリ
unmatch:ifmatch
はOn Lispの機能限定版if-match
です。
特徴としては、低速、低容量、緩いライセンスがあげられます。
世の中には様々なCommon Lispのパターンマッチングライブラリがあり、
どれもが洗練された優れたものばかりです。
本ライブラリはそれらとは全く対極のものであり、
機能性や性能などは考慮していません。
求められているのはコードの手軽さです。
インストール
下記をコピーして使ってください。
asdファイルなんて用意していません。
;; unmatch.lisp [Unlicense] (defpackage :unmatch (:use :cl) (:export #:ifmatch)) (in-package :unmatch) (defun strequal (x y) (and (symbolp y) (string= x (symbol-name y)))) (defun charequal (x y) (and (symbolp y) (char= x (char (symbol-name y) 0)))) (defun matchpat (a b &optional c) (cond ((strequal "_" a) (values t c)) ((charequal #\? a) (let ((list (assoc a c :test #'eq))) (if list (values (equal (cdr list) b) c) (values t (acons a b c))))) ((and (consp a) (consp b)) (multiple-value-bind (x y) (matchpat (car a) (car b) c) (and x (matchpat (cdr a) (cdr b) y)))) (t (values (equal a b) c)))) (defun matchlet (a b &aux root) (labels ((rec (x) (cond ((consp x) (rec (car x)) (rec (cdr x))) ((charequal #\? x) (pushnew x root))))) (rec a) (mapcar (lambda (x) `(,x (cdr (assoc ',x ,b :test #'eq)))) root))) (defun matchrec (x) (cond ((and (consp x) (eq (car x) 'quote)) x) ((consp x) `(cons ,(matchrec (car x)) ,(matchrec (cdr x)))) ((or (charequal #\? x) (strequal "_" x)) `',x) (t x))) (defmacro ifmatch (pat expr then &optional else) (let ((x (gensym)) (y (gensym))) `(multiple-value-bind (,x ,y) (matchpat ,(matchrec pat) ,expr) (declare (ignorable ,y)) (if ,x (let ,(matchlet pat y) ,then) ,else))))
使い方
構文を下記に示します。
(ifmatch match expr then [else])
例えば下記の通り。
(ifmatch (_ ?a (_ ?b . _) 10) '(10 20 (30 40 50 60) 10) (list ?a ?b)) -> (20 40)
strjis: 日本語テキスト入出力ライブラリ
【追記】ISO-2022-JP-2004に対応しました。
strjisの紹介
Common Lispで日本語のテキストを読み書きするライブラリを作成しました。
下記のエンコードを扱うことができます。
Common Lispにて日本語を読み書きする方法は、すでに色々と存在します。
わざわざこのライブラリを使用する必要はないかもしれません。
日本語を読み書きする一般的な方法は、 次の情報が参考になると思います。
LISPUSER Common Lisp と 日本語 と 文字コード
http://lispuser.net/commonlisp/japanese.html逆引きCommon Lisp 処理系:日本語の扱い
https://lisphub.jp/common-lisp/cookbook/index.cgi?p=%bd%e8%cd%fd%b7%cf%3a%c6%fc%cb%dc%b8%ec%a4%ce%b0%b7%a4%a4
本ライブラリは次の利点があります。
- 全部ANSI Common Lispで作成してある
- 各エンコード間で変換ができる
- 「JIS X 0213のコード対応表」を使っている
- JISエンコードの読み書きができる
欠点も色々とあります。
stream
として扱えない (stream
への入出力は可)open
の:external-format
に指定できない- 規格に厳密に従っているわけではない
今回は標準ANSI Common Lispだけで作りたかったので、 例えば配列から配列へ変換するような作りになっています。 別の機会にでも、勉強してstreamに対応させたりしてみたいです。
UNICODEの変換表は、下記のサイトを参考にさせていただきました。
Unicode Consortium
http://www.unicode.org/
http://www.unicode.org/Public/MAPPINGS/OBSOLETE/EASTASIA/JIS
http://www.unicode.org/Public/UNIDATA/EastAsianWidth.txtJIS X 0213のコード対応表
http://x0213.org/codetable/
http://x0213.org/codetable/iso-2022-jp-2004-std.txt
JIS X 0213のコード対応表につきましては、「Copyright (c) 2006-2017 Project X0213」さんが 作成したものであり、自由に使用・配布・改変等してもよいとのことでしたので、 加工して変換テーブルの実装に利用させていただきました。 ありがとうございます。
ライブラリの使用方法は下記のファイルにまとめました。
https://github.com/nptcl/strjis/blob/master/docs/readme.ja
動作確認はsbcl
, clisp
, ccl
にて行っています。
ただ、これらは全てbase-char
が21bit以上のコードを格納できる処理系です。
例えばAllegroCL
やLispWorks
なんかは、一文字16bitで処理しているとのことで、
そうなるとどこまでうまく動くかどうかわかりません。
使用例を下記に示します。
strjisの使い方
簡単な使い方をいくつか載せます。
JISをUTF-8に変換
JISの文字1B 24 42 あいう
を変換します。
(coerce-list '(#x1B #x24 #x42 #x24 #x22 #x24 #x24 #x24 #x26) :input 'jis :output 'utf8) -> (227 129 130 227 129 132 227 129 134)
SHIFT-JISからUTF-8 BOMありに変換
あいう
を変換します。
(coerce-list #(#x82 #xA0 #x82 #xA2 #x82 #xA4) :input 'shiftjis :output 'utf8bom) -> (239 187 191 227 129 130 227 129 132 227 129 134)
リストではなく配列を返却したい
関数coerce-vector
を使用します。
(coerce-vector '(#x82 #xA0 #x82 #xA2 #x82 #xA4) :input 'shiftjis :output 'utf8bom) -> #(239 187 191 227 129 130 227 129 132 227 129 134)
入力に文字列を指定したい
処理系がUnicode対応である必要があります。
(coerce-list "Hello" :output 'eucjis) -> (72 101 108 108 111)
出力を文字列にしたい
処理系がUnicode対応である必要があります。
(coerce-string '(#x1B #x24 #x42 #x24 #x22 #x24 #x24 #x24 #x26) :input 'jis) -> "あいう"
ファイルを変換したい
テキストファイルをJISに変換します。
処理系がUnicode対応である必要があります。
(with-open-file (input #p"input.txt" :direction :input) (with-open-file (output #p"output.txt" :direction :output :if-exists :supersede :if-does-not-exist :create :element-type '(unsigned-byte 8)) (coerce-stream input output :output 'jis)))
入力のファイル形式を指定して変換したい
EUC-JISのテキストファイルをUTF-16BE BOMなしに変換します。
入力も出力も(unsigned-byte 8)
でopen
してください。
(with-open-file (input #p"input.txt" :direction :input :element-type '(unsigned-byte 8)) (with-open-file (output #p"output.txt" :direction :output :if-exists :supersede :if-does-not-exist :create :element-type '(unsigned-byte 8)) (coerce-stream input output :input 'eucjis :output 'utf16be)))
入力にbabel
を使いたい
文字列あいう
の読み込みをbabel
に任せます。
(coerce-list (babel:string-to-octets "あいう" :encoding :utf-8) :input 'utf8 :output 'utf16le) -> (66 48 68 48 70 48)
入力にopen
のexternal-format
を使いたい
sbcl
の実行例です。
(with-open-file (input #p"input.txt" :direction :input :external-format :utf-8) (coerce-list input :output 'utf16le)) -> UTF-16LEのリスト
ISO-2022-JPを読み込みたい
【追記】ISO-2022-JP-2004に対応しました。
入力ISO-2022-JPからUTF-8に変換。
(coerce-string x :input 'iso2022jp :output 'utf8) →ISO-2022-JPからUTF-8に変換
書き込みは、ISO-2022-JP-2004に対応しています。
(coerce-string x :input 'utf8 :output 'iso2022jp) →UTF-8からISO-2022-JP-2004に変換
ISO-2022-JPの扱いについては別途説明します。
https://github.com/nptcl/strjis/blob/master/docs/readme.ja
関数の説明
変換に使用する関数は次の通り。
(defun coerce-list (x &key input output) ...) (defun coerce-vector (x &key input output) ...) (defun coerce-string (x &key input output) ...) (defun coerce-stream (x output-stream &key input output) ...)
x
は入力データです。
数値の配列、数値のリスト、文字列、streamのどれかを指定できます。
input
は入力エンコードタイプであり、下記のsymbol
から選択します。
utf8 ascii jis eucjp eucjis shiftjis unicode utf16 utf16v utf16be utf16le utf32 utf32v utf32be utf32le
output
は出力エンコードタイプであり、下記のsymbolから選択します。
ascii jis eucjp eucjis shiftjis unicode utf8 utf8bom utf8no utf16 utf16v utf16be utf16le utf16bebom utf16lebom utf32 utf32v utf32be utf32le utf32bebom utf32lebom
utf16v
は、0
から#xFFFF
までの数値を扱います。
utf32v
は、0
から#x10FFFF
までの数値を扱います。
ascii
は#x00
~#x7F
までの文字を限定で扱います。
utf8no
は、BOMを除去します。
utf16
とutf32
は、BOMからbig-endianかlittle-endianを判定します。
BOMが無かったらbig-endianであるとみなします。
unicode
は、char-code-limit
の値を見て、適切なタイプを判断します。
もしchar-code-limit
が255
以下ならutf8
です。
もしchar-code-limit
が65535
以下ならutf16v
です。
それ以外ならutf32v
です。
その他
eucjp
とeucjis
は同じです。
JISのエスケープシーケンスは、第1・第2水準漢字に相当するものが、 JIS C 6226-1978, JIS X 0208-1983, JIS X 0208-1990, JIS X 0213:2000 1面, JIS X 0213:2004 1面と様々ありますが、 全部同じものとして処理します。 JIS X 0213:2000 2面とJIS X 0212-1990補助漢字すら同じに扱います。
規約違反のUnicode文字は全てエラーです。
例えばコードが#x110000
を超えていたり、
Surrogate Code Pointを使ってみたり、
UTF-8で冗長な方法で表現していた場合は読み込みません。
オブジェクト関連で思ったこと
規約を翻訳して思ったことは、現状のnpt
ではかなり規約違反があるということです。
:allow-other-keys
なんて対応してないですもん。
地道に修正していこうと思います。
他、気になったことを忘れないように記載します。
qualifierはsymbol
だけじゃなくて何でも良かった?
自分が知らなかっただけですが、qualifierはnon-listとの記載がありました。
nil
以外のsymbol
だけが許容されるんだとばかり思っていたんですが、
何でも良かったんですね。
と思っていたんですが、確認してみるとどうもよくわからない。
ひとまずinteger
, character
, string
を試してみたんですが、
結果は下記の通り。
clisp
はinteger
,character
,string
を全部を受け付けるsbcl
はinteger
とcharacter
のみccl
はcharacter
,string
のみ
一例を下記に示します。
(define-method-combination test () ((code1 (10 20 "Hello")) (code2 (30 40 "aaabbb"))) `(progn ,@(mapcar (lambda (m) `(call-method ,m)) code1) ,@(mapcar (lambda (m) `(call-method ,m)) code2))) (defgeneric zzz (a) (:method-combination test)) (defmethod zzz 30 40 "aaabbb" (a) (format t "ccc ~A~%" a)) (defmethod zzz 30 40 "aaabbb" ((a integer)) (format t "bbb ~A~%" a)) (defmethod zzz 10 20 "Hello" (a) (format t "aaa ~A~%" a)) (zzz 999) →clispは正常に動作 →sbclはエラー、stringの"Hello", "aaabbb"がダメ →cclはエラー、integerの10, 20, 30, 40がダメ
正常パターンの動作結果は下記の通り。
aaa 999 bbb 999 ccc 999
なんなんだこれは。
まあqualifierなんてもんは、
symbol
だけを使うのが無難なんじゃないでしょうか。
1つか複数の返却値ってのはおかしい
0個の返却値も含まれるはず。
この文は「7.6.6.2 Standard Method Combination」あたりに出てくるものであり、
methodの返却値がgeneric-functionでどう扱われるかを説明したものです。
自分で訳しておいてケチ付けるのもアレですが、 たぶん翻訳がおかしいんだと思います。 元の文は「value or values」となっており、 それを「1つか複数の返却値」みたいに訳しています。 でも、たぶん複数の返却値もちゃんと考慮するんですよっていう 気遣いなんじゃないかと思うんですよ。
【間違い】defstruct
がmethod-defining formsに入っているのはおかしいのでは?
【追記】すみません、間違いです。投稿してから気が付きました。
methodの定義は、reader
/writer
ではなく、print-object
を対象にしています。
よってこの章の言ってることは間違いです。
これは規約の間違いなんじゃないかと思います。
「7.6.1 Introduction to Generic Functions」の「Figure 7-1.」では、
メソッドを定義するオペレーターの中にdefstruct
を入れています。
でもdefstruct
はreader
/writer
にあたる関数の生成機能はありますが、
ぜんぶ通常の関数を対象にしているため、generic-functionの生成機能はないはず。
だからこそ、defstruct
が生成する関数は、generic-function特有の
effective methodを特定する動作がない分だけちょっと早いはずなんです。
早いといってもクソみたいな差だと思いますが。
でも自分はまだdefstruct
を作成したことが無いので
勘違いしているかもしれません。
共有スロットはクラス宣言時にはまだ確保されていないかも?
ANSI Common Lisp範囲外の話だと思います。
規約では7.5.1章に「Defining a shared slot immediately creates a slot.」
みたいに書かれています。
共有スロットが宣言されたとき、即座にスロットが作成されるという意味です。
でもdefclass
なんかで:allocation
が:class
のスロットを宣言した瞬間では、
まだそのスロットは値の確保などが済んでいないかもしれません。
sbcl
では、class-prototype
経由で共有スロットにアクセスすると、
まだfinalizeが完了していないということでエラーになってしまいます。
以前説明したforward-referenced-class
関係の話ですね。
でもmake-instance
を経由してもらえば全然大丈夫なので、
これを規約違反とみなすのは無理があるとは思います。
標準のmethod-combinationは例文を見た方がわかりやすい
標準のmethod-combinationと言えば、standard
か、
or
といった短いフォームになりますが、
使い方を覚えようとしていた時代は、何の解説書を見ても
いまいち良くわからなかったことを覚えています。
規約には難しそうなことがごちゃごちゃ書かれていますが、
処理系を実装するんじゃない限り
define-method-combination
の使い方をちょっとだけ覚えてから
マクロdefine-method-combination
の規約に記載されている
例文を見た方がわかりやすいのではないでしょうか。
下記に例文を抜き出します。
- Macro DEFINE-METHOD-COMBINATION
;The default method-combination technique (define-method-combination standard () ((around (:around)) (before (:before)) (primary () :required t) (after (:after))) (flet ((call-methods (methods) (mapcar #'(lambda (method) `(call-method ,method)) methods))) (let ((form (if (or before after (rest primary)) `(multiple-value-prog1 (progn ,@(call-methods before) (call-method ,(first primary) ,(rest primary))) ,@(call-methods (reverse after))) `(call-method ,(first primary))))) (if around `(call-method ,(first around) (,@(rest around) (make-method ,form))) form))))
短いフォームは次の通り。
;The same thing, using the :order and :required keyword options (define-method-combination or (&optional (order ':most-specific-first)) ((around (:around)) (primary (or) :order order :required t)) (let ((form (if (rest primary) `(or ,@(mapcar #'(lambda (method) `(call-method ,method)) primary)) `(call-method ,(first primary))))) (if around `(call-method ,(first around) (,@(rest around) (make-method ,form))) form)))
わからないならごめんなさい。
7章Objectsのジェネリック関数関連の和訳
【追記】7章Objectsの全てを日本語訳しました。
https://nptcl.github.io/npt-japanese/docs/ansicl/7.html
https://nptcl.github.io/npt-japanese/md/ansicl/7.html
ふたつのリンクは、レイアウトが違うだけでどちらも同じ内容なので好きな方を見てください。
リンク先はDictionaryも翻訳してます。
ただし翻訳してあるのは7章だけです。
7章Objectsのクラス関連の和訳の続きです。
目次
7.6 ジェネリック関数とメソッド 7.6.1 ジェネリック関数の紹介 7.6.2 メソッドの紹介 7.6.3 特定パラメーターと限定子の合致 7.6.4 ジェネリック関数の全てのメソッドのラムダリストの合意 7.6.5 ジェネリック関数とメソッドのキーワード引数 7.6.5.1 ジェネリック関数とメソッドのキーワード引数の例 7.6.6 メソッドの選択とコンビネーション 7.6.6.1 有効なメソッドの決定 7.6.6.1.1 適用可能なメソッドの選択 7.6.6.1.2 優先順位による適用可能なメソッドのソート 7.6.6.1.3 ソートされた適用可能なメソッドのMethod-Combination実行 7.6.6.2 Standard Method-Combination 7.6.6.3 Method-Combinationの宣言 7.6.6.4 組み込みのMethod-Combination 7.6.7 メソッドの継承
7. オブジェクト
7.1~7.5までは7章Objectsのクラス関連の和訳を参照。
7.6 ジェネリック関数とメソッド
7.6.1 ジェネリック関数の紹介
ジェネリック関数は、指定された引数のクラスか、 あるいは引数の同一性に依存して動作する関数です。 ジェネリック関数オブジェクトは、メソッドの集合、 ラムダリスト、method-combination、そしてその他の情報に関連付けられます。
通常の関数のように、ジェネリック関数は引数を取り、 一連のオペレーションを実行し、そしておそらくは有効な値を返却します。 通常の関数は単一のコードの実体を持ち、関数が呼び出されたときに常に実行されます。 ジェネリック関数はコードの実体を複数の集合として持ち、 その集合の一部か全部を関数実行のときに選択します。 選ばれたコードの集合とその組み合わせは、ジェネリック関数に渡される1つか複数の引数から、 クラスかあるいは同一性により決定します。それはmethod-combinationによって決定が行われます。
通常の関数とジェネリック関数は、同一の構文により呼び出されます。
ジェネリック関数は本物の関数なので、funcall
とapply
の最初の引数として使用されたり、
あるいは引数を渡したりすることができます。
ジェネリック関数の関数名の設定は、いくつかの手順のひとつとして確立します。
それはグローバル環境内において、ensure-generic-function
,
defmethod
(暗にensure-generic-function
が呼ばれる),
defgeneric
(これもまた暗に、ensure-generic-function
が呼ばれる)によって確立されます。
レキシカル環境において、ジェネリック関数の関数名の束縛を確立するための
標準的な方法は提供されていません。
defgeneric
フォームが評価されるとき、
(ensure-generic-function
によって)次の3つのうちの1つの手順が取られます。
もし指定した名前のジェネリック関数がもう存在している場合は、 存在しているジェネリック関数オブジェクトを修正します。 いま実行された
defgeneric
フォームによって宣言されたメソッドは追加され、 そして以前のdefgeneric
フォームによって定義された、 存在していたジェネリック関数のどんなメソッドも削除されます。 いま実行されたdefgeneric
フォームによるメソッドの追加は、defmethod
,defclass
,define-combination
, そしてdefstruct
によって 定義されたメソッドも置き換えます。 ジェネリック関数内のその他のメソッドについては、影響は無いですし置き換えもされません。もし指定した名前が、通常の関数、マクロ、特殊オペレーターによる名前であった場合は、 エラーが発せられます。
そうでなければ、ジェネリック関数は
defgeneric
フォーム内にある メソッド定義により宣言されたメソッドとともに作成されます。
いくつかのオペレーターは、ジェネリック関数のオプションの定義として、
使用するmethod-combinationのタイプや引数優先順位を指定することが許されています。
これらのオペレーターは、「ジェネリック関数のオプションを指定するオペレーター」と言います。
この分類の中で、標準的なオペレーターはdefgeneric
だけです。
いくつかのオペレーターは、ジェネリック関数のメソッドを定義します。 これらのオペレーターはメソッド定義オペレーターと言われます。 このオペレーターに関連付けられたフォームは、メソッド定義フォームと呼ばれます。 標準的なメソッド定義オペレーターを次の表に示します。
defgeneric define-combination defmethod defstruct defclass 表7-1 標準メソッド定義オペレーター
注意として、標準メソッド定義オペレーターのdefgeneric
だけは、
ジェネリック関数のオプションを指定することが出来ます。
defgeneric
といくつかの実装定義オペレーターは、
ジェネリック関数のオプションを指定することが可能であり、
「ジェネリック関数のオプションを指定するオペレーター」と言われます。
7.6.2 メソッドの紹介
メソッドは、クラス特定か、あるいは同一性特定による動作と、 ジェネリック関数のオペレーションを定義します。
メソッドオブジェクトは、次のものと結びつけられます。 それは、メソッドの動作を定義したコード。 与えられたメソッドが適用するかどうかを特定するための一連の特定パラメーター。 ラムダリスト。そして、method-combinationがメソッドの区分けをするために使われる、一連の限定子。
メソッドオブジェクトは関数ではないため、関数として呼び出すことはできません。 ジェネリック関数が呼び出されたときのような場合において、 オブジェクトシステムの様々な仕組みがメソッドオブジェクトを受け取り、 そしてメソッド関数を呼び出します。 このような動作を、メソッドが実行された、あるいはメソッドが呼び出されたと言います。
メソッド定義フォームは、 ジェネリック関数の引数起因により定義メソッドを実行するためのコードを含みます。 メソッド定義フォームが評価されたとき、メソッドオブジェクトは作成され、 次の4つのアクションが取られます。
もし指定した名前のジェネリック関数がすでに存在しており、 さらに特定パラメーターと限定子が 新しいものと一致するメソッドオブジェクトがすでに存在していた場合は、 新しいメソッドオブジェクトが古いものと置き換えられます。 定義されたメソッドが別の特定パラメーターと限定子の場合は、 7.6.3 特定パラメーターと限定子の合致を参照。
もし指定した名前のジェネリック関数がすでに存在しており、 さらに特定パラメーターと限定子が 新しいものと一致するメソッドオブジェクトが存在していなかった場合は、 既存のジェネリック関数オブジェクトは、新しいメソッドオブジェクトを含むように修正されます。
もし指定した名前が、通常の関数か、マクロ、特別オペレーターの名前であった場合は、 エラーが発せられます。
それ以外の場合は、メソッド定義フォームによって定義されたメソッドとともに、 ジェネリック関数が作成されます。
もし新しいメソッドのラムダリストがジェネリック関数のラムダリストに合致していない場合は、 エラーが発せられます。 もしジェネリック関数オプションを指定できないメソッド定義オペレーターが 新しいジェネリック関数を作成する場合は、 ジェネリック関数のラムダリストは、メソッドのものと合致するように、 メソッド定義フォームから生成されるメソッドのラムダリストから導出されます。 合致の議論については、7.6.4 ジェネリック関数の全てのメソッドのラムダリストの合意を参照。
各メソッドは特定されたラムダリストを持っており、それはメソッドが適用されたときに決定します。
特定されたラムダリストは、通常のラムダリストに似ていますが、
要求パラメータの名前の代わりに特定パラメーターとなるのが違っています。
特定パラメーターは(変数名 特定パラメーター名)
のリストであり、
特定パラメーター名は次のうちの1つを取ります。
シンボル:特定パラメーターはシンボルによるクラス名です。
クラス:特定パラメーターはクラス自身です。
(eql form)
:特定パラメーターは型特定子(eql object)
を満たし、object
はform
を評価した結果となります。form
フォームはメソッド定義フォームが評価された中でのレキシカル環境内によって評価されます。 注意として、form
はメソッドが定義されたときに、ただ一度だけ評価されます。 ジェネリック関数が呼び出されるたびに評価されるのではありません。
特定パラメーター名は、ユーザーレベルのインターフェースである
defmethod
のようなマクロで使用されることを意図しており、
特定パラメーターは関数のインターフェース上で使われます。
要求パラメーターのみを特定化することができ、
各要求パラメーターは特定パラメーターが存在しなければなりません。
表記を単純にするために、もしメソッド定義フォームの特定化されたラムダリスト内において、
要求パラメーターが単純に変数名だけであった場合は、
その要求パラメーターはデフォルトのクラスt
が指定されます。
ジェネリック関数に引数の集合が与えられたとき、 適用するメソッドは、特定パラメーターが対応する引数によって満たされるジェネリック関数のメソッドです。 次の定義は、メソッドが適用可能かどうか、また引数が特定パラメーターを満たすかどうかとは、 どういう意味であるのかを示します。
<A1, ..., An>
がジェネリック関数の要求パラメーターの順番であるとします。
<P1, ..., Pn>
がメソッドM
における要求パラメーターに対応する特定パラメーターの順番であるとします。
各Ai
の型が、型特定子Pi
によって特定されるとき、メソッドM
は適用可能です。
全ての有効な特定パラメータは有効な型指定子でもあるため、
関数typep
は、メソッドの選択において、
引数が特定パラメーターを満たすかどうかを決定するために使用できます。
全ての特定パラメーターがクラスt
のメソッドは、デフォルトメソッドと呼ばれます。
これは常に適用可能ですが、他のもっと特定的なメソッドによってシャドウされるかもしれません。
メソッドは限定子を持てます。 これはmethod-combinationがメソッドを区別するための方法としての手順を与えます。 1つか複数の限定子を持っているメソッドは、限定されたメソッドと呼ばれます。 限定子を持っていないメソッドは、限定されていないメソッドと呼ばれます。 限定子はリスト以外のオブジェクトです。 標準のmethod-combinationによって定義された限定子の型はシンボルです。
この定義の中で、「プライマリメソッド」と「補助メソッド」という語は、
これらを使用するmethod-combinationタイプにおいて、メソッドを区分けするために使用されます。
method-combinationのstandard
では、プライマリメソッドは限定されていないメソッドであり、
補助メソッドは単一の限定子である:around
, :before
, :after
のうちの1つを指定したメソッドです。
これらのメソッドは、順にaround
メソッド、before
メソッド、after
メソッドと呼びます。
method-combinationタイプがdefine-method-combination
の
短いフォームを使用して定義されたとき、
プライマリメソッドはmethod-combinationタイプの名前を限定子に与えたメソッドになります。
そして補助メソッドは:around
の限定子です。
このように「プライマリメソッド」と「補助メソッド」という語は、
method-combinationタイプにおける、ただの相対的な定義となります。
7.6.3 特定パラメーターと限定子の合致
2つのメソッドが特定パラメーターと限定子それぞれにおいて 互いに合致したと言えるのは、次に示す状況が当てはまる場合です。
両方のメソッドが同じ数の要求パラメーターを持っているとき。 例えば、2つのメソッドの特定パラメーターが、
P1,1 ... P1,n
とP2,1 ... P2,n
であったとき。1≦i≦n
の各P1,i
がP2,i
に一致したとき。 特定パラメーターP1,i
がP2,i
に一致したとは、P1,i
とP2,i
が同じクラスであるか、 あるいはP1,i=(eql object1)
,P2,i=(eql object2)
,(eql object1 object2)
のとき。 それ以外では、P1,i
とP2,i
は一致していません。2つの限定子のリストが、
equal
で等しいとき。
7.6.4 ジェネリック関数の全てのメソッドのラムダリストの合意
下記に示すこれらの定義は、ラムダリストの集合の合意を定義します。 ラムダリストには、指定したジェネリック関数の各メソッドのラムダリストと、 ジェネリック関数自身で定義されたラムダリストを含みます。
各ラムダリストは、同じ数の要求パラメーターを持つ必要があります。
各ラムダリストは、同じ数の
&optional
パラメーターを持つ必要があります。 各メソッドは、&optional
パラメーターに独自のデフォルト値を提供することができます。もしどれかのラムダリストが
&rest
か&key
を持つなら、 各ラムダリストはそのうちの1つか両方を指定する必要があります。もしジェネリック関数のラムダリストが
&key
を持つなら、 各メソッドは&key
の後の全てのキーワードの名前を受け付けるようにする必要があります。 受け付ける方法は、全ての名前を明に指定する方法、 あるいは&allow-other-keys
を指定する方法がありますが、&key
の指定ではなく&rest
を指定する方法でも問題ありません。 各メソッドは、独自のキーワード引数を追加で受け付けることができます。 キーワードの名前の有効性のチェックはジェネリック関数が行い、各メソッドでは行いません。 メソッドが実行されたときには、キーワード引数に名前が:allow-other-keys
、 値がtrue
であるペアが与えられたように呼び出されますが、 そのような引数のペアは渡されません。&allow-other-keys
の使用は、ラムダリスト間で一貫している必要はありません。 もし&allow-other-keys
がジェネリック関数か適用メソッドのラムダリストに指定されている場合、 ジェネリック関数が呼び出されるときには、どんなキーワード引数も受け付けるでしょう。&aux
の使用は、メソッド間で一貫している必要はありません。
もしジェネリック関数のオプションを指定できないメソッド宣言オペレーターが
ジェネリック関数を作成した場合、
さらにメソッドのラムダリストにキーワードパラメーターが指定されている場合は、
ジェネリック関数のラムダリストには&key
が指定されます(しかしキーワード引数は指定されない)。
7.6.5 ジェネリック関数とメソッドのキーワード引数
ジェネリック関数かそのメソッドがラムダリストに&key
を指定しているとき、
ジェネリック関数によって受け取れるキーワード引数の集合の定義は、
適用可能なメソッドによって変化します。
ある呼び出しにおいてジェネリック関数が受け取れるキーワード引数の集合とは、
適用可能な全てのメソッドによって受け取ることができるキーワード引数と、
ジェネリック関数に&key
があるならば&key
以降に示されるキーワード引数の集合です。
&key
の指定が無く、&rest
の指定があるメソッドは、
受け付けるキーワード引数の集合には影響しません。
もし適用可能なメソッドのどれかのラムダリストか、
あるいはジェネリック関数の宣言によるラムダリストが&allow-other-keys
を含んでいる場合は、
そのジェネリック関数は全てのキーワード引数を受け取ります。
ラムダリスト一致の規則は、
各メソッドが次のような全てのキーワード引数を受け付けるようにすることを要求します。
それは、ジェネリック関数の定義において&key
の後に指定したものを明に受け取れるようにするか、
&allow-other-keys
を指定するか、あるいは&key
が無い場合に&rest
を設定するかになります。
各メソッドは、ジェネリック関数の定義にあるキーワード引数に加えて、
独自のキーワード引数を追加で受け取るようにすることができます。
もしジェネリック関数は、渡されたキーワード引数がどの適用メソッドにも受け付けられなかった場合は、 エラーを発する必要があります。3.5 関数呼び出しのエラーチェックを参照。
7.6.5.1 ジェネリック関数とメソッドのキーワード引数の例
例えば、下記の2つのwidth
メソッドが定義されていることを考えます。
(defmethod width ((c character-class) &key font) ...) (defmethod width ((p picture-class) &key pixel-size) ...)
その他のwidth
以外のメソッドとジェネリック関数は存在しないと仮定します。
下記のフォームを評価したときには、
キーワード引数:pixel-size
が適用可能なメソッドで受け付けられないため、
エラーが発せられます。
(width (make-instance 'character-class :char #\Q) :font 'baskerville :pixel-size 10)
下記のフォームの評価は、エラーが発せられます。
(width (make-instance 'picture-class :glyph (glyph #\Q)) :font 'baskerville :pixel-size 10)
下記のフォームの評価は、もしcharacter-picture-class
という名前のクラスが、
picture-class
とcharacter-class
両方のサブクラスであった場合には、
エラーにはならないでしょう。
(width (make-instance 'character-picture-class :char #\Q) :font 'baskerville :pixel-size 10)
7.6.6 メソッドの選択とコンビネーション
ジェネリック関数が特定の引数とともに呼び出されたとき、 実行するコードを決定しなければなりません。 このコードは、これらの引数に対する有効なメソッドと呼ばれます。 有効なメソッドは、ジェネリック関数内の適用可能なメソッドを結びつけたものであり、 このメソッドのいくつかか、あるいは全てのメソッドが呼び出されます。
もしジェネリック関数が呼び出されたときに、適用可能なメソッドが存在しなかった場合は、
ジェネリック関数no-applicable-method
が呼び出されます。
その呼び出した結果の返却値は、最初のジェネリック関数が呼び出されたものの返却値として使用されます。
no-applicable-method
の呼び出しは、キーワード引数が受付可能かどうか先行してチェックされます。
7.6.5 ジェネリック関数とメソッドのキーワード引数を参照。
もし有効なメソッドが決定されたら、 ジェネリック関数に渡されたものと同じ引数とともに呼び出されます。 どのような返却値であれ、それはジェネリック関数が返却した値として返却されます。
7.6.6.1 有効なメソッドの決定
有効なメソッドは、下記の3ステップによって決定されます。
適用可能なメソッドの選択します。
優先順位によって適用可能なメソッドをソートし、最も特定的なメソッドを最初に選択します。
ソートされた適用可能なメソッドをmethod-combinationに渡して実行し、有効なメソッドを生成します。
7.6.6.1.1 適用可能なメソッドの選択
この手順は、7.6.2 メソッドの紹介で定義されています。
7.6.6.1.2 優先順位による適用可能なメソッドのソート
2つのメソッドの優先順位を比べるために、特定パラメーターが順番に調べられます。
デフォルトの調査順は左から右ですが、defgeneric
か、あるいは別のオペレーターによる
ジェネリック関数の:argument-precedence-order
オプションによって、逆順に指定されます。
各メソッドの対応する特定パラメーターが比較されます。 特定パラメーターのペアが一致していたら、次のペアが比較されます。 もし対応する全ての特定パラメーターが一致していたのであれば、 2つのメソッドは違った限定子を持っている必要があります。 このケースの場合、メソッドはどちらの順番でも選択できます。 一致についての詳細は、7.6.3 特定パラメーターと限定子の合致を参照。
もしいくつかの特定パラメーターが一致していなかった場合、 最初に一致しなかった特定パラメーターのペアが優先順位を決定します。 もしどちらの特定パラメーターもクラスであったとき、 その2つのメソッドの対応する特定パラメーターを見て、 クラス優先リストの中に早く現われた方がより特定的なメソッドとなります。 適用可能なメソッドの集合から選択する方法を行っているため、 特定パラメーターは引数のクラスのクラス優先リストに存在することが保証されます。
もし対応する特定パラメーターのペアのうち、ちょうど1つが(eql object)
であったときは、
その特定パラメーターを持つメソッドが、他のメソッドより優先します。
もし両方の特定パラメーターがeql
形式であったときは、
特定は一致するとしなければなりません
(そうでなければ2つのメソッドはこの引数において両方とも適用できなかったでしょう)。
適用可能なメソッドリストの結果は、最も特定的なメソッドが最初であり、 最も特定的ではないメソッドが最後になります。
7.6.6.1.3 ソートされた適用可能なメソッドのMethod-Combination実行
単純な場合として、method-combinationはstandard
が使われており、
全ての適用可能なメソッドはプライマリメソッドであるとします。
この場合は、有効メソッドは最も特定的なメソッドとなります。
メソッドは、次に特定的なメソッドを関数call-next-method
の使用にて呼び出すことができます。
call-next-method
によって呼び出されるメソッドは、次のメソッドと言います。
関数next-method-p
は、次のメソッドが存在するかどうかをテストします。
もしcall-next-method
が呼ばれたものの、次の特定的なメソッドが存在しなかった場合は、
ジェネリック関数no-next-method
が呼び出されます。
一般的に、有効なメソッドは、適用可能なメソッドを組み合わせた結果のいくつかとなります。
これは次に記載されたような目的によりフォームとして定義されます。
適用可能なメソッドは、いくつかが呼ばれるか、あるいは全部が呼ばれるかを定義します。
また、返却値は1つか複数が返却されるように定義します。
その返却値はジェネリック関数として返却されるものです。
付加的にはいくつかのメソッドがcall-next-method
を用いてアクセス可能になるように定義します。
有効なメソッドにおける各メソッドの役割は、 メソッドの限定子と特定子によって決定されます。 限定子はメソッドに印をつけるものであり、 限定子の意味は手続きにおいて印を用いることで決定されます。 もし適用可能なメソッドが認識できない限定子を持っていた場合はエラーを発し、 有効なメソッドの中にこのメソッドが存在しないものとします。
method-combinationのstandard
が限定されたメソッドと一緒に使われたときは、
有効なメソッドは7.6.6.2 Standard Method-Combinationに記載されたものとして生成されます。
他のタイプのmethod-combinationは、defgeneric
かあるいは別のオペレーターで
ジェネリック関数のオプション:method-combination
を使うことで使用できます。
この方法により、手順をカスタマイズできます。
新しいタイプのmethod-combinationは、
define-method-combination
マクロを使うことによって定義することができます。
7.6.6.2 Standard Method-Combination
method-combinationのstandard
は、standard-generic-function
クラスによって提供されます。
これはmethod-combinationのタイプが指定されなかった場合か、
あるいは組み込みのmethod-combinationタイプであるstandard
が指定された場合に使われます。
プライマリメソッドは有効なメソッドのメインとなる動作として定義されます。 補助メソッドは3つあるうちの1つの方法を用いて動作を変更します。 プライマリメソッドは限定子を持ちません。
補助メソッドは、限定子:before
, :after
, そして:around
のメソッドです。
method-combinationのstandard
は、メソッドに対して2つ以上の限定子を許容しません。
もしメソッド定義で複数の限定子をもつメソッドを定義した場合は、エラーが発せられます。
before
メソッドは、ただひとつの限定子として:before
キーワードを持ちます。before
メソッドは、プライマリメソッドの前に実行されるコードを定義します。after
メソッドは、ただひとつの限定子として:after
キーワードを持ちます。after
メソッドは、プライマリメソッドの後に実行されるコードを定義します。around
メソッドは、ただひとつの限定子として:around
キーワードを持ちます。around
メソッドは、他の適用可能なメソッドの代わりとして実行されますが、 いくつかのシャドウされたメソッドを(call-next-method
経由で)呼び出すコードを、 明に含むことができます。
method-combinationのstandard
の意味を次に示します。
もし
around
メソッドが存在する場合は、最も特定的なaround
メソッドが呼ばれます。 これはジェネリック関数に対して1つか複数の返却値を提供します。around
メソッドのコード内では、次のメソッドを呼ぶためのcall-next-method
が使用できます。 次のメソッドから戻ったとき、around
メソッドは返却された値に基づいて、 さらにコードを実行することができます。 もしcall-next-method
を使用したときに呼び出せる適用可能なメソッドが存在しなかった場合は、 ジェネリック関数no-next-method
が呼び出されます。 関数next-method-p
は、次のメソッドが存在するかどうかを決定するために使われます。もし
around
メソッドがcall-next-method
を実行したとき、 次の特定的なaround
メソッドが適用可能であれば呼び出されます。 もしaround
メソッドが存在しないか、 あるいは最も特定的ではないaround
メソッドによってcall-next-method
が呼び出された場合は、 次に示すものとして他のメソッドが呼び出されます。全ての
before
メソッドがmost-specific-first
の順番で呼ばれます。 これらの返却値は無視されます。 もしbefore
メソッド内でcall-next-method
が使用された場合は、エラーが発せられます。最も特定的なプライマリメソッドが呼び出されます。 プライマリメソッドのコード内では、 次の特定的なプライマリメソッドを呼び出すための
call-next-method
が使用できます。 メソッドから戻ったとき、以前のプライマリメソッドは返却された値に基づいて、 さらにコードを実行することができます。 もしcall-next-method
を使用したときに、呼び出せる適用可能なメソッドが存在しなかった場合は、 ジェネリック関数no-next-method
が呼び出されます。 関数next-method-p
は、次のメソッドが存在するかどうかを決定するために使われます。 もしcall-next-method
が使われなかった場合は、最も特定的なプライマリメソッドだけが呼び出されます。全ての
after
メソッドがmost-specific-last
の順番で呼ばれます。 これらの返却値は無視されます。 もしafter
メソッド内でcall-next-method
が使用された場合は、エラーが発せられます。
もし
around
メソッドが呼び出されなかった場合は、 最も特定的なプライマリメソッドが、 1つか複数の値をジェネリック関数の返却値として返却します。 最も特定的ではないaround
メソッドからcall-next-method
の呼び出しによって返却される1つか複数の返却値は、 最も特定的なプライマリメソッドの返却値となります。
method-combinationのstandard
では、適用可能なメソッドが存在しても、
適用可能なプライマリメソッドが存在しなかった場合はエラーが発せられます。
before
メソッドはmost-specific-first
順にて実行され、
after
メソッドはleast-specific-first
順に実行されます。
この設計の違いの根拠を、例として次のように示します。
クラスC1
がスーパークラスであるC2
の動作を、
before
メソッドとafter
メソッドに追加することで変更することを考えます。
クラスC2
の振る舞いがC2
のメソッドとして直接定義するか、
あるいはスーパクラスを継承によるものかに関わらず、
クラスC1
のインスタンスによって呼び出されるメソッドの相対的な順番には影響しません。
クラスC1
のbefore
メソッドは、クラスC2
の全てのメソッドの前に実行されます。
クラスC1
のafter
メソッドは、クラスC2
の全てのメソッドの後に実行されます。
対称的に、全てのaround
メソッドが実行されるのは、他のメソッドが実行される前です。
このように最も遠いaround
メソッドは、最も特定的なプライマリメソッドの前に実行されます。
もしプライマリメソッドのみが宣言されており、
さらにcall-next-method
が使用されなかった場合は、
最も特定的なメソッドのみが実行されます。
つまり最も特定的なメソッドが他の一般的なメソッドをシャドウしたということです。
7.6.6.3 Method-Combinationの宣言
マクロdefine-method-combination
は、method-combinationの新しいフォームを定義します。
これは、有効なメソッドの生成をカスタマイズする仕組みを提供します。
標準の有効なメソッドの生成手順は、7.6.6.1 有効なメソッドの決定に記載されています。
define-method-combination
には、2つのフォームが存在します。
短いフォームは単純な宣言方法であり、長いフォームはもっと強力で冗長です。
長いフォームはdefmacro
と似ています。
コード本体は式であり、Lispフォームを計算します。
これはmethod-combination内の構造を任意に制御する仕組みと、
メソッドの限定子を任意に扱う仕組みを提供します。
7.6.6.4 組み込みのMethod-Combination
オブジェクトシステムは、組み込みのmethod-combinationタイプをいくつか提供しています。
これらのmethod-combinationタイプのうちの1つを、ジェネリック関数で使うことができます。
指定する方法は、method-combinationタイプの名前を、
defgeneric
の:method-combination
オプションに引数として与えるか、
その他のオペレーターでジェネリック関数のオプション:method-combination
を指定することです。
組み込みのmethod-combinationタイプの名前を、次の表に示します。
+ and append list max min nconc or progn standard 表7-2 組み込みのMethod-Combinationタイプ
組み込みのmethod-combinationタイプであるstandard
の意味は、
7.6.6.2 Standard Method-Combinationに記載しました。
他の組み込みmethod-combinationタイプは、
シンプルな組み込みmethod-combinationタイプと呼ばれています。
シンプルなmethod-combinationタイプは、
短い形式のdefine-method-combination
によって定義されたかのように動作します。
メソッドは次の2つの役割を認識します。
around
メソッドは、ただひとつの限定子として:around
キーワードを持ちます。around
メソッドの意味はmethod-combinationのstandard
と同じです。around
メソッド内ではcall-next-method
とnext-method-p
の関数の使用が提供されます。プライマリメソッドは、ただひとつの限定子としてmethod-combinationの名前を持ちます。 例えば、組み込みのmethod-combinationタイプの
and
は、 ただひとつの限定子であるand
を指定したメソッドをプライマリメソッドとして認識します。call-next-method
とnext-method
の関数はプライマリメソッドでは提供されません。
シンプルな組み込みmethod-combinationタイプの意味を下記に示します。
もし
around
メソッドが存在するならば、最も特定的なaround
メソッドが呼ばれます。 これはジェネリック関数へ1つか複数の返却値を提供します。around
メソッドのコード内では、次のメソッドを呼ぶためのcall-next-method
が使用できます。 もしcall-next-method
を使用したときに、呼び出せる適用可能なメソッドが存在しなかった場合は、 ジェネリック関数no-next-method
が呼び出されます。 関数next-method-p
は、次のメソッドが存在するかどうかを決定するために使われます。 次のメソッドから戻ったとき、around
メソッドは返却された値に基づいて、 さらにコードを実行することができます。もし
around
メソッドがcall-next-method
を実行したとき、 次の特定的なaround
メソッドが適用可能であれば呼び出されます。 もしaround
メソッドが存在しないか、 あるいは最も遠いaround
メソッドによってcall-next-method
が呼び出されたときは、 組み込みmethod-combinationタイプの名前と適用可能なプライマリメソッドのリストから、 評価されるとジェネリック関数の返却値を生成する様なLispフォームが導出されます。 例えば、method-combinationタイプの名前がoperator
のとき、 ジェネリック関数が次のフォームによって呼び出されることを考えます。
(generic-function a1...an)
ここでM1, ... Mk
はこの順に適用可能なプライマリリストであるとします。
そのとき、Lispフォームは次のように導出されます。
(operator <M1 a1...an> ... <Mk a1...an>)
もし式<Mi a1...an>
が評価されたとき、メソッドMi
は引数a1...an
を適用します。
例えばoperator
がor
のとき、式<Mi a1...an>
は、
ただ<Mj a1...an>
, 1≦j<i
がnil
を返却したときのみに評価されます。
プライマリメソッドのデフォルトの順番は:most-specific-first
です。
しかし、:method-combination
オプションの2番目の引数に
:most-specific-last
を指定したときは逆順にすることができます。
シンプルな組み込みmethod-combinationタイプは、
メソッドに対して正確に1つの限定子を要求します。
もし限定子が存在しない適用可能なメソッドか、
あるいはmethod-combinationタイプが認識しない限定子を指定したときはエラーが発せられます。
もしaround
メソッドが存在するものの、プライマリメソッドが存在しない場合は、エラーが発せられます。
7.6.7 メソッドの継承
サブクラスは、クラスの全てのインスタンスのどんな適用可能なメソッドも継承します。 また、クラスのどんなサブクラス全てのインスタンスにも適用可能です。
メソッドの継承は、メソッド定義オペレーターによって作成されたメソッドであっても、 同じ方法によって行われます。
メソッドの継承の詳細は、7.6.6 メソッドの選択とコンビネーションに記載されています。
7章Objectsのクラス関連の和訳
【追記】7章Objectsの全てを日本語訳しました。
https://nptcl.github.io/npt-japanese/docs/ansicl/7.html
https://nptcl.github.io/npt-japanese/md/ansicl/7.html
ふたつのリンクは、レイアウトが違うだけでどちらも同じ内容なので好きな方を見てください。
リンク先はDictionaryも翻訳してます。
ただし翻訳してあるのは7章だけです。
ANSI Common Lispの規格書の、「7. Objects」の一部の和訳です。
必要になったので必死に翻訳しました。
訳があっているかどうかは知らん。
翻訳元は下記の通り。
draft proposed American National Standard for Information Systems Programming Language Common Lisp Version 15.17R, X3J13/94-101R. Fri 12-Aug-1994 6:35pm EDT http://www.cs.cmu.edu/afs/cs/Web/Groups/AI/lang/lisp/doc/standard/ansi/dpans/
HyperSpecだと下記の通り。
7. Objects http://www.lispworks.com/documentation/HyperSpec/Body/07_.htm
7.6 Generic Functions and Methods
以降は長いのでまた今度。
【追記】続きを投稿しました。7章Objectsのジェネリック関数関連の和訳
目次
7. オブジェクト 7.1 オブジェクトの作成と初期化 7.1.1 初期化引数 7.1.2 初期化引数の有効性の宣言 7.1.3 初期化引数のデフォルト値 7.1.4 初期化引数の規則 7.1.5 Shared-Initialize 7.1.6 Initialize-Instance 7.1.7 Make-InstanceとInitialize-Instanceの宣言 7.2 インスタンスのクラスの変更 7.2.1 インスタンスの構造の修正 7.2.2 新しく追加された局所スロットの初期化 7.2.3 インスタンスのクラスの更新のカスタマイズ 7.3 インスタンスの再初期化 7.3.1 再初期化のカスタマイズ 7.4 メタオブジェクト 7.4.1 標準メタオブジェクト 7.5 スロット 7.5.1 スロットの紹介 7.5.2 スロットへのアクセス 7.5.3 スロットの継承とスロットオプション 【別投稿】7.6 ジェネリック関数とメソッド
7. オブジェクト
7.1 オブジェクトの作成と初期化
ジェネリック関数make-instance
は、クラスの新しいインスタンスを作成し返却します。
最初の引数はクラスか、クラスの名前であり、残りの引数は初期化引数リストです。
新しいインスタンスの初期化は、いくつかのステップから成ります。
内容は次のようになります。
指定されなかった初期化引数の値に対して、明に指定された初期化引数とデフォルト値を結びつけるステップ。
初期化引数の有効性をチェックするステップ。
インスタンスの記憶領域を確保するステップ。
スロットに値を埋めるステップ。
そして追加の初期化を行うためにユーザーが提供したメソッドを実行するステップ。
make-instance
の各ステップはジェネリック関数により実装されているため、
それぞれのステップをカスタマイズする仕組みが提供されています。
加えて、make-instance
自体がジェネリック関数であるため、自身もカスタマイズできます。
オブジェクトシステムは各ステップに対して、システムで提供されたメソッドを用意しています。 メソッドは初期化全体の手順の標準的な振る舞いを定義したものです。 標準の振る舞いは、下記の4つの簡単な仕組みによって、初期化を制御することができます。
スロットの初期化引数としてのシンボルの宣言。 初期化引数は
defclass
のスロットオプションである、:initarg
を使うことで宣言できます。 これは、make-instance
の呼び出し時に、 スロットの値を設定するための仕組みとして提供されたものです。初期化引数のデフォルト値フォームの指定。初期化引数のデフォルト値フォームは、
defclass
のクラスオプションである:default-initargs
を使うことで定義できます。 もし初期化引数がmake-instance
の引数として明に提供されなかった場合、 デフォルト値フォームはdefclass
が宣言されたレキシカル環境の中で評価されます。 そして評価された結果の値は、初期化引数の値として使用されます。スロットのデフォルト初期化値フォームの提供。スロットのデフォルト初期化値フォームは、
defclass
のスロットオプション:initform
を使うことで宣言されます。 もしmake-instance
の引数かあるいは:default-initargs
のデフォルト値にて、 スロットに対応する初期化引数が与えられていなかった場合、 デフォルト値フォームはdefclass
が宣言されたレキシカル環境の中で評価されます。 そして評価された結果の値はスロットに格納されます。 局所スロットの:initform
フォームは、インスタンスが作成されたとき、 クラスの再定義によりインスタンスを更新するとき、 そしてインスタンスを違うクラスの定義に更新するときに使用されるでしょう。 共有スロットの:initform
フォームに関しては、 定義のときか、再定義のときに使用されます。initialize-instance
とshared-initialize
のメソッド定義。 スロットの値を設定するこれらの振る舞いは、 システムが提供するメソッドで提供されており、initialize-instance
は、shared-initilize
を呼び出すように実装されています。 ジェネリック関数shared-initialize
は初期化の部分を実装しており、 次の4つの状況で共有されています。 それは、インスタンス作成時、インスタンスの再初期化時、 クラスの再定義によるインスタンスの更新時、 そして違うクラス定義へインスタンスを更新するときです。 システムが提供するshared-initialize
のメソッドは、 スロットの値を更新するための上記の振る舞いを直接実装しているため、initialize-instance
は単純にshared-initialize
を呼び出すだけとなります。
7.1.1 初期化引数
初期化引数は、オブジェクトの作成と初期化を制御します。
よくキーワードを初期化引数の名前にするのが便利で使われますが、
初期化引数の名前はnil
を含むどんなシンボルでも使用できます。
初期化引数は、次の2つの方法である、
スロットの値を埋めるためか、
あるいは初期化メソッドの引数に提供するときに使用します。
単一の初期化引数は、両方の目的で使用されます。
初期化引数リストは、初期化引数の名前と値のプロパティリストです。
この構造は、通常のプロパティリストとして同一であり、
引数リストの&key
パラメーターとして処理される部分としても同一です。
これらのリストは、もし初期化引数の名前が初期化引数リストに複数現れた場合は、
もっとも左側に現れた値が指定され、残りのものは無視されます。
make-instance
の引数(最初の引数よりあとのもの)の形は、初期化引数リストです。
初期化引数はスロットと結び付けることができます。
もし初期化引数が初期化引数リストの中で値を持っている場合、
その値は新しく作成されたオブジェクトのスロットに格納されます。
もし:initform
フォームがスロットと結び付けられていた場合でも、初期化引数の方が上書きをします。
1つの初期化引数は、複数のスロットを初期化することができます。
共有スロットを初期化する初期化引数は、以前の値を置き換えて、共有スロットに値を格納します。
初期化引数はメソッドに結び付けることができます。
オブジェクトが作成されて、特定の初期化引数が与えられた場合、
ジェネリック関数であるinitialize-instance
, shared-initialize
, そしてallocate-instance
は、
キーワード引数のペアとして、初期化引数の名前と値とともに呼び出されます。
もし初期化引数の値が初期化引数リストで提供されていなかった場合は、
メソッドのラムダリストがデフォルト値を提供します。
初期化引数は次の4つの状況によって使用されます。 インスタンスの作成時、インスタンスの再初期化時、 クラス再定義によるインスタンスの更新時、そして違うクラス定義へのインスタンスを更新するときです。
初期化引数は特定のクラスのインスタンスの作成と初期化時に制御で使用されるため、 初期化引数は、クラスの「初期化引数は〜」のように記述します。
7.1.2 初期化引数の有効性の宣言
初期化引数は、4つの状況にて有効性がチェックされます。
初期化引数はひとつの状況では有効かもしれませんが、他はそうではないかもしれません。
例えば、システムで提供されたmake-instance
のメソッドのstandard-class
クラスを対象とした場合を考えます。
もし初期化引数が与えられていたものの、有効性としての宣言がされていなかった場合、
メソッドは初期化引数の有効性チェックにおいてエラーのシグナルを発することになります。
初期化引数の有効性の宣言は、次の2つの意味があります。
スロットを設定するときの初期化引数は、
defclass
のスロットオプションである:initarg
によって有効であるとして宣言されます。 スロットオプションの:initarg
は、スーパークラスから継承されます。 よって、クラスのスロット設定時の有効な初期化引数の集合は、 クラスとスーパークラスによって有効であると宣言されたスロット設定時の初期化引数の和集合となります。 初期化引数による値の設定は、4つの状況すべてにおいて有効です。メソッドの引数として与えられた初期化引数は、 これらメソッドの宣言によって有効であると定義されます。 メソッドのラムダリストとして定義されたキーワードパラメーターのキーワード名は、 全てのクラスの適用可能なメソッドの初期化引数となります。 適用されるメソッドのラムダリストにある
&allow-other-keys
の存在は、 初期化引数の有効性のチェックを無効にします。 よってメソッドの継承は、メソッドに引数として渡される有効な初期化引数の集合を制御します。 メソッドの定義を持ったジェネリック関数は、下記に示すものとして、 有効な初期化引数の宣言を守ります。関数
allocation-instance
,initialize-instance
とshared-initialize
により、 クラスのインスタンスを作成するとき。 クラスのインスタンスを作成するとき、 これらのメソッドにより有効だと宣言された初期化引数は有効です。関数
reinitialize-instance
とshared-initialize
により、インスタンスの再初期化が行われるとき。 インスタンスの再初期化が行われるとき、 これらのメソッドにより有効だと宣言された初期化引数は有効です。関数
update-instance-for-redefined-class
とshared-initialize
により、 再定義されたクラスにインスタンスを更新するとき。 再定義されたクラスにインスタンスを更新するとき、 これらのメソッドにより有効だと宣言された初期化引数は有効です。関数
update-instance-for-different-class
とshared-initialize
により、 違うクラスの定義にインスタンスを更新するとき。 違うクラスの定義にインスタンスを更新するとき、 これらのメソッドにより有効だと宣言された初期化引数は有効です。
クラスの有効な初期化引数の集合は、スロットの値の設定か、
あるいは初期化引数の前宣言として与えられる:allow-other-keys
に従った
メソッドの引数かのどちらかの初期化引数の集合です。
:allow-other-keys
のデフォルト値はnil
です。
もし初期化引数:allow-other-keys
の値がtrue
であるならば、
初期化引数の有効性の確認は無効となります。
7.1.3 初期化引数のデフォルト値
クラスオプションである:default-initargs
を使うことで、
初期化引数のデフォルト値フォームを提供することができます。
もしいくつかのクラスによって初期化引数が有効であると宣言された場合は、
デフォルト値フォームは違うクラスによって設定されるかもしれません。
このような場合では、:default-initargs
は継承された初期化引数によって提供されたデフォルト値が使用されます。
オプション:default-initargs
は、初期化引数へのデフォルト値の提供のみに使用されます。
このオプションでは、シンボルを有効な初期化引数の名前として宣言しません。
さらに、オプション:default-initargs
は、
インスタンス作成時における初期化引数のデフォルト値の提供としてのみ使用されます。
クラスオプションの引数である:default-initargs
は、
初期化引数の名前とフォームが交互に現れるリストです。
各フォームは、初期化引数に対応するデフォルト値のフォームです。
初期化引数のデフォルト値のフォームは、
make-instance
の引数に初期化引数として現れていなかった場合、
かつ、もっと特定的なクラスによってデフォルト値が定義されていなかった場合のみに、
評価されて使用されます。
デフォルト値のフォームは、defclass
フォームのレキシカル環境で評価されたものが提供され、
評価された結果は初期化引数の値として使用されます。
make-instance
に指定された初期化引数は、
デフォルトの初期化引数と結び付けられ、
デフォルト初期化引数リストを生成します。
デフォルト初期化引数は、初期化引数の名前と値を交互にリストにしたものです。
このリストは、指定されていない初期化引数のデフォルト値を決定するものであり、
また明示的に初期化引数が指定されたものは、
デフォルト初期化引数リストのより早く表れたもののリストとします。
デフォルト初期化引数は、クラス優先リストの順番に従ったクラスのデフォルト値に順番付されます。
:default-initargs
と:initform
では、どちらもスロットの初期化に使用されますが、
両者の間には目的に違いがあります。
クラスオプションである:default-initargs
は、ユーザーに対して、
初期化引数がスロットを初期化されているかどうか、
あるいはメソッドに渡されるかどうかを知ることなしに、デフォルト値を与える仕組みを提供します。
もしmake-instance
を呼ぶ際に、初期化引数を明示的に与えなかった場合はデフォルト値が使用されますが、
デフォルト値は呼び出し時に指定されたものとして呼び出されます。
対称的に、スロットオプションである:initform
は、
ユーザーがスロットのデフォルト値フォームを与えるための仕組みとして提供されます。
:initform
フォームはスロットの初期化に使用されますが、
ただmake-instance
に与えられた初期化引数に対応するスロットとの結びつきがなかった場合、
あるいは:default-initargs
にデフォルト値の指定がなかった場合のみ、:initform
にて初期化が行われます。
初期化引数のデフォルト値フォームの評価順序と、
:initform
フォームの評価順序は定義されてはいません。
もし評価順序が重要である場合は、
代わりにinitialize-instance
かshared-initialize
メソッドを使用してください。
7.1.4 初期化引数の規則
スロットオプションの:initarg
は、スロット対して複数定義されるかもしれません。
もし初期化引数に複数の定義がされるかもしれないときには、下記に示すルールが適応されます。
もし同じ初期化引数の名前が
:initarg
スロットオプションに複数現れた場合は、 初期化引数は複数のスロットを初期化できます。初期化引数の名前は、複数の初期化メソッドのラムダリストに現れます。
初期化引数の名前は、スロットオプションの
:initarg
と、初期化メソッドのラムダリストの両方に現れます。
もしmake-instance
に与えられた引数が、同じスロットを初期化するような複数の初期化引数であった場合、
さらに初期化引数が違った名前であったときは、初期化引数リストの最も左の初期化引数の値が採用されます。
もし複数の違った初期化引数が同じスロットを初期化する場合、
さらにスロットはデフォルト値を持っており、
make-instance
の引数には明示的に指定されていなかったときは、
初期化引数は最も特定的なクラスのクラスオプション:default-initargs
に現れる値が採用されます。
もしひとつの:default-initargs
クラスオプションが、
複数の初期化引数により同じスロットを初期化する場合、
さらにmake-instance
の引数には明示的に指定がなかったときは、
クラスオプション:default-initargs
の最も左側の値が採用され、
残りのデフォルト値フォームの値は無視されます。
make-instance
の引数として明示的に与えられた初期化引数は、
デフォルト初期化引数の左側に現れます。
例えば、クラスC1
とC2
が違うスロットに対してデフォルト初期化引数の値を与えた場合を考えます。
C1
はC2
よりも特定的であるとします。
C1
によって提供されたデフォルト初期化引数は、
デフォルト初期化引数リストにおいては、
C2
によって提供されたのデフォルト初期化引数の左側に位置します。
もし単一のクラスオプション:default-initargs
が、
2つの違ったスロットに対して初期化引数の値が与えられた場合、
クラスオプション:default-initargs
の最も左に位置する初期化引数が、
デフォルト初期化引数リストの最も左側に現れます。
もしスロットが:initform
フォームと:initarg
スロットオプションの両方を持っており、
さらに初期化引数が:default-initargs
によるデフォルト値により与えられているか、
あるいはmake-instance
の引数により与えられていた場合、
:initform
フォームは使われませんし評価もされません。
上記の規則の例を示します。
(defclass q () ((x :initarg a))) (defclass r (q) ((x :initarg b)) (:default-initargs a 1 b 2))
フォーム デフォルト初期化引数リスト スロットXの値 ---------- (make-instance 'r) (a 1 b 2) 1 (make-instance 'r 'a 3) (a 3 b 2) 3 (make-instance 'r 'b 4) (b 4 a 1) 4 (make-instance 'r 'a 1 'a 2) (a 1 a 2 b 2) 1
7.1.5 Shared-Initialize
ジェネリック関数shared-initialize
は、
インスタンスの作成時、インスタンスの再初期化時、
クラス再定義によるインスタンス更新時、違うクラスへのインスタンス更新時において、
インスタンスのスロット値を、初期化引数か:initform
フォームによって設定する際に使用されます。
method-combinationはstandard
が使用されます。
引数は次のような順番で受け取ります。
初期化されるインスタンス、インスタンスのアクセス可能なスロット名の集合、
そして任意の長さの初期化引数です。
最初2つよりあとの引数は、初期化引数リストの形にしなければなりません。
shared-initialize
の2番目の引数は、下記のどちらかに従います。
引数はスロットの名前のリスト(空リストでも可)であり、 スロット名の集合を指定したものです。
引数はシンボル
t
であり、すべてのスロットの集合を指定したものです。
システムが提供しているshared-initialize
のメソッドでは、
第一引数の特定パラメーターがstandard-object
クラスのものが存在します。
このメソッドは共有か局所かに関わらず、各スロットに対して次の振る舞いを行います。
もし初期化引数リスト中の初期化引数がスロットへの値を特定した場合は、 メソッドが実行する前に対象のスロットにすでに値が格納されていても、 スロットへ特定した値が格納されます。 影響があるスロットは、
shared-initialize
の第二引数で指定されたスロットとは独立しています。第二引数によって指定されたどんなスロットも、 この時点においてまだ
unbound
であった場合は、:initform
フォームに従って初期化されます。:initform
フォームを持つどのスロットも、 フォームはdefclass
宣言のレキシカル環境にて評価され、 その結果がスロットに格納されます。 例えば、before
メソッドがスロットへ値を格納する場合、:initform
フォームはスロットへの値の格納には使用されないでしょう。 もし第二引数が指定した名前が、 インスタンスのアクセス可能なスロットに対応していなかった場合は、 結果は定義されていません。この規則は7.1.4 初期化引数に従います。
ジェネリック関数shared-initialize
は、
システムが提供するメソッドreinitialize-instance
,
update-instance-for-different-class
, update-instance-for-redefined-class
,
そしてinitialize-instance
によって呼び出されます。
このようにメソッドは
これらすべてのコンテキスト上で実行できるようなアクションを指定するように、
shared-initialize
を記述することができます。
7.1.6 Initialize-Instance
ジェネリック関数initialize-instance
は、
新しく作成されたインスタンスを初期化するために、
make-instance
によって呼び出されます。
method-combinationはstandard
が使われます。
initialize-instance
のメソッドは、
単純に初期値をスロットに指定できないような初期化を実行するために定義できます。
初期化中では、次に示したアクションを実行したあとにinitialize-instance
が呼び出されます。
デフォルト初期化引数リストは、 提供された初期化引数リストと各クラスのデフォルト初期化引数を結びつける計算がされます。
デフォルト初期化引数リストの有効性はチェックされます。 もしどの初期化引数の有効として宣言されていなかった場合は、エラーが発せられます。
新しいインスタンスはスロットが
unbound
として作成されます。
ジェネリック関数initialize-instance
は
新しいインスタンスとデフォルト初期化引数とともに呼び出されます。
システムが提供するinitialize-instance
のメソッドでは、
特定パラメーターはstandard-object
クラスのものが存在します。
このメソッドは、ジェネリック関数shared-initialize
を呼び出し、
初期化引数に対応したものか、
あるいは:initform
フォームに対応した値を設定します。
ジェネリック関数shared-initialize
の引数は、
インスタンス、t
、デフォルト初期化引数を指定して呼び出されます。
注意として、initialize-instance
はデフォルト初期化引数リストを
shared-initialize
の呼び出し時に提供します。
そして最初のステップとして、
システムが提供するshared-initialize
のメソッドは、
make-instance
呼び出し時に提供された初期化引数と、
デフォルト初期化引数リストの両方を集計して呼び出されます。
initialize-instance
のメソッドは、
インスタンスの初期化時に、特定のアクションを定義することができます。
もしinitialize-instance
のafter
メソッドだけが定義された場合、
これらはシステムが提供した初期化後に実行されます。
したがってこれらは、initialize-instance
の標準的な動作には干渉しないでしょう。
オブジェクトシステムは、initialize-instance
メソッドの構築に便利な2つの関数を提供しています。
関数slot-boundp
は、スロットが値を持っているかどうかを示すbool
値を返却します。
これはinstance-initialize
のafter
メソッドを記述する際に、
まだ初期化されていないスロットのみを初期化するような仕組みを提供します。
関数slot-makunbound
は、スロットの値を削除します。
7.1.7 Make-InstanceとInitialize-Instanceの宣言
ジェネリック関数make-instance
は、最適化を考えない場合は、
下記に示す宣言のように実行されます。
(defmethod make-instance ((class standard-class) &rest initargs) ... (let ((instance (apply #'allocate-instance class initargs))) (apply #'initialize-instance instance initargs) instance)) (defmethod make-instance ((class-name symbol) &rest initargs) (apply #'make-instance (find-class class-name) initargs))
make-instance
の定義で省かれているコードは、
initargs
をデフォルト初期化引数によって指定する部分であり、
また初期化引数の結果を初期化引数に設定するかどうか決定するために、
スロットに値が設定されておらず、
メソッドの引数として供給もされていないかどうかをチェックする部分となります。
ジェネリック関数initialize-instance
は、最適化を考えない場合は、
下記に示す宣言のように実行されます。
(defmethod initialize-instance ((instance standard-object) &rest initargs) (apply #'shared-initialize instance t initargs)))
これらのコードはカスタマイズ可能です。
プログラマーへのインターフェイスレベルとしてカスタマイズできるものは、
defclass
のオプションである、:initform
, :initarg
そして:default-initargs
が含まれますし、
同様にmake-instance
, allocate-instance
, そしてinitialize-instance
のメソッド宣言があげられます。
shared-initialize
のメソッドを定義することも可能です。
この関数は、ジェネリック関数のreinitialize-instance
, update-instance-for-redefined-class
,
update-instance-for-defferent-class
, そしてinitialize-instance
によって実行されます。
メタオブジェクトレベルでは、追加でカスタマイズをサポートします。
処理系は、initialize-instance
とshared-initialize
について明確な最適化を許容しています。
7章にあるshared-initialize
の定義では、可能な最適化についての説明があります。
7.2 インスタンスのクラスの変更
関数change-class
は、
インスタンスのクラスを現在のクラスCfrom
から違うクラスCto
へ変更する際に使用します。
この関数はインスタンスの構造を変化させて、
クラスCto
の定義へ適用させるような変更を行います。
インスタンスのクラスの変更は、
スロットへの追加と削除が行われるでしょう。
インスタンスのクラスの変更は、関数eq
により定義されたものとの同一性を変更しません。
関数change-class
がインスタンスに対して実行されたとき、
2つの手順により更新が行われます。
最初の手順は、新しい局所スロットの追加と、
新しいバージョンには定義されない局所スロットの削除によるインスタンスの構造の変更です。
二番目の手順は、新しく追加された局所スロットの初期化と、
他にユーザーが定義したアクションの実行です。
これら2つの手順は、次に示す2つの章によって定義されています。
7.2.1 インスタンスの構造の修正
インスタンスをクラスCto
へ修正するために、
クラスCto
には定義されているがCfrom
には定義されていない局所スロットは追加され、
クラスCto
には定義されていないがCfrom
には定義されている局所スロットは削除されます。
クラスCto
とクラスCfrom
の両方に定義されている局所スロットの値は保持されます。
もし局所スロットがunbound
であった場合は、unbound
のままです。
クラスCfrom
では共有スロットであり、Cto
では局所スロットとして定義された
スロットの値は保持されます。
最初の更新ステップでは、どの共有スロットの値も影響がありません。
7.2.2 新しく追加された局所スロットの初期化
更新の二番目の手順では、新しく追加されたスロットを初期化し、
ユーザー定義のアクションを実行します。
このステップは、ジェネリック関数update-instance-for-different-class
によって定義されます。
ジェネリック関数update-instance-for-different-class
は、
最初の更新手順が完了したあとに、change-class
によって実行されます。
ジェネリック関数update-instance-for-different-class
は、
change-class
によって計算された引数により実行されます。
最初の引数は、更新されるインスタンスのコピーであり、
クラスCfromの
インスタンスです。
このコピーは、ジェネリック関数change-class
に動的エクステントとして保有されます。
二番目の引数は、change-class
によって更新されるインスタンスであり、
クラスCto
のインスタンスです。残りの引数は、初期化引数リストです。
システムが提供するupdate-instance-for-different-class
メソッドは、
2つの特定パラメーターがあり、
どちらもstandard-object
クラスです。
最初、このメソッドは、初期化引数の有効性をチェックし、
もし指定された初期化引数が有効であると宣言されていなかった場合は、
エラーが発せられます(詳細は7.1.2 初期化引数の有効性の宣言を参照)。
それから、このメソッドはジェネリック関数shared-initialize
を、
次に示す引数とともに呼び出します。
引数は、新しいインスタンス、新しく追加されるスロット名のリスト、
そして受け取った初期化引数です。
7.2.3 インスタンスのクラスの更新のカスタマイズ
update-instance-for-different-class
のメソッドは、
インスタンスを更新するとき、
特定のアクションを定義することができます。
もしupdate-instance-for-different-class
にafter
メソッドのみが定義された場合は、
これはシステムが提供する初期化のメソッドのあとに実行されます。
よってupdate-instance-for-different-class
の標準的な動作には干渉しないでしょう。
shared-initialize
のメソッドは、クラスの再定義を
カスタマイズするために定義されるでしょう。
詳細は7.1.5 Shared-Initializeを参照。
7.3 インスタンスの再初期化
ジェネリック関数reinitialize-instance
は、
初期化引数に従ってスロットの値を変更するときに使用されます。
再初期化のプロセスにより、スロットの値が変更され、
ユーザーが定義するアクションが実行されます。
これはスロットの追加と削除といったインスタンスの構造の修正は行いません。
また、:initform
フォームを使ったスロットの初期化を行いません。
ジェネリック関数reinitialize-instance
は、直接呼び出されるでしょう。
これは引数に一つのインスタンスが要求されます。
またreinitialize-instance
かshared-initialize
によって使用される、
任意の数の初期化引数を受け取ります。
要求されるインスタンスの引数より後の引数は、初期化引数リストの形式でなければなりません。
システムが提供するreinitialize-instance
のメソッドは、
特定パラメーターにstandard-object
クラスを取ります。
最初、メソッドは初期化引数の有効性をチェックし、
もし指定された初期化引数が有効であると宣言されていなかった場合は、
エラーが発せられます(詳細は7.1.2 初期化引数の有効性の宣言を参照)。
このメソッドは、ジェネリック関数shared-initialize
を、
次に示す引数とともに呼び出します。
引数は、インスタンス、nil
、そして受け取った初期化引数です。
7.3.1 再初期化のカスタマイズ
メソッドreinitialize-instance
は、インスタンスを更新するとき、
特定のアクションを定義することができます。
もしreinitialize-instance
にafter
メソッドのみが定義された場合は、
メソッドはシステムが提供する初期化のメソッドのあとに実行されます。
よってreinitialize-instance
の標準的な動作に干渉しないでしょう。
shared-initialize
のメソッドは、クラスの再定義を
カスタマイズするために定義されるでしょう。
詳細は7.1.5 Shared-Initializeを参照。
7.4 メタオブジェクト
オブジェクトシステムの実装は、クラス、メソッド、そしてジェネリック関数を扱います。 オブジェクトシステムは、メソッドとクラスによって定義された、 ジェネリック関数の集合を含みます。これらのジェネリック関数の振る舞いは、 オブジェクトシステムの振る舞いを定義します。 これらのメソッドが定義されているクラスのインスタンスは、メタオブジェクトと呼ばれます。
7.4.1 標準メタオブジェクト
オブジェクトシステムは、標準メタオブジェクトと呼ばれる
メタオブジェクトの集合を提供します。
これらはstandard-object
クラスと、
standard-method
, standard-generic-function
, method-combination
の
それぞれのクラスのインスタンスを含みます。
standard-method
クラスは、defmethod
とdefgeneric
フォームによって 定義されるメソッドの標準クラスです。standard-generic-function
クラスは、defmethod
,defgeneric
,defclass
のフォームによって 定義されるジェネリック関数の標準クラスです。standard-object
という名前のクラスは、standard-class
クラスのインスタンスです。 またstandard-object
は、自分自身とstructure-class
を除く、standard-class
のインスタンスである全てのクラスのスーパークラスです。
すべてのmethod-combinationオブジェクトは、
method-combination
クラスのサブクラスのインスタンスです。
7.5 スロット
7.5.1 スロットの紹介
standard-class
がメタクラスのオブジェクトは、
0個かそれ以上の名前の付いたスロットを持ちます。
オブジェクトのスロットは、オブジェクトのクラスによって決められます。
各スロットは、値を保有できます。
スロットの名前は、変数名として使うのに有効な構文のシンボルです。
スロットが値を持っていないときは、そのスロットはunbound
であると言われます。
もしunbound
のスロットを読み込んだ場合は、
ジェネリック関数のslot-unbound
が呼び出されます。
システムが提供するslot-unbound
のメソッドでは、
引数の特定パラメーターがt
クラスのものが提供されており、エラーが発せられます。
もしslot-unbound
が値を返却する場合は、
第一返却値はスロットの値として、そのときに使用されるものとなります。
スロットのデフォルト値フォームは、
スロットオプション:initform
によって定義されます。
:initform
フォームに値が提供された場合は、
defclass
が評価された中のレキシカル環境にてフォームが評価されます。
defclass
が評価された中のレキシカル環境に沿った:initiform
のことを、
補足された初期化フォームと呼びます。
詳細は7.1 オブジェクトの作成と初期化を参照。
局所スロットとして定義されたスロットは、 正確に一つのインスタンスがアクセス可能です。 すなわち、唯一つのスロットが確保されます。 共有スロットとして定義されたスロットは、 クラスとそのサブクラスによって与えられる、複数のインスタンスから見ることができます。
defclass
フォームによるクラスが、スロット特定子に名前を含んでいたとき、
クラスは名前が与えられたスロットが定義されたと言います。
局所スロットの宣言では、即座にはスロットが作成されません。
なぜならクラスのインスタンスが作成されるときに、スロットが作成されるからです。
共有スロットは宣言では、即座にスロットを作成します。
defclass
のスロットオプション:allocation
は、スロットの定義時に種類を指定します。
もしスロットオプション:allocation
の値が:instance
ならば、局所スロットが作成されます。
もしスロットオプション:allocation
が:class
ならば、共有スロットが作成されます。
もしスロットがインスタンスのクラスによって定義された場合、 あるいはクラスのスーパークラスから継承された場合は、 スロットはクラスのインスタンスからアクセス可能であると言います。 インスタンスからは、せいぜい一つの名前付きスロットがアクセス可能です。 クラスによって定義された共有スロットは、 クラスのすべてのインスタンスからアクセス可能です。 スロットの継承による詳細な説明は、7.5.3 スロットの継承とスロットオプションを参照。
7.5.2 スロットへのアクセス
スロットは次の2つの方法にてアクセスできます。
関数slot-value
を使用する方法、
そしてdefclass
フォームによって生成されるジェネリック関数を使用する方法です。
関数slot-value
は、
defclass
フォームで対象のクラスのインスタンスにて
アクセス可能に設定したスロットに対しては、
どんなスロットの名前でも指定してアクセスすることができます。
マクロdefclass
は、スロットの読み書きをするメソッドを生成するための構文が提供されています。
もしreader
のメソッドが要求された場合、
スロットの値を読むためのメソッドが自動的に生成されますが、
ただし値を格納するためのメソッドは生成されません。
もしwriter
のメソッドが要求された場合、
スロットの値を書き込むためのメソッドが自動的に生成されますが、
ただし値を読み込むためのメソッドは生成されません。
もしaccessor
のメソッドが要求された場合は、
スロットの値を読むためのメソッドと、
スロットの値を書き込むためのメソッドが自動的に生成されます。
reader
とwriter
のメソッドは、slot-value
を使用して実装されます。
スロットに対してreader
かwriter
のメソッドを指定するときは、
ジェネリック関数の名前とそれに沿って生成されるメソッドの名前を直接指定します。
もしwriter
メソッドの名前をシンボルname
に指定した場合、
スロットへ書き込むためのジェネリック関数の名前がシンボルname
となり、
そのジェネリック関数の引数は、
新しい値、インスタンスの順に2つ取ります。
もしaccessor
メソッドの名前をシンボルname
に指定した場合、
スロットから読み込むためのジェネリック関数の名前がシンボルname
となり、
そしてスロットへ書き込むためのジェネリック関数の名前が、リストの(setf name)
となります。
スロットオプションの:reader
, :writer
, :accessor
の指定により、
作成か修正が行われたジェネリック関数は、
正確に普通のジェネリック関数として扱うことができます。
注意として、slot-value
はスロットから値を読み込むか書き込む時に使われますが、
そのスロットのreader
かwriter
のメソッドが存在するかどうかに関わらず使用できます。
slot-value
が使われる時、reader
かwriter
のメソッドは実行されません。
マクロwith-slots
は、
指定されたスロットがレキシカル変数として使えるようにするレキシカル環境を確立します。
マクロwith-slots
は、指定したスロットへアクセスするために関数slot-value
を実行します。
マクロwith-accessors
は、
指定したスロットがレキシカル変数としてスロットのaccessor
を通して
使えるようにするレキシカル環境を確立します。
マクロwith-accessors
は、指定したスロットへアクセスするために、
適切なaccessor
を実行します。
7.5.3 スロットの継承とスロットオプション
クラスC
のインスタンスについて、アクセス可能な全てのスロットの名前の集合は、
クラスC
とそのスーパークラスによって定義されたスロットの名前の集合の和集合となります。
インスタンスの構造は、そのインスタンスの局所スロットの名前の集合です。
単純な場合として、たった1つのクラスC
と、そのスーパークラスにて、
名前ありのスロットを定義したとします。
もしスロットがC
のスーパークラスによって定義された場合、
そのスロットは継承されたと言えます。
スロットの特性は、クラス定義のスロット指定子によって決定されます。
スロットS
を定義したクラスを考えます。
もしスロットオプション:alocation
が:instance
ならば、S
は局所スロットであり、
C
の各インスタンスはS
と名前の付いた独自のスロットをもち、
S
には独自の値が格納されます。
もしスロットオプション:allocation
が:class
ならば、
S
は共有スロットであり、S
が定義されたクラスに値が格納されます。
そしてC
の全てのインスタンスは、その1つのスロットにアクセスできます。
もしスロットオプション:allocation
が省略された場合は、:instance
が使用されます。
一般的に、複数のクラスである、C
とそのスーパークラスは、
1つの名前付きのスロットを定義できます。
そのような場合、C
のインスタンスに対しては、
与えられた名前に対してただ1つのスロットがアクセス可能です。
そしてそのスロットの特性は、いくつかのスロットの指定子を
計算によって結び付けたものになります。
計算方法を次に示します。
1つのスロットの名前に対する全てのスロット指定子は、 クラス
C
のクラス優先順位リストのクラスに従って、 最も特定的なものからそうでないものへ順序付けられます。 どのスロット指定子の直下に特定されるかの全ての参照は、この順序に従って調査されます。スロットの確保は、最も特定的なスロット指定子によって制御されます。 もし最も特定的なスロット指定子がスロットオプション
:allocation
を含んでいなかった場合は、:instance
が使用されます。 特定的ではないスロット指定子は、確保には影響しません。スロットのデフォルト初期値フォームは、 スロットオプション
:initform
が含まれる最も特定的なスロット指定子の、:initform
の値になります。 もしスロット指定子が:initform
を持っていなかった場合、 スロットはデフォルト初期値フォームを持ちません。スロットの値は、常に型
(and T1 ... Tn)
となるでしょう。 ここでT1 ... Tn
とは、全てのスロット指定子が持っているスロットオプション:type
の値です。 もしスロット指定子がスロットオプション:type
を持っていなかった場合は、 スロットの値は常に型t
となります。 スロットの型に合っていない値をスロットに格納しようとした結果については未定義です。指定したスロットを初期化する際に使用する初期化引数の集合は、 全てのスロット指定子の中のスロットオプション
:initarg
で定義された初期化引数の和集合です。スロットのドキュメント文字列は、 スロットオプション
:documentation
が含まれる最も特定的なスロット指定子の、:documentation
の値になります。 もしスロット指定子が:documentation
を持っていなかった場合、 スロットはドキュメント文字列を持ちません。
スロットの確保の規則では、共有スロットはシャドウすることができます。
例えば、もしクラスC1
が、スロットの名前S
、
スロットオプション:allocation
が:class
のスロットを定義した場合、
そのスロットは、C1
とその全てのサブクラスのインスタンスからアクセス可能です。
しかし、もしC2
がC1
のサブクラスであり、C2
が名前S
のスロットを定義した場合、
C2
とその全てのサブクラスのインスタンスでは、C1
のスロットは共有されません。
クラスC1
が共有スロットを定義したときは、
次の条件の時にC1
のどんなサブクラスC2
でもその単一のスロットは共有されます。
それは、C2
のdefclass
フォームで同じ名前のスロットを定義していないとき。
あるいは、C2
のクラス優先リスト内において
同じ名前のスロットを定義しているクラスを見たとき、
C1
よりも先導しているものがC2
のスーパークラスに存在していない場合です。
型の規則による結果は、スロットの値が関連するスロットの 各スロット指定子の型の条件を満たすことです。 スロットの型の条件が守られていない値を スロットに格納しようとした際の結果は未定義なので、 スロットの値は型の条件の安全性を失うでしょう。
スロットオプション:reader
, :writer
, :accessor
は、
スロットの特性を宣言すると言うよりは、
メソッドを作成するものです。
reader
とwriter
メソッドは、
7.6.7 メソッドの継承で説明される定義により継承されます。
スロットにアクセスするメソッドは、スロットの名前と、 スロットの値の型のみを使用します。 例えば、スーパークラスが、 指定した名前により共有スロットにアクセスすることを期待するメソッドを提供した場合、 またサブクラスが同じ名前で局所スロットを定義した場合を考えます。 もしスーパークラスによって提供されたメソッドを、 サブクラスのインスタンス上で使用した場合、 メソッドは局所スロットにアクセスします。
7.6 ジェネリック関数とメソッド
7.6章は7章Objectsのジェネリック関数関連の和訳を参照。
subtypep実装の基本
正直、あっているかどうかわからないのですが、
確認のために記載していこうと思います。
今回の投稿だけでは終わらないので、気が向いたら続きを書きます。
subtypep
の使い方
subtypep
とは、左の型が右の型に含まれているかどうかを確認する関数です。
こんな感じで覚えておけばいいと思います。
(subtypep 小 大) のとき t
正確には等しい時もt
となります。
例をあげます。
;; integerはrealに含まれる。 (subtypep 'integer 'real) →t; t ;; stringはstringに含まれる(同じなので) (subtypep 'string 'string) →t; t
返却値は二つありますが、第二返却値がnil
の場合は
判断できなかったという意味になります。
型satisfies
を用いると、たぶん大体はnil; nil
となります。
(subtypep 'integer '(satisfies hello)) →nil; nil
実験では、hello
という関数が存在しなくてもnil; nil
が返却されました。
つまり関数を実行すらしていないということです。
なお、第二返却値がnil
の場合は、必ず第一返却値はnil
です。
t; nil
のような返却のパターンはありません。
よって、返却値は下記の3通りとなります。
第1 | 第2 | 意味 |
---|---|---|
t |
t |
含まれる |
nil |
t |
含まれない |
nil |
nil |
わからない |
subtypep
の実装はとても難しいです。
理由は、実数の範囲指定、and
、or
、not
があるからです。
範囲指定とは例えば次のようなことを言います。
(subtypep '(integer 10 20) '(real * 40)) →t; t
整数10~20
は、実数40
以下に含まれます、という回答になります。
and
, or
, not
はそのままなので、
あらためて説明する必要はないかと思います。
例えばこんな感じ。
(subtypep 'integer '(not character)) →t; t (subtypep '(integer 30 80) '(or (real 10 50) (real 40 100))) →t; t
範囲指定とand
, or
, not
を一気に説明するのは無理なので、
今回はnot
から説明していきます。
subtypep
のnot
型not
とは、その名の通り反対を意味します。
subtypep
にnot
が含まれる場合はどうなるかわかるでしょうか?
例えば下記の例を考えます。
(subtypep 'integer 'real) →t; t
含まれる場合は、not
にすると、返却値はたぶんnil
になるのではないでしょうか。
(subtypep '(not integer) 'real) →nil; t
あっていました。
じゃあ右をnot
にすると?
(subtypep 'integer '(not real)) →nil; t
こっちもあっていますね。
では次の例を考えましょう。
(subtypep 'string 'real) →nil; t
元々の返却値がnil
の場合は、否定したらt
になるのでしょうか?
(subtypep '(not string) 'real) →nil; t
なりませんでした。
でも右をnot
にするとt
になります。
(subtypep 'string '(not real)) →t; t
じゃあ、もともとnil
の場合は、右をnot
にすると返却値は必ずt
になるのか?
と言うと、そうでもありません。
(subtypep 'real 'integer) →nil; t (subtypep 'real '(not integer)) →nil; t
どういうこと?
これを理解するには、型の範囲をちゃんと考えなければいけません。
次の2例を考えます。
(subtypep 'string 'real) →nil; t (subtypep 'real 'integer) →nil; t
どちらも返却はnil
ですが、分けて考える必要があります。
上のstring
とreal
は、重複することが全くない、完全なる排他です。
しかし下のreal
とinteger
は、重複する部分があるものです。
両者が完全に排他の場合、右を否定した場合のみt
となります。
これは図にするとわかりやすいのではないでしょうか。
例えば、例として含まれる範囲を|****|
で表すとしましょう。
(subtypep 'string 'real) string: ------|****|---------------- real : -----------------|****|----- ★互いに疎で含まれない
分かってもらえるでしょうか。
横軸はてきとうです。
こんな感じだとして、|****|
の部分がお互いに含まれないと表現します。
real
のnot
を取ると次のようになります。
(subtypep 'string '(not real)) string : ------|****|---------------- (not real): *****************|----|***** ★|ここ|★が含まれる
こうすると、string
の|****|
の部分が、(not real)
に
完全に含まれています。
一方、real
とinteger
は次のようになります。
(subtypep 'real 'integer) real : ------|*****************|--- integer: -----------------|****|----- ★realの方が大きいのでnil
real
とinteger
は互いに疎ではなく、重複する部分があります。
よってnot
を取ると次のようになります。
(subtypep 'real '(not integer)) real : ------|*****************|--- (not integer): *****************|----|*****
(not integer)
はreal
を包括できていません。
こんなふうに、subtypep
では、nil
を返却する場合は、
排他的なのか、そうじゃないのかをちゃんと調べないと、
not
の場合に対応することができません。
subtypep
でnot
の有無を網羅すると、パターンは次の4通りになります。
(subtypep 'a 'b) (subtypep '(not a) 'b) (subtypep 'a '(not b)) (subtypep '(not a) '(not b))
このうちで、もともとnil
だったものがt
になるパターンは次の2通りです。
(subtypep 'a '(not b))
a
とb
が排他的の場合はt
(subtypep '(not a) '(not b))
a
がb
を含む場合はt
下の場合の、2つがどちらもnot
の判定は楽です。
a
とb
を逆にした(subtypep 'b 'a)
を調べればいいだけですから。
問題は排他的の場合です。
これは計算だったりアルゴリズムで算出できるものではなく、
実装する段階で、これこれのパターンは排他ですよという情報を
自分で返却するようにしなければなりません。
どうやって実装するのかというと、地道にif
文を並べて作りましょう。
subtypep
実装の基本の第一段階は、
not
, and
, or
が含まれない場合の関数subtypep!
を作成し、
次の4パターンを返却をさせることです。
(subtypep! 'a 'b) :true 含まれる t; tに対応 :false 含まれない nil; tに対応 :exclude 排他 nil; tに対応 :invalid わからない nil; nilに対応
では、:exclude
を考慮して、not
が現れたらt
とnil
を返却するような
subtypep!
を作ればいいのかと思うかもしれませんが、
それだと機能が足りません。
:exclude
を考慮して、:true
, :false
, :exclude
(と:invalid
)を返却するような
subtypep!
を作成するのです。
これらの値は、のちに作成するand
, or
でも使用します。
では、ひとまずand
, or
がない場合の
『not
ありsubtypep!
』のアルゴリズムを示します。
(subtypep! a b)
の場合 (★not
なしの場合)(subtypep! a b)
をそのまま返却
(subtypep! (not a) b)
の場合(subtypep! b a)
が (★逆です):true
なら:exclude
を返却:exclude
なら:false
を返却- その他ならそのまま返却
(subtypep! a (not b))
の場合(subtypep! a b)
が:true
なら:exclude
を返却:exclude
なら:true
を返却- その他ならそのまま返却
(subtypep! (not a) (not b))
の場合(subtypep! b a)
が (★逆です):true
なら:true
を返却:exclude
なら:false
を返却- その他ならそのまま返却
これでどうでしょうか。
eqlには3種類の意味がある
表題の通り、Common Lispでは下記の意味があります。
- 関数
eql
- 型
eql
eql-specializer
なんでこんなにあるんでしょうね。
兄弟分である、eq
、equal
、equalp
さんたちは1つの意味しかありませんよ?
それぞれ説明していきます。
関数eql
普通はこいつです。
2つのオブジェクトが等しいかどうかを調べるときに使います。
関数eq
より適当ではないものの、関数equal
みたいに
詳しく調査して欲しくないという位置づけだと思います。
厳密に定義されており、下記のどれかが当てはまっているならt
です。
eq
がt
の場合。- どちらも
number
であり、同じ型で同じ値の場合 - どちらも
character
であり、同じ文字の場合
1つめのeq
はそのままアドレスを比較します。
2つめは、同じ型が要求されているので、数値として等しいだけの場合はダメ。
* (eql 0.0 0) →nil * (eql 0.0 0.0) →t * (eql 0.0d0 0.0s0) →nil * (eql 0.0d0 0.0d0) →t
3つめはそのままです。
* (eql #\a #\a) →t * (eql #\a #\A) →nil
型eql
Type-specifier
と呼ばれるものであり、
型という中で、値がeql
かどうかを調査します。
例えば次の通り。
* (typep 10 '(eql 10)) →t * (typep (cons nil nil) `(eql ,(cons nil nil))) →nil
余談ですが、型を評価する際に、内部で別の形式に変形することが良くあります。
例えば
(or (integer 10 200) (integer 100 300))
を
(integer 10 300)
のように変えたりします。
型eql
の場合も
(eql 10)
を
(integer 10 10)
に変形することがあります。
だから何だってわけじゃないんですが。
eql-specializer
これはmethod
を定義するときに使うものです。
method
は、引数に値の特性を指定することができます。
例えば下記の通り。
(defmethod aaa ((a integer) (b string)) ...)
この場合は、第一引数a
にinteger
を、第二引数b
にstring
を指定しています。
注意して欲しいのは、このinteger
やstring
と記述しているのは、
「型」ではなく「class
およびeql-specializer
」です。
下記の例の引数は、型(eql 10)
ではなく、eql-specializer
の指定です。
(defmethod bbb ((a (eql 10))) ...)
何が違うんだとしか思えないでしょう。
integer
と書けて、(eql 10)
と書けるなら、型じゃないのか?
でも型じゃないんです。
型が指定できないので
(defmethod bbb ((a (satisfies oddp))) ...)
や
(defmethod bbb ((a (not (eql 10)))) ...)
はエラーです。
こいつもエラーなので注意。
(defmethod bbb ((a (real 10 20))) ...)
じゃあinteger
とstring
は型じゃないのか?
と思われるかもしれませんが、型じゃありません。
型のふりをしたclos
オブジェクトです。
clos
の世界とlisp
の世界をある程度似せるために、
symbol
だけで表記された標準の型については、
同じclass
が用意されている場合があります。
何が用意されているのかは処理系依存ですが、
find-class
関数を使うことで調査できます。
clisp
の例を示します。
(find-class 'integer nil) →#<BUILT-IN-CLASS INTEGER> (find-class 'atom nil) →nil
なお、eql-specializer
の引数は、
defmethod
宣言時に一度だけ評価されます。
method
が呼び出されるたびに評価されるわけではありません。
例を示します。
呼び出すたびに値が増えていく関数hello
を作成します。
(let ((value 0)) (defun hello () (incf value)))
generic-function
のaaa
を作成します。
(defgeneric aaa (arg))
関数hello
をeql-specializer
の引数として、method
を定義します。
;; ここで(hello)は1を返却 (defmethod aaa ((arg (eql (hello)))) :hello) →#<STANDARD-METHOD ((EQL 1))>
実行してみます。
;; eql-specializerが機能している (aaa 1) →:HELLO ;; 何度か実行する (aaa 1) →:HELLO (aaa 1) →:HELLO ;; 確認 (hello) →2 ;; エラー (aaa 2) →no-applicable-method