nptclのブログ

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

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

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で設定した長さに余裕がある場合は 領域の拡張が行われないので、共有状態は解除されません。