nptclのブログ

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

整数を英語で表現する5(Lispコード)

だいぶ前に、整数を英語で表現する方法について説明しました。

整数を英語で表現する1 - nptclのブログ
整数を英語で表現する2(中学レベル) - nptclのブログ
整数を英語で表現する3(巨大な数) - nptclのブログ
整数を英語で表現する4(序数と負数) - nptclのブログ

これらの説明に基づいて、Common Lispで実装しましたので配布します。

cwsystem
https://github.com/nptcl/cwsystem

以下、説明です。

radix-string

整数を英語で表現するには、関数radix-stringを使用します。
機能は(format nil "~R" x)と同じですが、速度とメモリが許す限り巨大な数値を表せます。
いくつか例を示します。

通常の使用

(cwsystem:radix-string 123)
"one hundred twenty-three"

マイナス

 (cwsystem:radix-string -4)
"minus four"

序数

(cwsystem:radix-string 20 nil)
"twentieth"

巨大な数

(cwsystem:radix-string (ash 1 200))
"one novendecillion six hundred six octodecillion nine hundred thirty-eight septendecillion forty-four sedecillion two hundred fifty-eight quindecillion nine hundred ninety quattuordecillion two hundred seventy-five tredecillion five hundred forty-one duodecillion nine hundred sixty-two undecillion ninety-two decillion three hundred forty-one nonillion one hundred sixty-two octillion six hundred two septillion five hundred twenty-two sextillion two hundred two quintillion nine hundred ninety-three quadrillion seven hundred eighty-two trillion seven hundred ninety-two billion eight hundred thirty-five million three hundred one thousand three hundred seventy-six"

unit-string

3桁区切りの単位を取得する関数unit-stringもあります。

通常の使用

(cwsystem:unit-string 0)
"thousand"

(cwsystem:unit-string 1)
"million"

(cwsystem:unit-string 2)
"billion"

The Conway-Wechsler Systemの3桁

(cwsystem:unit-string 789)
"novemoctogintaseptingentillion"

序数

(cwsystem:unit-string 345 nil)
"quinquadragintatrecentillionth"

3桁の連結

(cwsystem:unit-string 1234567890)
"milliquattuortrigintaducentilliseptensexagintaquingentillinonagintaoctingentillion"

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">

のような形で記載されています。

【追記】下記の記載方法もあるそうです。

<meta charset="euc-jp">

このフォームの対応は最後にまとめて記載します。

しかし、データを取得する際にはまずエンコード問題を解決しなければなりません。 そこで、取得した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の自動化を行う場合は、 たぶん誰かがすでに作っているであろう専門のライブラリを使った方が楽かもしれません。

【追記】別のcharset記載について

charsetの記載方法は下記のフォームもあるそうです。

<meta charset="euc-jp">

対応したLispコードを下記に示します。

;;
;;  guess-html-encoding
;;
(defun guess-html-encoding-upcase (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-string (x)
  (let ((x (guess-html-encoding-upcase x)))
    (cond ((string= x "UTF8") :utf8)
          ((string= x "ASCII") :utf8)
          ((string= x "JIS") :jis)
          ((string= x "ISO2022JP") :jis)
          ((string= x "EUC") :eucjis)
          ((string= x "EUCJP") :eucjis)
          ((string= x "EUCJIS") :eucjis)
          ((string= x "SHIFTJIS") :shiftjis)
          ((string= x "SJIS") :shiftjis)
          ((string= x "CP932") :shiftjis)
          ((string= x "MS932") :shiftjis)
          ((guess-html-encoding-windows x) :shiftjis))))

(defun guess-html-encoding-charset1 (x)
  ;; <meta charset="euc-jp">
  (first-match (:meta :charset ?x . _) x
    (guess-html-encoding-string ?x)))

(defparameter +guess-html-encoding-charset2+
  (cl-ppcre:create-scanner "^.+(?i)charset(?-i)\\s*=\\s*\\\"?(\\S+)\\\"?\\s*$"))
(defun guess-html-encoding-charset2 (x)
  ;; <meta http-equiv="content-type" content="text/html; charset=Shift_JIS">
  (first-match (:meta :http-equiv ?x :content ?y) x
    (when (and (equalp ?x "content-type")
               (stringp ?y))
      (multiple-value-bind (str group)
        (cl-ppcre:scan-to-strings +guess-html-encoding-charset2+ ?y)
        (when str
          (guess-html-encoding-string
            (elt group 0)))))))

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

(defun guess-html-encoding (x)
  (let ((x (guess-html-encoding-html x)))
    (or (guess-html-encoding-charset1 x)
        (guess-html-encoding-charset2 x))))


;;  fetch
(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 :size #x010000))))

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/docs/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/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を除去します。

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の全てを日本語訳しました。

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

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

ジェネリック関数は本物の関数なので、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 メソッドの選択とコンビネーションに記載されています。