Gauche:immutableなデータの注意点
immutableなデータの注意点
Gauche 0.9.10 から、immutable なペアについては、変更するとエラーが出るようになりました。
( https://practical-scheme.net/gauche/man/?l=jp&p=変更可能なペアと変更不可なペア )
このため、以下の説明は、もう古いものとなっています。
(ただし、ベクタ等、他のリテラル定数については、まだ注意が必要なものも存在します)
(2020/12/30 11:46:36 UTC)
- 多くのプログラミング言語で、immutable なデータが使われています。
データを不変にすることで、(1)副作用がなくなる、(2)スレッドセーフなデータとして扱える、
(3)効率的なデータ・キャッシュを実現できる、等のメリットが得られます。
- Scheme にも mutable なデータと immutable なデータがあります。
例えば、リテラル定数は immutable なデータです (R7RS 3.4)。
- ただ、多くの Scheme 実装では、
immutable なデータの破壊的変更をしないように、ユーザが注意する必要があります。
(エラーになれば分かりやすいのですが、エラーにならない場合が多いです。。。)
例1. リテラル定数
;; リテラルを破壊的変更している例 (悪い例) (define x '(1 2 3 4 5)) (define y '(1 2 3 4 5)) (set-car! y 100) (print x) ; -> (1 2 3 4 5) ? (結果は保証されない) (print y) ; -> (100 2 3 4 5) ? (結果は保証されない)
- 上記の例では、y を破壊的変更しています。
現状の Gauche では、普通に変更することができます。
- しかし、将来的には最適化によって、
2個の同一な immutable データである '(1 2 3 4 5) は、
メモリ上で実体が共有されるかもしれません。
(現在でも、プリコンパイルされた場合は同値のリテラルは実体が共有されます)
- そうすると、y を変更することによって、x も変更されてしまうかもしれません。
定数と思っていたデータが、予想外に変化することで、
相当分かりにくい不具合の原因になる可能性があります。
(x を破壊的変更する命令は、スクリプト内のどこにも存在しない。。。)
- この場合の対策としては、(1)リテラルを使わない、(2)破壊的変更を使わない、
もしくは、(3)copy 系の手続きを使って mutable なコピーを取得する、
ことが挙げられます。
;; リテラルを使わない(コンストラクタを使う)例 (define x (list 1 2 3 4 5)) (define y (list 1 2 3 4 5)) (set-car! y 100) (print x) ; -> (1 2 3 4 5) (print y) ; -> (100 2 3 4 5)
;; (リテラルの)破壊的変更を使わない例 (define x '(1 2 3 4 5)) (define y '(1 2 3 4 5)) (set! y (cons 100 (cdr y))) (print x) ; -> (1 2 3 4 5) (print y) ; -> (100 2 3 4 5)
;; mutable なコピーを取得する例 (define x '(1 2 3 4 5)) (define y '(1 2 3 4 5)) (set! y (list-copy y)) (set-car! y 100) (print x) ; -> (1 2 3 4 5) (print y) ; -> (100 2 3 4 5)
- 最後の例は、
防衛的コピーと言われるようです。(← すいません意味が違ったようです)
パフォーマンスは悪くなりますが、破壊的変更を安全にするためには、
有用な場合があります。
(ただし、list-copy はリストの背骨の部分のみをコピーするので注意してください)
- また、コンパイラは定数に対する副作用のない演算の一部を最適化でコンパイル時に行うので、
変更のあるデータ構造の初期値にうっかりリテラルを指定すると以外な結果が得られることがあります。
次の手続きfooは、ペアpのcdrに引数をセットしてから(cdr p)を返していますが、
返り値はセットした値になりません:
(define (foo x) (define p '(head . tail)) (set-cdr! p x) (cdr p)) (foo 'newtail) ; -> tail ; newtailにならない
- これは、pが定数で変更されていないため、コンパイラが(cdr p)を定数としてコンパイル時に
(cdr '(head . tail)) => tail という計算をしてしまうためです。
(define p (cons 'head 'tail)) と、fooが呼ばれる度に新たなペアが作られるように
書くのが正しいです。
将来は警告を出せるようにするつもりです。
(この例はわざとらしいですが、実際のコードで起きたケースについては Some improvements of constant propagation 参照)
例2. ライブラリの手続き
;; append の戻り値を破壊的変更している例 (悪い例) (define x (list 3 4 5)) (define y (append (list 1 2) x)) (list-set! y 2 300) (print x) ; -> (300 4 5) (print y) ; -> (1 2 300 4 5)
- ライブラリの手続きの中には、引数と戻り値とでデータが共有されるものが存在します。
例えば、上記の append の場合、最後の引数と戻り値とで、データが共有されます。
これも、破壊的変更をすると、両方のデータが変更されてしまうことから、
immutable なデータとして考える必要があります。
(共有部分を理解した上で、あえて変更する場合もあるかもしれませんが。。。)
- この場合の対策としては、例えば、
mutable なコピーを取得してから、破壊的変更を行うようにします。
(ただし、list-copy はリストの背骨の部分のみをコピーするので注意してください)
;; append の戻り値をコピーしてから破壊的変更する例 (define x (list 3 4 5)) (define y (append (list 1 2) x)) (set! y (list-copy y)) (list-set! y 2 300) (print x) ; -> (3 4 5) (print y) ; -> (1 2 300 4 5)
まとめ/関連情報
- Scheme で破壊的変更を使う場合、
データが mutable であるかどうかを、よく確認する必要があります。
- ライブラリの手続きが immutable なデータを返すかどうかは、
マニュアル等でしっかり確認する必要があります。
マニュアルを読んでもよく分からない場合には、おおむね次のように判断できるようです。
- コンストラクタ は mutable なデータを返す
- copy 系の手続きは mutable なデータを返す
(copy 系の手続きであっても immutable なデータを返す場合には、
xxx/shared のような名前になることがあるようです (SRFI-13 等)) - それ以外の手続きは、特に記述がなければ、immutable なデータを返す
(mutable なデータを返す場合には、
newly allocated (SRFI-146 等) や freshly-allocated (SRFI-1 等) 等が、
説明に書いてあることが多いようです)
(ここでいう immutable は、R7RS 3.4 で述べられているものとは異なり、
「データの共有部分が存在して、破壊的変更を行うと、
他の変数の内容も変化してしまう」可能性があることを意味しています。
(すなわち、完全に共有部分の仕様を把握しているのでなければ、
実質的に immutable なデータと考えて、プログラミングをする必要があるということです))
- 「破壊的変更」に関連して、いくつかのsrfiでは、
「linear update (線形更新)」と仕様に定められているものがあります。
(例: GaucheRefj:take!。Gaucheでは「その場で更新」と訳されていることもあります)
これは効率のために引数を破壊的変更する「かもしれない」関数です
(必ず変更されるとは限りません)。
詳しくはScheme:LinearUpdateで説明します。
- immutable と破壊的変更を共存させると、どうしても問題が起こりやすくなります。
このため、いくつかの実装では、破壊的変更を排除したものがあるようです。
- このような実装を使うことで、データの不整合を心配せずにプログラミングを行うことができます。
また、データの共有やGC等の技術により、性能的にも大きな問題はないようです。
将来的には、Scheme 自体でも、破壊的変更を排除する提案が出されるかもしれません。
(R8RS とか。。。ただ、互換性を考えると難しいか。。。)
hamayama(2018/08/24 12:19:28 UTC)(2018/08/25 05:43:17 UTC)
(2018/08/26 06:41:16 UTC)(2018/08/28 06:03:54 UTC)
(2020/12/30 11:46:36 UTC)