nptclのブログ

Common Lisp処理系nptの開発メモです。https://github.com/nptcl/npt

sbclのスクリプトファイル作成

FreeBSD, Linuxsbclスクリプトファイルを作成するメモです。

長々と書きますので、結果だけを先に示します。
スクリプトファイルの1行目には次のように記載すると便利ですね。

#!/usr/bin/env -S sbcl --script

スクリプトファイルの作成

sbclには引数--scriptを指定することにより、スクリプトファイルを読み込むことができます。
つまりは次のように呼び出しを行うことができます。

$ sbcl --script script-file.lisp

しかしこのオプションは、おそらく上記のようにコマンドラインで実行するためのものではなく、 スクリプトファイルに組み込んで使うものだと思います。
問題はこれをどのように記載するかです。

Unix系のOSでは、テキストファイルの1行目を#!で開始することで、 スクリプトに渡す実行ファイルを指定することができます。
開始1byteから#で始める必要があるので、UTF-8のBOMありはエラーになるので注意。

例えば次の通り。

#!/usr/bin/sbcl --script
(format t "Hello~%")

実行してみます。

$ cat > test.sh
#!/usr/bin/sbcl --script
(format t "Hello~%")
^D
$ chmod +x test.sh
$ ./test.sh
Hello

うまく行ったならおめでとう!
でも、上記の書き方だとダメな場合があります。

sbclの場所が違う

Linuxだと/usr/bin/sbclですが、FreeBSDでは/usr/local/bin/sbclとなります。
次のように変更することで動作はします。

#!/usr/local/bin/sbcl --script
(format t "Hello~%")

あるいはsymbolic linkを作成するのでもいいと思います。

# ln -s /usr/local/bin/sbcl /usr/bin/sbcl

しかしこれは問題を解決したと言えるのでしょうか?
実行ファイルの場所が違う問題は、sbclに限らずUnix系ではよく生じる問題です。
一般的には/usr/bin/envを用いて解決します。 envは実行するファイルを環境変数PATHから探し呼び出します。
次のような記載を見たことがある人もいると思います。

#!/usr/bin/env perl
...

今の場合はperlではなくsbclですが、 /usr/bin/usr/local/binPATHに登録されているのであれば、 同じように置き換えることで実行できるかもしれません。

#!/usr/bin/env sbcl --script
(format t "★注意:たぶん失敗する~%")

たぶん失敗すると記載したように、これだとうまく行かないかもしれません。
どうもFreeBSD 6.0までは上記でうまく行けたようなのです。
しかし問題があったためkernelに仕様変更が生じました。
今は次のように、引数-Sを記載するのが正しいとのことです。

#!/usr/bin/env -S sbcl --script
(format t "Hello~%")

これはFreeBSDだけではなくLinuxも正しく動作します。

動作確認を行う場合は、引数のチェックを含めて行った方が良いです。
例えば次のスクリプトファイルを用意します。

#!/usr/bin/env -S sbcl --script
(format t "~S~%" sb-ext:*posix-argv*)

スクリプト名をtest.shとしたときの実行結果を下記に示します。

$ ./test.sh
("sbcl")
$ ./test.sh 10 20 30
("sbcl" "10" "20" "30")

sbclに渡す引数を変更したい

スクリプトで実行するsbclの引数を変更したい場合があります。
例えば--coreを指定したい場合はどうするべきでしょうか。

$ ./test.sh --core /path/to/sbcl.core
("sbcl" "--core" "/path/to/sbcl.core")

たぶん目的とは違った結果になってしまいます。
このように、スクリプトの引数に指定しても何の解決にもなりません。

スクリプトに埋め込む

一つの方法は、スクリプトの1行目に埋め込むことです。
例えば次のようなスクリプトファイルを作成します。

#!/usr/bin/env -S sbcl --core /path/to/sbcl.core --script
(format t "~S~%" sb-ext:*posix-argv*)

これはこれで良いのですが、もし移植性を考えるのであれば、 この方法は使用できないでしょう。

実行するsbclを別のものにする

例えば$HOME/bin/上にsbclというスクリプトを作り、 それをPATHに登録する方法です。
実行するsbclそのものを変更するため、 元々のスクリプトには手を入れる必要がありません。

ユーザーが使用するshellによって手順が変わりますが、 今はbashを使っているものとします。
login時にシステムが自動的に$HOME/binPATHに追加してくれるならよいのですが、 たぶん自分で設定する必要があると思います。
次の手順を実施します。

$ cd $HOME
$ mkdir bin
$ chmod 700 bin
$ vi .bashrc
最終行に下記を追記
export PATH="$HOME/bin:$PATH"

$ vi .bash_profile
次の内容を追記
if [[ -r $HOME/.bashrc ]]; then
  source $HOME/.bashrc
fi

次にsbcl本体のスクリプトファイルを作成します。

$ cd $HOME/bin
$ touch sbcl
$ chmod +x sbcl
$ vi sbcl

次の内容で保存します。

#!/bin/sh
/usr/bin/sbcl --core /path/to/sbcl.core "$@"

一度logoutしてからloginしなおします。
次に起動確認を行います。

$ which sbcl
/home/xxx/bin/sbcl
$ sbcl --version
SBCL 1.4.12

引数のチェックを行ったスクリプトを用意します。

#!/usr/bin/env -S sbcl --script
(format t "~S~%" sb-ext:*posix-argv*)

実行確認を行います。

$ ./test.sh
("/usr/bin/sbcl")
$ ./test.sh 10 20 30
("/usr/bin/sbcl" "10" "20" "30")

なお、.bashrc, .bash_profile, .profileあたりのファイルは、 login時、bash実行時、sshなど外部接続時にて、 読み込まれるファイルが違ったりしますので、 必要に応じてチェックしてみてください。

日本のWebサイトから情報を取得

Common Lispにより、Webから特定の情報を取得する方法について考えます。

例えば、あるサイトから自分の住んでいる場所の天気だけを取得するような場合です。
私は日本語しかわからないので、当然日本のサイトを対象にします。
そうなるとエンコードの問題が出てきます。

本投稿では、下記のライブラリを用いてWebからの情報取得を行います。

CL-PPCRE
https://edicl.github.io/cl-ppcre/
Drakma
https://edicl.github.io/drakma/
CL-HTML-Parse
https://www.cliki.net/CL-HTML-Parse
Anaphora
https://common-lisp.net/project/anaphora/

自作のライブラリも使います。

strjis
https://github.com/nptcl/strjis
http://nptcl.hatenablog.com/entry/2019/06/13/024132
unmatch
http://nptcl.hatenablog.com/entry/2019/06/13/132538

処理系はsbclを用います。

方法は、htmlファイルをLispオブジェクトに変換してパターンマッチで検索するというものです。
日本語のサイトを対象とするので、エンコードの取り扱いを行わなければなりません。
まずはstrjisでどのように読み込むかを考えます。
そのあとunmatchで検索を行います。

htmlファイルの取得

URIからhttp経由で情報を取得する場合は、drakmaライブラリを使用します。
http-requestを行う時点でexternal-formatを指定できるのですが、 日本語のサイトの場合はhtmlファイルを読み込まないうちは、 一体どのエンコードで記載されているのかがわかりません。

仕方がないのでhttp-requestはbinaryデータを取得することにします。

(defparamter +uri+ "http://.../")
(drakma:http-request +uri+ :force-binary t)
-> #(...)

エンコードタイプは、htmlファイルの中に、

<meta http-equiv="content-type" content="text/html; charset=Shift_JIS">

のような形で記載されています。
しかし、データを取得する際にはまずエンコード問題を解決しなければなりません。 そこで、取得したbinaryデータを強制的にascii形式に変換することにします。

(setq x (drakma:http-request +uri+ :force-binary t))

(let ((strjis:*recovery-unicode* nil))
  (strjis:coerce-string x :input 'ascii :recovery t))
-> 文字列

変数*recovery-unicode*nilに設定することで、 asciiコード以外の全て文字を削除することができます。
返却された文字列が正しいhtmlファイルだと仮定して、 cl-parse-htmlに読み込ませます。

(cl-html-parse:parse-html
  (let ((strjis:*recovery-unicode* nil))
    (strjis:coerce-string x :input 'ascii :recovery t)))
-> tree

返却値はhtmlの内容を表したtreeとなります。
ここから、contents-typeを取得する方法を考えます。

パターンマッチ

unmatchを使い、tree構造から特定の内容を検索する機能を追加します。

(defun first-match-list (match body)
  (when (consp body)
    (multiple-value-bind (a b) (funcall match body)
      (or a (if b a
              (or (first-match-list match (car body))
                  (first-match-list match (cdr body))))))))

(defun list-match-list (match body)
  (let (ret)
    (labels ((rec (x) (when (consp x)
                        (multiple-value-bind (a b) (funcall match x)
                          (when (or a b)
                            (push a ret)))
                        (rec (car x))
                        (rec (cdr x)))))
      (rec body))
    (nreverse ret)))

(defun call-match-pattern (proc match data body)
  (let ((g (gensym)))
    `(,proc
       (lambda (,g) (unmatch:ifmatch ,match ,g (progn ,@body)))
       ,data)))

(defmacro first-match (match data &body body)
  (call-match-pattern 'first-match-list match data body))

(defmacro list-match (match data &body body)
  (call-match-pattern 'list-match-list match data body))

マクロfirst-matchlist-matchはどちらもifmatchと似ていますが、 パターンマッチの対象をtreeとみなして遡って検索していくことが違います。
マクロfirst-matchは、最初にマッチしたものを処理して返却します。
マクロlist-matchは、マッチしたすべてのものを処理してリストとして返却します。

contents-typeを取得する場合は次のようになります。

(defun meta-content-charset (html)
  (first-match (:meta :http-equiv ?x :content ?y) html
    (when (equalp ?x "content-type")
      ?y)))

cl-html-parseの内容を変数treeとしたとき、 実行結果は例えば次のようになります。

(meta-content-charset tree)
-> "text/html; charset=Shift_JIS"

このように、cl-html-parseunmatchを組み合わせることで、 特定の情報を抽出することができます。

エンコード情報の取得

htmlファイルのbinaryデータから、エンコードタイプを取得するコードを下記に示します。

(defparameter +guess-html-charset-string+
  (cl-ppcre:create-scanner "^.+(?i)charset(?-i)\\s*=\\s*\\\"?(\\S+)\\\"?\\s*$"))
(defun guess-html-charset-string (html)
  (awhen (meta-content-charset html)
    (multiple-value-bind (str group)
      (cl-ppcre:scan-to-strings +guess-html-charset-string+ it)
      (when str
        (elt group 0)))))

(defun guess-html-encoding-string (str)
  (string-upcase
    (remove-if-not #'alphanumericp str)))

(defun guess-html-encoding-windows (str)
  (and (<= 3 (length str))
       (string= (subseq str 0 3) "WIN")))

(defun guess-html-encoding-html (html)
  (awhen (guess-html-charset-string html)
    (let ((str (guess-html-encoding-string it)))
      (cond ((string= str "UTF8") :utf8)
            ((string= str "ASCII") :utf8)
            ((string= str "JIS") :jis)
            ((string= str "ISO2022JP") :jis)
            ((string= str "EUC") :eucjis)
            ((string= str "EUCJP") :eucjis)
            ((string= str "EUCJIS") :eucjis)
            ((string= str "SHIFTJIS") :shiftjis)
            ((string= str "SJIS") :shiftjis)
            ((string= str "CP932") :shiftjis)
            ((string= str "MS932") :shiftjis)
            ((guess-html-encoding-windows str) :shiftjis)))))

(defun guess-html-encoding (x)
  (guess-html-encoding-html
    (cl-html-parse:parse-html
      (let ((strjis:*recovery-unicode* nil))
        (strjis:coerce-string x :input 'ascii :recovery t)))))

使用例を示します。

(setq x (drakma:http-request +uri+ :force-binary t))

(guess-html-encoding x)
-> :SHIFTJIS

htmlファイルの内容取得

エンコードさえ取得できればあとは簡単です。

(defun fetch (uri &key (guess :utf8))
  (let* ((x (drakma:http-request uri :force-binary t))
         (encode (or (guess-html-encoding x) guess)))
    (cl-html-parse:parse-html
      (strjis:coerce-string x :input encode))))

(fetch +uri+)
-> ((:!DOCTYPE ...) (:HTML ...))

エンコード情報の取得は万能ではなく失敗することもあるため、 引数:guessにてデフォルトのエンコードタイプを指定できるようにしています。

では、取得したhtmlからパターンマッチで検索していきます。

検索: 最初にマッチした情報を返却する

次の実行例を考えます。

(first-match ((:form :action _ :method ?x . _) . ?body) (fetch +uri+)
  (when (equalp ?x "post")
    ?body))
-> tree

マッチする内容は、例えば次のようなhtml構文の内容です。

<form action="call.cgi" method="POST" id="callid">
[ここの内容が返却されます]
</form>

first-matchは先頭から検索をしていき、最初にヒットした内容がbody句で処理されます。 パターンマッチの候補が複数あった場合でも、最初の内容だけが対象となります。

検索: マッチした情報を集めてリストで返却

次の実行例を考えます。

(list-match (:a :href ?x . _) (fetch +uri+)
  ?x)
-> list

マッチする内容は、例えば次のようなhtml構文を寄せ集めたリストです。

<a href="ここの内容が返却されます">

例えば下記のようになります。

("/inex.html" "/path/to/" "http://.../")

検索: 最初にマッチした内容から、別の検索で情報を集める

上記二例の複合です。

(first-match ((:form :action _ :method ?x . _) . ?body) (fetch +uri+)
  (when (equalp ?x "post")
    (list-match (:a :href ?x . _) ?body
      ?x)))

最初にマッチした<form...>の内容から、<a href...>を集めてリストとして返却します。

最後に

これらの方法を使うことで、特定の情報を取得することもできますし、 FORMから必要なhidden情報を集めてPOSTしたりすることもできます。 drakmacookieに対応しているため、例えばログインを行ったりすることもできます。 作者はこの方法を使って自動化を行っていたことがありました。

ただし、実際にWebの自動化を行う場合は、 たぶん誰かがすでに作っているであろう専門のライブラリを使った方が楽かもしれません。

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で日本語のテキストを読み書きするライブラリを作成しました。

strjis
https://github.com/nptcl/strjis

下記のエンコードを扱うことができます。

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

Babel
https://common-lisp.net/project/babel/

本ライブラリは次の利点があります。

欠点も色々とあります。

  • 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.txt

JIS 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/doc/readme.ja

動作確認はsbcl, clisp, cclにて行っています。 ただ、これらは全てbase-charが21bit以上のコードを格納できる処理系です。 例えばAllegroCLLispWorksなんかは、一文字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)

入力にopenexternal-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/doc/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を除去します。

utf16utf32は、BOMからbig-endianかlittle-endianを判定します。 BOMが無かったらbig-endianであるとみなします。

unicodeは、char-code-limitの値を見て、適切なタイプを判断します。
もしchar-code-limit255以下ならutf8です。
もしchar-code-limit65535以下ならutf16vです。
それ以外ならutf32vです。

その他

eucjpeucjisは同じです。

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を試してみたんですが、 結果は下記の通り。

  • clispinteger, character, stringを全部を受け付ける
  • sbclintegercharacterのみ
  • cclcharacter, 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を入れています。

でもdefstructreader/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の規約に記載されている 例文を見た方がわかりやすいのではないでしょうか。

下記に例文を抜き出します。

;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のクラス関連の和訳の続きです。

目次

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によって決定が行われます。

通常の関数とジェネリック関数は、同一の構文により呼び出されます。

ジェネリック関数は本物の関数なので、funcallapplyの最初の引数として使用されたり、 あるいは引数を渡したりすることができます。

ジェネリック関数の関数名の設定は、いくつかの手順のひとつとして確立します。 それはグローバル環境内において、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)を満たし、 objectformを評価した結果となります。 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,nP2,1 ... P2,nであったとき。

  • 1≦i≦nの各P1,iP2,iに一致したとき。 特定パラメーターP1,iP2,iに一致したとは、P1,iP2,iが同じクラスであるか、 あるいはP1,i=(eql object1), P2,i=(eql object2), (eql object1 object2)のとき。 それ以外では、P1,iP2,iは一致していません。

  • 2つの限定子のリストが、equalで等しいとき。

7.6.4 ジェネリック関数の全てのメソッドのラムダリストの合意

下記に示すこれらの定義は、ラムダリストの集合の合意を定義します。 ラムダリストには、指定したジェネリック関数の各メソッドのラムダリストと、 ジェネリック関数自身で定義されたラムダリストを含みます。

  1. 各ラムダリストは、同じ数の要求パラメーターを持つ必要があります。

  2. 各ラムダリストは、同じ数の&optionalパラメーターを持つ必要があります。 各メソッドは、&optionalパラメーターに独自のデフォルト値を提供することができます。

  3. もしどれかのラムダリストが&rest&keyを持つなら、 各ラムダリストはそのうちの1つか両方を指定する必要があります。

  4. もしジェネリック関数のラムダリストが&keyを持つなら、 各メソッドは&keyの後の全てのキーワードの名前を受け付けるようにする必要があります。 受け付ける方法は、全ての名前を明に指定する方法、 あるいは&allow-other-keysを指定する方法がありますが、 &keyの指定ではなく&restを指定する方法でも問題ありません。 各メソッドは、独自のキーワード引数を追加で受け付けることができます。 キーワードの名前の有効性のチェックはジェネリック関数が行い、各メソッドでは行いません。 メソッドが実行されたときには、キーワード引数に名前が:allow-other-keys、 値がtrueであるペアが与えられたように呼び出されますが、 そのような引数のペアは渡されません。

  5. &allow-other-keysの使用は、ラムダリスト間で一貫している必要はありません。 もし&allow-other-keysジェネリック関数か適用メソッドのラムダリストに指定されている場合、 ジェネリック関数が呼び出されるときには、どんなキーワード引数も受け付けるでしょう。

  6. &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-classcharacter-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ステップによって決定されます。

  1. 適用可能なメソッドの選択します。

  2. 優先順位によって適用可能なメソッドをソートし、最も特定的なメソッドを最初に選択します。

  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インスタンスによって呼び出されるメソッドの相対的な順番には影響しません。 クラスC1beforeメソッドは、クラスC2の全てのメソッドの前に実行されます。 クラスC1afterメソッドは、クラス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-methodnext-method-pの関数の使用が提供されます。

  • プライマリメソッドは、ただひとつの限定子としてmethod-combinationの名前を持ちます。 例えば、組み込みのmethod-combinationタイプのandは、 ただひとつの限定子であるandを指定したメソッドをプライマリメソッドとして認識します。 call-next-methodnext-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を適用します。 例えばoperatororのとき、式<Mi a1...an>は、 ただ<Mj a1...an>, 1≦j<inilを返却したときのみに評価されます。

プライマリメソッドのデフォルトの順番は:most-specific-firstです。 しかし、:method-combinationオプションの2番目の引数に :most-specific-lastを指定したときは逆順にすることができます。

シンプルな組み込みmethod-combinationタイプは、 メソッドに対して正確に1つの限定子を要求します。 もし限定子が存在しない適用可能なメソッドか、 あるいはmethod-combinationタイプが認識しない限定子を指定したときはエラーが発せられます。 もしaroundメソッドが存在するものの、プライマリメソッドが存在しない場合は、エラーが発せられます。

7.6.7 メソッドの継承

サブクラスは、クラスの全てのインスタンスのどんな適用可能なメソッドも継承します。 また、クラスのどんなサブクラス全てのインスタンスにも適用可能です。

メソッドの継承は、メソッド定義オペレーターによって作成されたメソッドであっても、 同じ方法によって行われます。

メソッドの継承の詳細は、7.6.6 メソッドの選択とコンビネーションに記載されています。

7章Objectsのクラス関連の和訳

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-instanceshared-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-instanceshared-initializeにより、 クラスのインスタンスを作成するとき。 クラスのインスタンスを作成するとき、 これらのメソッドにより有効だと宣言された初期化引数は有効です。

    • 関数reinitialize-instanceshared-initializeにより、インスタンスの再初期化が行われるとき。 インスタンスの再初期化が行われるとき、 これらのメソッドにより有効だと宣言された初期化引数は有効です。

    • 関数update-instance-for-redefined-classshared-initializeにより、 再定義されたクラスにインスタンスを更新するとき。 再定義されたクラスにインスタンスを更新するとき、 これらのメソッドにより有効だと宣言された初期化引数は有効です。

    • 関数update-instance-for-different-classshared-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-instanceshared-initializeメソッドを使用してください。

7.1.4 初期化引数の規則

スロットオプションの:initargは、スロット対して複数定義されるかもしれません。

もし初期化引数に複数の定義がされるかもしれないときには、下記に示すルールが適応されます。

  • もし同じ初期化引数の名前が:initargスロットオプションに複数現れた場合は、 初期化引数は複数のスロットを初期化できます。

  • 初期化引数の名前は、複数の初期化メソッドのラムダリストに現れます。

  • 初期化引数の名前は、スロットオプションの:initargと、初期化メソッドのラムダリストの両方に現れます。

もしmake-instanceに与えられた引数が、同じスロットを初期化するような複数の初期化引数であった場合、 さらに初期化引数が違った名前であったときは、初期化引数リストの最も左の初期化引数の値が採用されます。

もし複数の違った初期化引数が同じスロットを初期化する場合、 さらにスロットはデフォルト値を持っており、 make-instanceの引数には明示的に指定されていなかったときは、 初期化引数は最も特定的なクラスのクラスオプション:default-initargsに現れる値が採用されます。 もしひとつの:default-initargsクラスオプションが、 複数の初期化引数により同じスロットを初期化する場合、 さらにmake-instanceの引数には明示的に指定がなかったときは、 クラスオプション:default-initargsの最も左側の値が採用され、 残りのデフォルト値フォームの値は無視されます。

make-instanceの引数として明示的に与えられた初期化引数は、 デフォルト初期化引数の左側に現れます。 例えば、クラスC1C2が違うスロットに対してデフォルト初期化引数の値を与えた場合を考えます。 C1C2よりも特定的であるとします。 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-instanceafterメソッドだけが定義された場合、 これらはシステムが提供した初期化後に実行されます。 したがってこれらは、initialize-instanceの標準的な動作には干渉しないでしょう。

オブジェクトシステムは、initialize-instanceメソッドの構築に便利な2つの関数を提供しています。 関数slot-boundpは、スロットが値を持っているかどうかを示すbool値を返却します。 これはinstance-initializeafterメソッドを記述する際に、 まだ初期化されていないスロットのみを初期化するような仕組みを提供します。 関数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-instanceshared-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-classafterメソッドのみが定義された場合は、 これはシステムが提供する初期化のメソッドのあとに実行されます。 よってupdate-instance-for-different-classの標準的な動作には干渉しないでしょう。

shared-initializeのメソッドは、クラスの再定義を カスタマイズするために定義されるでしょう。 詳細は7.1.5 Shared-Initializeを参照。

7.3 インスタンスの再初期化

ジェネリック関数reinitialize-instanceは、 初期化引数に従ってスロットの値を変更するときに使用されます。

再初期化のプロセスにより、スロットの値が変更され、 ユーザーが定義するアクションが実行されます。 これはスロットの追加と削除といったインスタンスの構造の修正は行いません。 また、:initformフォームを使ったスロットの初期化を行いません。

ジェネリック関数reinitialize-instanceは、直接呼び出されるでしょう。 これは引数に一つのインスタンスが要求されます。 またreinitialize-instanceshared-initializeによって使用される、 任意の数の初期化引数を受け取ります。 要求されるインスタンスの引数より後の引数は、初期化引数リストの形式でなければなりません。

システムが提供するreinitialize-instanceのメソッドは、 特定パラメーターにstandard-objectクラスを取ります。 最初、メソッドは初期化引数の有効性をチェックし、 もし指定された初期化引数が有効であると宣言されていなかった場合は、 エラーが発せられます(詳細は7.1.2 初期化引数の有効性の宣言を参照)。 このメソッドは、ジェネリック関数shared-initializeを、 次に示す引数とともに呼び出します。 引数は、インスタンスnil、そして受け取った初期化引数です。

7.3.1 再初期化のカスタマイズ

メソッドreinitialize-instanceは、インスタンスを更新するとき、 特定のアクションを定義することができます。 もしreinitialize-instanceafterメソッドのみが定義された場合は、 メソッドはシステムが提供する初期化のメソッドのあとに実行されます。 よって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クラスは、defmethoddefgenericフォームによって 定義されるメソッドの標準クラスです。

  • 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のメソッドが要求された場合は、 スロットの値を読むためのメソッドと、 スロットの値を書き込むためのメソッドが自動的に生成されます。 readerwriterのメソッドは、slot-valueを使用して実装されます。

スロットに対してreaderwriterのメソッドを指定するときは、 ジェネリック関数の名前とそれに沿って生成されるメソッドの名前を直接指定します。 もしwriterメソッドの名前をシンボルnameに指定した場合、 スロットへ書き込むためのジェネリック関数の名前がシンボルnameとなり、 そのジェネリック関数の引数は、 新しい値、インスタンスの順に2つ取ります。 もしaccessorメソッドの名前をシンボルnameに指定した場合、 スロットから読み込むためのジェネリック関数の名前がシンボルnameとなり、 そしてスロットへ書き込むためのジェネリック関数の名前が、リストの(setf name)となります。

スロットオプションの:reader, :writer, :accessorの指定により、 作成か修正が行われたジェネリック関数は、 正確に普通のジェネリック関数として扱うことができます。

注意として、slot-valueはスロットから値を読み込むか書き込む時に使われますが、 そのスロットのreaderwriterのメソッドが存在するかどうかに関わらず使用できます。 slot-valueが使われる時、readerwriterのメソッドは実行されません。

マクロ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とその全てのサブクラスのインスタンスからアクセス可能です。 しかし、もしC2C1のサブクラスであり、C2が名前Sのスロットを定義した場合、 C2とその全てのサブクラスのインスタンスでは、C1のスロットは共有されません。 クラスC1が共有スロットを定義したときは、 次の条件の時にC1のどんなサブクラスC2でもその単一のスロットは共有されます。 それは、C2defclassフォームで同じ名前のスロットを定義していないとき。 あるいは、C2のクラス優先リスト内において 同じ名前のスロットを定義しているクラスを見たとき、 C1よりも先導しているものがC2スーパークラスに存在していない場合です。

型の規則による結果は、スロットの値が関連するスロットの 各スロット指定子の型の条件を満たすことです。 スロットの型の条件が守られていない値を スロットに格納しようとした際の結果は未定義なので、 スロットの値は型の条件の安全性を失うでしょう。

スロットオプション:reader, :writer, :accessorは、 スロットの特性を宣言すると言うよりは、 メソッドを作成するものです。 readerwriterメソッドは、 7.6.7 メソッドの継承で説明される定義により継承されます。

スロットにアクセスするメソッドは、スロットの名前と、 スロットの値の型のみを使用します。 例えば、スーパークラスが、 指定した名前により共有スロットにアクセスすることを期待するメソッドを提供した場合、 またサブクラスが同じ名前で局所スロットを定義した場合を考えます。 もしスーパークラスによって提供されたメソッドを、 サブクラスのインスタンス上で使用した場合、 メソッドは局所スロットにアクセスします。

7.6 ジェネリック関数とメソッド

7.6章は7章Objectsのジェネリック関数関連の和訳を参照。