nptclのブログ

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

一年が経ちました

早いもので一年が経過しました。
nptの開発のために立ち上げたブログですが、 思ったより進んだような気がします。
ブログを書き始めた当時なんて、絶対作れないと思ってたんですが、 あと残り3つの関数を作成できれば完成です。
ただ、ここからが長いかもしれません。

今回はnptの致命的な欠点を話題に出します。
それは速度です。
遅すぎる。
まあバグがいっぱいあるのも致命的なんですけどね。

nptはネイティブな機械語を使っていないので遅いのは当然なのですが、 それにしたって、まともに使用できる限界を超えています。
例として、nptC言語のソースを生成するプログラムである、 mk.eastasian.lispというプログラムの実行時間を示します。

$ time sbcl --script mk.eastasian.lisp

real    0m0.119s
user    0m0.083s
sys     0m0.016s


$ time ccl -l mk.eastasian.lisp -e '(quit)'

real    0m0.359s
user    0m0.154s
sys     0m0.008s


$ time clisp -m 2g mk.eastasian.lisp

real    0m2.291s
user    0m2.285s
sys     0m0.011s


$ time npt --script mk.eastasian.lisp

real    1m1.046s
user    0m58.610s
sys     0m2.425s

sbclccl機械語で実行できるため、とても早く1秒以内で終わっています。
機械語を用いていないclispでも2.2秒。
それに対してnptは1分越え。ひどい。
sysが2秒越えって、何に使ってるんだ?

インタープリタの部分が致命的にダメなのだとは思います。
標準関数はCで書かれているので、たぶん早いはずなんです。

色々な工夫をしているはずなのですが、どうしたらいいんでしょうね。
とりあえずは最適化を作ろうかと思っています。
現状では、一切の最適化が存在してないのです。
それ以外にも、速度の改善は優先して行おうと思います。
というのも自分が使いたいんです。
これじゃあ使い物にならん。

この遅さなら最適化だけではどうにもならないでしょう。
ダメなら全体の考え直しです。
つらいけど、でもまあ仕方がない。
どうしていいのか全然わからないので、 できることを一つずつやって行こうと思います。

速度を解決しないとどうにもなりませんね。

Happy Hacking Keyboard Liteの掃除

Happy Hacking Keyboardではありません。
Happy Hacking Keyboard Lite2でもないです。
Happy Hacking Keyboard Liteです。

いつ買ったのかもう記憶にありませんが、ずっと酷使してきました。
今ならレビューできるような気がする。

長所は下記の通り

  • 頑丈過ぎる

短所は下記の通り

  • うるさすぎる

とにかく壊れない。
このキーボードは大切に使ってきたわけじゃありません。
本当に乱暴に適当にひどい扱いで使ってきました。
時には放置して違うキーボードを使っていました。

短所の「うるさすぎる」ってのも強烈で、 周りに人がいれば何事が起きたのか心配になるレベルでした。
まあ人によっては優しく打つ人だっているはずなので、 問題にならないのかもしれません。
自分は打つ力が強いのか、音も強烈ですし、キーボードにも優しくない体質のようです。
幾多のキーボードをぶっ壊して来たのですけど、でもこいつは生き延びています。
正確には一回だけControlキーに不具合が生じたんですけど、それは後で説明します。

こんなことならLiteじゃないの買っておけばよかった。
たしかLiteを3,000円だったか4,000円くらいで買ったんですよね。
当時、無印の方は13,000円くらいだったような記憶があります。
で、今買おうと思ったら、Lite2が6,000円超で無印が35,000円超。
もう少しLiteを使おうかな。

ということで清掃します。

掃除する

汚すぎるので掃除します。
掃除する前はこんな感じ。

f:id:nptcl:20200210161145j:plain

写真で見るとあまり汚くないように見えますが、かなりひどいです。
裏はこんな感じ。

f:id:nptcl:20200210161358j:plain

上と下の角の部分がボコボコに汚れているのがわかるでしょうか。
これは溶けたんです。
どういう理屈で溶けたのか全然わからないんですけど、 下記の写真のようにコードをぐるぐる巻いてたら、 コードではなく、キーボードの方が溶けました。

f:id:nptcl:20200210161519j:plain

溶けるものなの!?
裏面だけじゃなく、表面もコードが接触していた部分はボコボコしています。
そういうものなんだ。

ちなみに、キーボードの端子はPS/2です。
いまどきそんなのないPCも多いので、USBで接続できるのをつけています。
こんな感じ。

f:id:nptcl:20200210161642j:plain

では掃除するので、キーを全部むしります。 キーを取る器具も売っているようですが、 自分はその辺に置いてたハサミを使いました。
ただ、スキマにハサミを差し込んで、くいっと上げるだけで取れます。

f:id:nptcl:20200210161746j:plain

本当は全部取ったときのそのままの写真を載せたかったのですが、 あまりに汚くて皆様に汚物を見せるのもどうかと思いましたので、 汚れを適当に取った後の写真にしました。

f:id:nptcl:20200210162021j:plain

とったキーたちです。
この状態では汚れまくっているので、 ティッシュとアルコールで拭きました。

多分水でもいいと思うのですが、なぜかエチルとメチルの混合が その辺に転がっていたので使ってみました。
刻印が取れるとかそういうことは無かったです。

キーボードの方に若干汚れがありましたので、 歯ブラシとアルコールで洗ってあげます。

f:id:nptcl:20200210162220j:plain

それではキーをつけて行きます。
下記の写真で示した通り、左Shift, Space, Returnの3つのキーだけ針金が付いていますので、 先に取り付けました。

f:id:nptcl:20200210162336j:plain

f:id:nptcl:20200210162411j:plain

以前、Controlキーが少しだけ押しづらくなったことがありました。
いいか悪いのか知りませんが、シリコンスプレーを塗ったら スムーズに押せるようになったので、 今回も気持ち程度塗っておきます。

f:id:nptcl:20200210162529j:plain

後はどんどん付けていこう。

f:id:nptcl:20200210162550j:plain

下記のキーは、特に薄くなっていたものです。
まあよく使うキーですよね。

f:id:nptcl:20200210162619j:plain

おわり!

f:id:nptcl:20200210162641j:plain

共有配列のサイズをむりやり変更する

Common Lispの共有配列の話題です。
共有配列の拡張に関して、細かい所がはっきりしなかったので、 実験した内容をメモとして残します。

共有配列とは、別の配列を参照するだけの配列のことです。
make-array:displaced-to引数を指定することで作成できます。

例をあげます。

(setq aaa (make-array 10 :initial-contents '(a b c d e f g h i j)))
  -> #(A B C D E F G H I J)

(setq bbb (make-array 7 :displaced-to aaa))
  -> #(A B C D E F G)

配列aaaは単なる配列であり、配列bbbaaaを参照しています。
配列bbbはただ参照しているだけなので、どちらかが変更されると、 もう片方も変更されたように見えます。

要素を変更してみます。

(setf (aref aaa 2) :hello)
  -> :HELLO

aaa
  -> #(A B :HELLO D E F G H I J)

bbb
  -> #(A B :HELLO D E F G)

共有配列を作成するときは、参照先をはみ出すように作成するとエラーになります。
例えば下記の通り。

(make-array 15 :displaced-to aaa)
  -> ERROR  ★配列aaaは10個しかない

しかし、タイトルにもあるように、本題はこのサイズ関係を無視して、 無理やりサイズを変更することにあります。

参照先のサイズを変更する

サイズの変更は、adjust-array関数を使います。
この関数は、変更しようとする配列の:adjustableの値によって動作が変わります。

まずは:adjustablenilの場合を考えましょう。
配列がこんな感じで作成された場合です。

(make-array 10 :adjustable nil)

引数:adjustableには厄介なことがあります。
この引数は、実は絶対ではなく努力目標みたいなものです。
Common Lispの規格では、:adjustablenilとした場合でも、 tと同じ挙動することが許されています。
つまり絶対にnilにするという方法は存在しないのです。

しかし確認すると、少なくともsbcl, clisp, cclでは tでもnilでも指定したものに作成することができました。

確認は次のようにして行います。

(adjustable-array-p (make-array 10 :adjustable nil))
  -> NIL  ★nilになっている

今回は:adjustableをちゃんと設定できないと話が進まないので、 :adjustablenilで作成できたとします。

もし:adjustablenilの場合は、 配列aaaに対してadjust-arrayで変更を行おうとしても、 その配列aaaには変更が生じません。
ただ単純に新しい配列を作成して返却しているだけです。

例をあげます。

(setq aaa (make-array 10 :initial-contents '(a b c d e f g h i j)))
  -> #(A B C D E F G H I J)

(setq bbb (make-array 7 :displaced-to aaa))
  -> #(A B C D E F G)

(setq ccc (adjust-array aaa 5))
  → #(A B C D E)
     ★問題ないが、変更ではなく新規作成になる

(eq ccc aaa)
  -> nil  ★違う配列

(setf (aref ccc 3) :hello)
 ★adjust-arrayの新規配列を変更

aaa
  -> #(A B C D E F G H I J)  ★変更なし

bbb
  -> #(A B C D E F G)  ★変更なし

ccc
  -> #(A B C :HELLO E)
     ★変更あり、ただcccだけが変更された

では:adjustabletの場合はどうなるのでしょうか?
下記の例を考えます。

(setq aaa (make-array 10 :initial-contents '(a b c d e f g h i j)
                         :adjustable t))
  -> #(A B C D E F G H I J)

(setq bbb (make-array 7 :displaced-to aaa))
  -> #(A B C D E F G)

(adjustable-array-p aaa)
  -> T  ★adjustableになっている

サイズの変更を行います。

(setq ccc (adjust-array aaa 5))
  -> #(A B C D E)5個だけど問題ないの?

(eq ccc aaa)
  -> T  ★同じ配列

本来、この時点でエラーなのは分かるでしょうか。
参照先である配列aaaは、サイズが10個だったからこそ、 配列bbbを7個で作成できたのです。

しかし配列aaaは、サイズ5個に減らされてしまいました。
少なくとも、いま配列bbbmake-arrayで作成しようとするとエラーです。

(setq bbb-error (make-array 7 :displaced-to aaa))
  -> ERROR, ★配列7個じゃなくて、5個以下にしてね

なぜadjust-arrayを実行した時点でエラーにならないかというと、 その時点では一体誰に:displaced-toされているか分からないからです。

では、配列bbbにアクセスするとどうなるでしょうか?
処理系によって行動が違います。

・sbcl
(format t "~S~%" bbb)
  -> #()

・clispとccl
(format t "~S~%" bbb)
  -> ERROR、★配列が小さい

format関数は全ての配列にアクセスしようとするので上記のような結果になります。
実はclispcclは、個別にアクセスすることができます。

・clispとccl
(aref bbb 2)
  →C

clispcclでは、参照先のデータにアクセスできるものの、 sbcl#()になっているようなので、それすら許されませんでした。

参照元のサイズを変更する

参照元のサイズ変更とは、配列bbbの変更を行うことです。
普通に行う分ならmake-arrayと変わらず、参照元のサイズを超えたらエラーです。

(setq aaa (make-array 10 :initial-contents '(a b c d e f g h i j)))
  -> #(A B C D E F G H I J)

(setq bbb (make-array 7 :displaced-to aaa))
  -> #(A B C D E F G)

(setq ddd (adjust-array aaa 15 :displaced-to aaa))
  -> ERROR ★15個ではなく10個以下にしてね

vector-push-extend関数を使う

サイズを拡張する方法は、adjust-arrayの他に、 vector-push-extend関数もあります。

この関数は、:fill-pointerの配列のみを受け取り、 必要に応じてサイズの拡張を行うというものです。
配列はadjustableである必要があるのですが、 sbclではそこまで要求していないようです。

参照先であるaaavector-push-extendを使うのは何も問題ありません。
しかし参照元であるbbbvector-push-extendを使って、 もし拡張が行われると共有配列が解除されてしまいます。

確認してみましょう。
まずは配列を:fill-pointerで作り直します。

(setq aaa (make-array 10 :initial-contents '(a b c d e f g h i j)))
  -> #(A B C D E F G H I J)

(setq bbb (make-array 7 :displaced-to aaa
                        :fill-pointer t
                        :adjustable t))
  -> #(A B C D E F G)

この状態では、参照されている状態です。

(setf (aref aaa 1) :xx)
  -> :XX

aaa
  -> #(A :XX C D E F G H I J)

bbb
  -> #(A :XX C D E F G)

vector-push-extendで拡張して、共有を解除してみます。

(vector-push-extend :yy bbb)
  -> 7

aaa
  -> #(A :XX C D E F G H I J)

bbb
  -> #(A :XX C D E F G :YY)

共有状態が解除されたということは、 領域が拡張されたタイミングで 参照先のコピーを作成したということになります。
すでに共有状態ではないので、片方の変更はもう片方に反映されません。

(setf (aref aaa 2) :zz)
  -> :ZZ

aaa
  -> #(A :XX :ZZ D E F G H I J)

bbb
  -> #(A :XX C D E F G :YY)

領域の拡張は、:displaced-toがないadjust-arrayで行っているから このような結果になっているのでしょう。
内部では次のような命令が実行されているのだと思います。

(adjust-array bbb 8 :fill-pointer t)

補足ですが、vector-push-extendを実行したときに、 まだfill-pointerで設定した長さに余裕がある場合は 領域の拡張が行われないので、共有状態は解除されません。

試しにC言語でCommon Lispを使う

nptの開発は結構進んでおり、 あとはcompile-file, load, stepだけとなりました。
まあバグを含め問題盛りだくさんなんですけどね。

nptの開発目的の一つとして、C言語に組み込んで使うという考えがあります。
だからわざわざnpt-amalgamationという訳の分からないものも作っています。

C言語の組み込みは、自分にとっては結構大切なものなので、 少しくらいは外部インターフェイスを作ろうかと頑張っていたのですが、 やはりなかなか難しいものです。

例えば下記のLispのコードを考えます。

(defun fact (x)
  (if (not (plusp x))
    1
    (* x (fact (1- x)))))

(defun test ()
  (format t "Fact: ~S~%" (fact 200)))

(test)

単なる階乗です。
これをnptのモジュールを使って、C言語で表してみます。
★ただし問題ありバージョン。

addr fact(addr x)
{
    addr y;

    if (! lisp_plusp(x))
        return lisp_fixnum(1);

    lisp_funcall8(&y, "-", x, lisp_fixnum(1), NULL);
    lisp_funcall8(&y, "*", x, fact(y), NULL);

    return y;
}

int test(void)
{
    addr value;

    value = fact(lisp_fixnum(200));
    lisp_format8(lisp_t(), "Fact: ~S~%", value, NULL);

    return 0;
}

実際に動かした結果は下記の通り。

Fact: 78865786736479050355236321393218506229513597768717326329474253324435944996
34033429203042840119846239041772121389196388302576427902426371050619266249528299
31113462857270763317237396988943922445621451664240254033291864131227428294853277
52424240757390324032125740557956866022603190417032406235170085879617892222278962
3703897374720000000000000000000000000000000000000000000000000

初期化などを抜かした一部抜粋ですが、こんな感じになりました。
個人的には割といいんじゃないかと思うんです。
もし上記のようにラフに使えるなら、 便利なモジュールになるんじゃないでしょうか。

でもダメ。
上のコードはGarbage Collectionのタイミングでメモリ破壊をおこします。

これ、なんて言うんでしょうか。
メモリリークの逆なんですよね。
C言語上だとまだ使ってるのに、Common Lispではもういらないだろうと判断して 勝手にメモリを開放していくやつなんですが、 nptの開発では本当に頻繁に起こります。

検索すると、C#とかでも生じる現象みたいです。
たぶんGarbage Collection型のメモリを持った環境だと 典型的な問題なんだと思います。

もちろん対策する方法はあるのですが、 普通の人にメモリ破壊が起きないように C言語でコードを書いてくださいと言われても無理でしょう。

Lispのモジュール化というのもなかなか難しいものです。
まあこのまま作っていく予定ですが。

define-compiler-macroを使ってみる

Common Lispdefine-compiler-macroを使ってみましょう。

ただ使うだけなら、検索すればすでに何人かの方が このマクロの正しい使い方を書いていますので、 そちらを見ていただいた方が絶対に有益な情報が得られると思います。

本投稿は、誰も話題に出していないsetfの宣言について説明します。

まずは普通の使い方

define-compiler-macroとは、コンパイルの時だけ展開するマクロです。
ふつうの関数だろうがマクロだろうが、 define-compiler-macroを使えば挙動を変えられるってことになります。
ただしコンパイルの時だけですが。

コンパイル」とは次の2つの関数で行われる動作のことです。

  • compile関数
  • compile-file関数

それでは例文を。

(defmacro aaa ()
  :hello)

(define-compiler-macro aaa ()
  :abc)

defmacroのマクロaaaは、ただ:helloを返却するだけです。
それに対してdefine-compiler-macroの方は、:abcを返却しています。

実行結果を示します。

(format t "~A~%" (aaa))
  -> :HELLO

(format t "~A~%" (funcall
                   (compile nil '(lambda () (aaa)))))
  -> :ABC

はい、何も難しくないですね。

setfを宣言するとどうなるか?

これが厄介なんです。
何が厄介なのかは大切なので、ちゃんと説明して行きます。

まず規約ではdefine-compiler-macroの第一引数は、function nameとされています。
function nameとは、symbol(setf symbol)のことなので、 規約として次のような宣言が許されます。

(define-compiler-macro (setf name) (...)
  ...)

しかし、これ以上の記載が無く例文も存在しません。
つまりどう実装するかは処理系に任されることになります。
では実験してみるとどうなるかというと、 sbcl, clisp, cclでは全て同じ動作をしたため、 まずはその挙動を紹介します。

(define-compiler-macro (setf aaa) (value inst)
  `(setf (cdr ,inst) ,value))

(funcall
  (compile nil '(lambda ()
                  (let ((x (cons 10 20)))
                    (setf (aaa x) 30)
                    (format t "~A~%" x)))))
-> (10 . 30)

例文では(setf aaa)(setf cdr)と同じ挙動になるように定義しています。
こう見ると何も問題は無いように見えます。

しかしそもそもsetfの定義は、上記のような通常のマクロでは表せられないものであり、 普通はdefine-setf-expanderによって定義するものです。

もし(setf cdr)と同じ挙動をする通常のsetfマクロを 作りたいのであれば次のようになります。

(define-setf-expander aaa (inst)
   (let ((g (gensym)))
     (values nil nil `(,g) `(setf (cdr ,inst) ,g) `(cdr ,inst))))

よって、次のようにdefine-compiler-macroを 定義する処理系が存在する可能性が出てきます。

;; ★こういう処理系もあるかもね
(define-compiler-macro (setf aaa) (inst)
   (let ((g (gensym)))
     (values nil nil `(,g) `(setf (cdr ,inst) ,g) `(cdr ,inst))))

存在するかどうかの確認はしていませんけどね。

個人的にはこの例の方が自然のように思えます。
では、なぜこうしなかったのか。

それは各処理系の都合なので私がわかるはずがないのですが、 もともとdefine-compiler-macroの目的とは、 最適化や高速化のために導入されたものなのではないでしょうか。
コンパイル時にはsetf形式を発見したら、 単純に置き換えるような仕組みであった方が何かと便利なのでしょう。

とはいっても、現状のsbcl, clisp, cclで見られる方式を採用した場合には 極めて大きな問題が浮上します。
それは、get-setf-expansion関数が使えないということ。
もっと分かりやすく言うと、setfマクロじゃなければ使えないということです。

例を示します。

(define-compiler-macro (setf aaa) (value inst)
  `(setf (cdr ,inst) ,value))

(funcall
  (compile nil '(lambda ()
                  (let ((x (cons 10 20)))
                    (shiftf (aaa x) 30)
                    (format t "~A~%" x)))))
-> ERROR, ★shiftfでは使えない

面白いことに、psetfならうまく行きました。

(funcall
  (compile nil '(lambda ()
                  (let ((x (cons 10 20)))
                    (psetf (aaa x) 30)
                    (format t "~A~%" x)))))
-> (10 . 30)

処理系を作る上では面倒この上ないです。
setfが現れたら、コンパイルの時だけdefine-compiler-macroの情報を 引っ張ってこなければいけないのですから。

いい加減な結論を出すとこんな感じ。

  • define-compiler-macro(setf symbol)は定義しない方がいいかもね

make-instances-obsoleteは何もしてないのでは?

Common Lispの、sbclとcclだけの話になります。

ジェネリック関数make-instances-obsoleteとは、 クラスの再定義に関係するものです。

こいつがよく分からない。
たぶん、クラスを再定義したあとで、 すでに作成されているインスタンスを一斉に更新するときに使うのだと思います。
なんで「思います」なのかというと、更新されているように見えないからです。

まずは簡単なクラス再定義の例を示します。

;; クラス作成
(defclass aaa ()
  ((bbb :initarg :bbb)))

;; インスタンスの作成
(defparameter instance (make-instance 'aaa :bbb 100))

;; インスタンスの更新手順
;; bbbの値をcccにコピー
(defmethod update-instance-for-redefined-class :before
  ((instance aaa) add del prop &rest initargs)
  (declare (ignore add del initargs))
  (setf (slot-value instance 'ccc) (getf prop 'bbb)))

;; クラスの再定義
(defclass aaa ()
  ((ccc :initarg :ccc)))

;; インスタンスにアクセス
(format t "~S~%" (slot-value instance 'ccc))
  -> 100

さて、ここで問題にしたいのは、instanceの更新が一体いつ行われるかです。
規約ではdefclassによる再定義からinstanceの読み書きが行われるまで、 いつでも良いことになっています。

でもタイミングが分からないというのは不安なので、 早々に更新してしまいたいと思うかもしれません。
そんなときに使うのがmake-instances-obsolete関数です。
この関数を用いることで、好きなタイミングで全てのインスタンスを 更新できるはずです。

実際には更新されないんですけどね。
実行例を示します。

;; クラス作成
(defclass aaa ()
  ((bbb :initarg :bbb)))

;; インスタンスの作成
(defparameter instance (make-instance 'aaa :bbb 100))

;; bbbの値をcccにコピー
(defmethod update-instance-for-redefined-class :before
  ((instance aaa) add del prop &rest initargs)
  (declare (ignore add del initargs))
  (setf (slot-value instance 'ccc) (getf prop 'bbb)))

;; クラスの再定義
(defclass aaa ()
  ((ccc :initarg :ccc)))

;; ★早々に更新してしまおう
(make-instances-obsolete 'aaa)

;; インスタンスにアクセス
(format t "~S~%" (slot-value instance 'ccc))
  -> 100

一見するとうまく行っています。

それではmake-instances-obsoleteの実行で 本当に更新されたのかどうかを確認します。
確認の方法はupdate-instance-for-redefined-classの中と 更新されるはずの所にformat文を入れましょう。

(defclass aaa ()
  ((bbb :initarg :bbb)))

(defparameter instance (make-instance 'aaa :bbb 100))

(defmethod update-instance-for-redefined-class :before
  ((instance aaa) add del prop &rest initargs)
  (declare (ignore add del initargs))
  (format t "~&<<update-instance-for-redefined-class>>~%")  ;; ★確認
  (setf (slot-value instance 'ccc) (getf prop 'bbb)))

(defclass aaa ()
  ((ccc :initarg :ccc)))

(make-instances-obsolete 'aaa)

(format t "Hello~%")  ;; ★確認
(format t "~S~%" (slot-value instance 'ccc))

実行結果は次の通り。

Hello
<<update-instance-for-redefined-class>>
100

うん、ダメですね。
Helloの出力の後に更新されています。

本来であれば、update-instance-for-redefined-class関数は make-instances-obsoleteで実行されるべきなのですが、 どう見てもslot-value関数の中で実行されています。

もしかしてやり方が間違っているのかな。
次の方法でも無理でした。

(make-instances-obsolete
  (find-class 'aaa))

色々やってみたのですが、更新されている様子は無かったです。

なぜmake-instances-obsoleteは何もしないのでしょうか?
処理系を実装しようとしてみると分かるのですが、 変更される前のクラスが生成したインスタンスを全て更新する方法は、 たぶんメモリの全走査しかないと思います。
つまりgerbage collectionと同じようなことをしなければなりません。
そこまでする必要があるかどうかの微妙なラインです。

make-instanceを実行するたびにバッファか何かに 全てのインスタンスを保存しておけばいいのでは? と考えるかもしれませんが、インスタンスって数が非常に多いので あまり現実的ではないのだと思います。

という訳でmake-instances-obsoleteは実現不可ということにしましょう。

defclassによる再定義を何度も実行したいから、 make-instances-obsoleteupdate-instance-for-redefined-classを 繰り返し実行しようぜ! とか思わない方がいいです。

ただ、make-instances-obsolete関数が何もしないからと言って、 関数が実行されていない訳ではありません。
クラス再定義の時に実行はされているようです。
次に例を示します。

(defclass aaa ()
  ((bbb :initarg :bbb)))

;; 一つでもインスタンスを作成しないとfinalizeが行われないため
;; make-instances-obsoleteは起動しない
(make-instance 'aaa)

;; ★良くない確認方法
(defmethod make-instances-obsolete ((inst standard-class))
  (declare (ignore inst))
  (format t "~&<<make-instances-obsolete>>~%"))

(format t "Hello~%")
(defclass aaa ()
  ((ccc :initarg :ccc)))

実行結果は下記の通り

Hello
<<make-instances-obsolete>>

上記の例はmake-instances-obsoleteの標準の動作を上書きしてしまうので、 例え何もしないとしても良い実装とは言えませんね。

まとめると次のような結論が得られるのかと思います。

  • クラス再定義時にmake-instances-obsoleteは起動される
  • でもmake-instances-obsoleteは何もしない
  • インスタンスの更新時期はアクセスされたときのみ

アクセスされたときとは、具体的には下記の四つの関数のどれかが 実行されたときだと考えていいと思います。

  • slot-value
  • slot-exists-p
  • slot-boundp
  • slot-makunbound

いや、他にもアクセス関数はあるでしょ? 例えばwith-slotsとか、と思うかもしれませんし、 それは正しいのですが、たぶん他の機能は結局上記の 4つの関数に帰着するのかと思います。

format Justification (幅揃え)

Common Lispformatの命令である、Justification ~<...~>の説明をします。

この機能はただ空白を均等に出力するだけなのですが、 習得するにはわりと困難だと思います。
理由は「いや、こんな機能どうせ使わないし」と 思って気分が乗らないからです。
だって本当に使わないし。

まあそれは置いておいたとして、機能そのものは何も難しいことはありません。
パッと見れば何をしているのか理解できます。
例えば下記の通り。

(format t "~30<AAA~;BBB~;CCC~>~%")
AAA           BBB          CCC

例では、~30<~>で囲まれた箇所を~;で3つに分けています。
その分かれた部分を、30文字に収まるように空白を均等に分配します。
それだけです。

じゃあ何が難しいのか。
一つはLogical Blockという全く関係のない機能が同居しているということ。
他は細かくてどうでもよさそうな機能がいくつもあるためです。

実際にJustificationではなくPretty Printingを使った方がいい場合があります。
しかしPretty Printingを使えと言われても、 たぶんほとんどの人が、そんな訳わからん機能なんて使えないよと 諦めるんじゃないでしょうか。

覚えなくても生きていける、だからこそ誰も覚えない。
そんな理屈がJustification(正当化)されてしまう、 これこそが真のJustificationです。

本投稿の目的は、不遇のJustificationを一つずつ例文として示すことです。

Logical Blockはまた今度

Logical Blockとは、Pretty Printingのpprint-logical-blockのことです。
Justificationは~<...~>で囲んだものですが、 Logical Blockは終わりのカッコに:を付けた、~<...~:>で囲んだものです。

両者はほとんど関係のない機能です。
Logical Blockの説明は、また別の機会にしようかと思います。

Justificationの使い方

では例をあげて行きます。

まずは単純な例

第一引数で幅30を指定します。

(format t "++~30<AAA~;BBB~;CCC~>++~%")
++AAA           BBB          CCC++

++++の間が幅30に収まるように均等に空白を分配しているのがわかります。

空白量の確認

各節の長さ(AABBBBBBBBBの文字数のこと)が違っていても空白の量は同じです。

(format t "++~30<AA~;BBBBBBBBB~;C~;DD~;EEE~>++~%")
++AA    BBBBBBBBB   C   DD   EEE++

:@

:@で、前後に空白を追加できます。
まずは:にて前に空白を挿入します。

(format t "++~30:<AAA~;BBB~;CCC~>++~%")
++       AAA       BBB       CCC++

@にて後ろに空白を挿入します。

(format t "++~30@<AAA~;BBB~;CCC~>++~%")
++AAA       BBB       CCC       ++

:@で前後に空白を挿入します。

(format t "++~30:@<AAA~;BBB~;CCC~>++~%")
++      AAA     BBB     CCC     ++

要素が一つの場合

要素が一つの場合は右揃えです。

(format t "++~30<AAA~>++~%")
++                           AAA++

右揃えなので、:は意味がありません。

(format t "++~30:<AAA~>++~%")
++                           AAA++

@を指定すると左揃えにになります。

(format t "++~30@<AAA~>++~%")
++AAA                           ++

:@の両方を指定すると中央揃えです。

(format t "++~30:@<AAA~>++~%")
++              AAA             ++

幅の指定

例文で示して来たとおり、第一引数は幅の大きさです。
例えば次の通り。

(format t "++~40<AAA~;BBB~;CCC~>++~%")
++AAA                BBB               CCC++

第一引数を省略すると0が指定されたことになります。

(format t "++~<AAA~;BBB~;CCC~>++~%")
++AAABBBCCC++

幅0に対して、AAABBBCCCの9文字を出力するのは無理なので、 そのまま出力されています。

幅の拡張

もし第一引数の幅に収まらなかった場合は、幅を拡張します。
拡張する量は、第二引数により指定できます。
省略時は1です。

(format t "++~,20<AAA~;BBB~;CCC~>++~%")
++AAA      BBB     CCC++

この例では、幅0に収まらなかったので、 幅20で試してみて、収まるようだったので その内容を出力しています。

(format t "++~5,20<AAA~;BBB~;CCC~>++~%")
++AAA        BBB        CCC++

この例では、幅5で試して見たのですが、 とても収まらなかったため20を足して幅25で出力しています。
もし幅25でダメだったら、さらに20を足して幅45で出力することになります。

最小の空白

第三引数にて、最小の空白の個数を指定できます。

(format t "++~,,5<AAA~;BBB~;CCC~>++~%")
++AAA     BBB     CCC++

省略された第一引数である幅0に収まらないので、 第三引数である最小の空白5文字とともに出力されています。

(format t "++~30,,5<AAA~;BBB~;CCC~>++~%")
++AAA           BBB          CCC++

この例では、幅30で空白を分配した結果、 空白量が最小値の5をクリアしているため、 第三引数を省略した結果と同じ内容が出力されています。

(format t "++~15<AAA~;BBB~;CCC~>++~%")
++AAA   BBB   CCC++

(format t "++~15,20,5<AAA~;BBB~;CCC~>++~%")
++AAA           BBB          CCC++

上の例は単純に幅15文字に収めた結果であり、空白は3つ出力されています。
下の例は、最低の空白量を5と指定していますが、 幅15だと空白は3つしかないため、 幅15から20を加算した幅35で再試行した結果を返却しています。

空白文字の指定

第四引数にて、空白文字を指定できます。
これはもう例文を見ると一発でわかります。

(format t "++~30,,,'*<AAA~;BBB~;CCC~>++~%")
++AAA***********BBB**********CCC++

中断

~^により中断することができます。
一応説明しておくと~^は引数がもうない場合に中断する命令です。

例えば次の通り。

(format t "++~30<AA~;BB~;CC~;DD~;~^EE~>++~%")
++AA        BB       CC       DD++

上記の例では、~^によりEEを無視して出力しています。

(format t "++~30<AA~;BB~;CC~;~^DD~;EE~>++~%")
++AA            BB            CC++

この例では、~^によりDD以降を全て無視して出力しています。
規約には~^~;の直後に指定するべきだとの記載があります。
なぜなら~^を指定するとその節は全て無視されるからです。
例を示します。

(format t "++~30<AA~;BB~;CC~;DD~^dd~;EE~>++~%")
++AA            BB            CC++

この例ではDD~^ddとなっていますが、 節の全てが無視されるので、ddだけではなく 最初のDDすら無視されているのがわかります。

画面の幅に合わせる

ここまでは~<の基本的な使い方を説明してきました。
ここからは「画面の幅」という考え方が登場します。

まずは下記の二例を見て行きましょう。

(format t "++~30<AAA~;BBB~;CCC~>++~%")
++AAA           BBB          CCC++

(format t "++~30<AAA~:;BBB~;CCC~>++~%")
++BBB                        CCC++

両者の違いは、AAA~;~:;のどちらで終わらせているかの違いになります。
上の例は今まで説明してきたものですが、 下の例はまた別の方法で処理されるため、出力が違っています。

~<...~>では、最初の節だけ:が付いた^:;で終わらせることができます。
その場合に限り「画面の幅」というものが考慮されます。

出力結果が変わっていますが、一体どういう事なのでしょうか?
動作としては、まずはAAA, BBB, CCCformatの処理を進めていくのですが、 最初のAAAは特別な場合に出力されるものとして保留しておき、 BBBCCCの2つの節に対していつもの~<...~>処理を行います。

幅揃えとしては最初の節であるAAAは含まれません。
30文字に収まるかどうかの判定は、AAAを除外したBBBCCCでのみ実施されるのです。
AAAは何なのかというと、もしBBBCCCの整形結果が画面の幅を超えていた場合は 最初にAAAがそのまま出力されます。

では画面の幅とは何なのでしょうか?
Pretty Printingだと*print-right-margin*でした。
でもJustificationはそんな変数を使いません。

画面の幅とは出力するstreamに問い合わせを行い、 もしstreamが端末に関係するならその幅を返却するということになるのかと思います。

完全に処理系依存ですが、例えば*terminal-io*とか、 リダイレクトされていない*standard-output*に出力するなら、 ioctl関数なんかでwindowサイズを調査するんじゃないでしょうか。
Pretty Printingの*print-right-margin*nilだった場合は そんな感じで幅を返却しています。

そんなふうに色々やってみても、 もしファイルへの出力だった場合には画面のサイズなんてありません。
画面の幅が分からなかったときは、72を使うんだそうです。
72は規約に書いてある定数です。

うちの端末は72なんかより大きいよ!
なんて思っても、たぶんどうにもならないと思うのですが、 一応は最初の節を終端させた~:;の第二引数で画面幅を指定できます。
例えばこんな感じ。

(format t "++~30<AAA~,10:;BBB~;CCC~>++~%")
++AAABBB                        CCC++

この例では、~,10:;により画面の幅を10に指定しています。
最初に++を出力していますので、その2文字を考慮し、 2文字+30文字=32文字が画面の幅である10を余裕で越えているため、 最初の節のAAAが出力されているのが分かります。

最初の++の2文字を考慮しているということは、 改行からの文字数をちゃんとカウントしているわけです。
まるでPretty Printingですね!

実際にそんな動作を期待して設計された機能のようです。
例えば規約に載っている次の例文を考えます。

"~%;; ~{~<~%;; ~1,50:; ~S~>~^,~}.~%"

上記の例にある~1,50:;の第一引数はあとで説明します。
実行例を次に示します。

(format t "~%;; ~{~<~%;; ~1,50:; ~S~>~^,~}.~%"
  '(100000000 2000000000 hellohellohello 4000000 599999999))

出力結果は下記の通り。

;;  100000000, 2000000000, HELLOHELLOHELLO,
;;  4000000, 599999999.

かなりわかりづらいのですが、 最初の節が~%;;であり、 画面50文字指定で~S,を出力していると考えればいいと思います。
もし画面50文字を超えた場合は、改行と;;が出力されるというもの。

Pretty Printingを使って書き換えるとこんな感じ。

(setq *print-pretty* t)
(setq *print-miser-width* nil)
(setq *print-right-margin* 50)

(defun output (list)
  (pprint-logical-block (nil list :per-line-prefix ";;  ")
    (loop (write (pprint-pop))
          (pprint-exit-if-list-exhausted)
          (write-string ", ")
          (pprint-newline :fill)))
  (fresh-line))

(output '(100000000 2000000000 hellohellohello 4000000 599999999))

出力結果は下記の通り。

;;  100000000, 2000000000, HELLOHELLOHELLO,
;;  4000000, 599999999

そのうち説明する予定ですが、formatのLogical Blockを使うと 次のように書くこともできます。

(setq *print-pretty* t)
(setq *print-miser-width* nil)
(setq *print-right-margin* 50)

(format t "~<;;  ~@;~@{~W~^, ~:_~}~:>~&"
  '(100000000 2000000000 hellohellohello 4000000 599999999))

出力結果は下記の通り。

;;  100000000, 2000000000, HELLOHELLOHELLO,
;;  4000000, 599999999

Pretty Printingは意味不明過ぎるけど、よく考えられていると思います。
以前の投稿である pprint-newlineの使い方 - nptclのブログ の説明を合わせて見ると 理解できるのではないでしょうか。

それでは続けて~:;の第一引数について説明します。
最初の節のAAAが出力されるには、 第一引数の量の余裕を持たせないといけないとのことです。
つまりBBBCCCの整形した文字数と現在位置だけではなく さらに第一引数の量を加算した結果が画面の幅と比較されます。
言い換えるなら、~1,50:;~,49:;は同じです。

規約の例である~1,50:;は、 出力されるカンマ,を考慮して第一引数に1を指定したとの記載があります。

こういうのを全部含めて考えると、画面の幅を考慮した出力は Pretty Printingを使った方が絶対いいですね。
覚えるまで大変ですが。

使用禁止の命令

Justificationは下記の命令が使えません。

~W          write
~_          pprint-newline
~I          pprint-indent
~:T         pprint-tab
~<...~:>    pprint-logical-block

つまり、Justificationの中では Pretty Printingに関わる全ての機能が使用不可になります。
writeも使えないのか。

よって次の命令はエラーになります。

(format t "~<~W~>~%" 100)

clispでは全く問題なく100が出力されますが、気にしないことにします。

最初の節での中断

二番目以降が中断された場合はすでに説明されていますが、 最初の節が中断された場合はどうなるでしょうか。

処理系によって動作がわかれました。
例えば次の通り。

(format t "++~30<A~^AA~;BBB~;CCC~>++~%")
sbcl, cclの結果
++++

clispの結果
++                              ++

画面の幅を考慮する~:;の場合も下記に示します。

(format t "++~30<A~^AA~:;BBB~;CCC~>++~%")
sbcl, cclの結果
++++

clispの結果
++                              ++

~:;の場合は、二番目の節を中断した場合も同じです。

(format t "++~30<AAA~:;B~^BB~;CCC~>++~%")
sbcl, cclの結果
++++

clispの結果
++                              ++

最初の節を中断した場合は、出力内容は期待しない方がよさそうですね。
個人的にはclispが正しいような気がします。