動かした石は、二度と戻らない
うちはリン。庭師をやっている。石を組み、灯籠を据え、植木を配し、依頼された庭の姿を仕上げるのが仕事だ。
いきなり本物の庭に手を入れるわけじゃない。まずは「意匠板」——石や灯籠や植木を模した符を、盤の上で動かしたり消したりして、実際に据える前に配置を試すための道具を使う。頭の中の思いつきを、盤の上で先に確かめる。それが、うちの仕事のやり方だった。
ところがこの意匠板、動かす・消すの操作が、盤のデータをその場で直に書き換えるだけの作りになっている。一度動かした石も、消した灯籠も、二度と元の位置・元の姿には戻らない。取り消しも、やり直しも、履歴も、何も無い。
頭の中では「もっとこう配置したら映えるのに」と分かっていても、うちはいつからか、大胆な配置換えを試すのをやめてしまっていた。戻せない恐れが、先に立つ。腕は落ちてないはずなのに、うちの手がけた庭は、いつからか似たり寄ったりになっていた。
先日、決定打があった。ある由緒ある庭で、代々受け継がれてきた灯籠を一つ、意匠板の上で「試しに」動かしてみたことがある。
| |
data class は、値をひとまとまりに運ぶための器だ。copy() は、一部だけ変えた新しい姿を作る書き方——今回なら、xとyだけを変えた新しいShapeを作って、そのまま盤に置き直している。
問題は、その置き直し方にある。moveもremoveも、盤のデータをその場で直接書き換えるだけで、「動かす前」「消す前」の姿をどこにも控えていない。一度上書きしたら、元の値はもう覚えていない——それだけのことだ。
あの灯籠を試しに動かした後、うちは思ったより見栄えが悪く、元の位置に戻そうとした。だが盤には、元の位置を覚えている仕組みが無い。うちは自分の記憶と控えのスケッチだけを頼りに、位置を勘で逆算して打ち返した。
| |
盤は「間違っている」とは一切教えてくれない。淡々と、間違った位置を返すだけだ。据えてから、依頼主に「前より心持ち右に寄っている気がする」と指摘され、うちは冷や汗をかいた。幸い大事には至らなかったが、二度とやりたくない。
名より先に、仕組みの評判
花市場に集う庭師仲間うちで、ここのところ、ある噂を耳にしていた。
「丘のアカデミーの見習いが、何かを直したらしい」。ただし、その見習いの名前より先に、直した中身のほうが先に伝わっていた。「切れ味のいい魔法を使えるようになった、って話じゃない。“やり直しがきく"仕組みを作ったらしい」。
うちは、その言い回しに引っかかった。名前より先に、成果の中身が伝わってくる。よほどのことなのか、あるいはただの尾ひれか。どちらにせよ、一度見ておく価値はありそうだった。うちは道具を仕舞い、アカデミーの丘を上った。
工房は、本棚と、宙にふわふわ浮かぶ光の粒でいっぱいだった。うちは物珍しさよりも、まず部屋の隅にちらりと目をやった。白髪の年寄りが一人、椅子に座っている。庭仕事とは縁のなさそうな、隠居の爺さんだろう——それだけ思って、うちはすぐに目を逸らした。
肝心の見習いは、机のほうにいた。一枚の護符を、几帳面に一つ折っては、指先でそっと元の折り目まで開き戻し、寸分違わず同じ形に戻るかを確かめてから、また折り直す、ということを飽きずに繰り返している。
「……戻せる形のまま、進めているか」
そう独りごちながら。うちが声をかけると、護符を置いて顔を上げた。
「あ……意匠板の、ご相談ですか」
これが噂の見習いか。派手なところは、どこにも無い。
うちは、意匠板の写しと、灯籠のずれの一件を語った。ルーク——と名乗ったその見習いは、写しをじっと見て、静かに言った。
「これは……一度上書きしたら、元の値はもう、どこにも残っていませんね。動かす前、消す前の姿を、盤がまるで覚えていない」
「そうなんだよ」とうちは頷いた。「だから、うちは怖くて、大胆には動かせねえ」
ルークが名付けたところによると、これは取り消し・履歴のできない手続き的操作——操作が盤のデータを直接書き換えるだけで、元へ戻る道がどこにも用意されていない作り、なんだそうだ。
「一度動かしたら、もう後戻りはできねえんだ」。うちの言葉を、ルークは黙って受け止めていた。
一つに、まとめておけば
ルークは写しをしばらく見つめてから、うちに向かってでなく、低く独りごちた。
「この手順……逐一控えておけば、あとから読み解いて、戻す分岐を書けば済む」
宣言というより、静かな見積もりだった。うちは机の端から、それをちらりと見ているだけだった。
ルークは、操作の記録を一つの型にまとめ始めた。
| |
移動の巻き戻しは書けた。削除の巻き戻しも、消した符を覚えておけば書けた。だが、追加の巻き戻しを書こうとして、ルークの手が止まった。
「追加の巻き戻しは、消すだけでいいのに……この記録の型には、もう使わない場所が、いくつも空いている」
dx・dy・removedShape——操作の種類によっては、nullのまま残る場所が目立ってくる。移動の記録には削除の情報が要らず、削除の記録には移動の情報が要らない。それでも同じ一つの型に収めようとするから、使わない場所が空いたままになる。
ルークの手が止まったのを見て、うちの中で、あの半日の冷や汗がふっと蘇った。
(……やっぱり、そう簡単にはいかないのか)
声には出さない。ただ、期待した分だけ、肩が強張るのを感じた。
隅の年寄りは、あいかわらずそこに座っていた。小さな砂時計を手に取り、無言でひっくり返しては、また戻す、ということを繰り返している。うちにはそれが、暇つぶしの手すさびに見えた。何をしているのかは、気にも留めなかった。
この折り目と、同じだ
ルークは、書きかけの記録から手を離した。机の端に置いていた、さっき折っていたあの護符を、ふと取り上げる。指先でゆっくり、元の折り目まで開き戻してみせながら、低く呟いた。
「……この折り目と、同じだ」
うちには、何のことか分からなかった。ルークは続けた。
「一つの記録に、全部の操作の"戻し方"を詰め込もうとしていた。でも、移動を戻すのに要る情報と、削除を戻すのに要る情報は、そもそも形が違う」
そこで、ルークは少し考えるように間を置いた。
「前に別のご依頼で、“巨大な分岐"を見たことがあります。あのときは、あり得る状態の組み合わせを、一つ残らず先回りして潰そうとして、失敗しました。でも、今度のこれは違う。組み合わせを網羅しようとしているんじゃない。一つの入れ物に、形の違う戻し方を、無理に詰め込もうとして歪んでいるんです」
うちには魔術のことは分からないが、その口ぶりからは、似たような壁に二度目にぶつかっているんじゃない、ということだけは伝わってきた。
「操作ごとに、戻すための情報の形が違うなら……」ルークが呟く。「操作そのものを、それぞれ自分で戻せる一つの実体にすればいい。この護符みたいに、一つひとつが、自分の折り目を、自分で覚えていればいい」
誰にも急かされたわけじゃない。ルークは、自分で手を止めて、自分で気づいたようだった。
操作そのものを、一枚の呪符に
ルークは、新しい設計に取りかかった。
「一つひとつの操作を、実行も、取り消しもできる形にします」
| |
interface——「こう呼べばこう応える」という約束の形だ。今回は、execute(実行する)とundo(取り消す)という、二つの口を持つ約束にした。
| |
「移動なら、動かした分だけ。削除なら、消した姿だけ。それぞれが、自分の戻し方に要る情報だけを持ちます。他の操作のことは、何も知らなくていい」
操作そのものを、実行も取り消しもできる一つの実体にする——これがCommand、操作をオブジェクトとして表現し、実行だけでなく取り消しも扱えるようにする定石だ、とルークは言った。
うちは、さっきの空いた場所だらけの記録を思い出した。「なるほどな……一つにまとめようとするから、余計な空きができる。それぞれが自分のことだけ覚えてりゃ、余計な場所は要らないってことか」
「はい」
DeleteCommandのremoveについて、ルークが付け加えた。「盤のremoveも、このタイミングで書き換えました。消した中身を、そのまま返すようにしたんです」。
| |
「前は、消したら何も返さない作りでした。消す前にわざわざ確認しにいかなくて済むように、MutableMapのremoveが持っている、消した値をそのまま返してくれる——無ければnullだ、という性質を、そのまま活かす形にしました」
一つ、うちには気になることがあった。
「庭の"全体の写し"を、動かすたびごっそり取っておけば、よくないのか。一手ごとに、盤ぜんぶの姿を保存しておくとか」
ルークは首を振った。
「写しを毎回まるごと取ると、庭が大きくなるほど写しが重くなりますし……それに、“何をしたか"という記録そのものが消えてしまいます。操作そのものを覚えておけば、軽いですし、“何をしたか"も一緒に残ります」
なるほど、とうちは思った。全部を丸ごと保存しておくやり方と、やったことだけを覚えておくやり方は、似ているようで違う。
続けて、ルークは操作を積んでおく係を書いた。
| |
ArrayDequeは、後に積んだものから先に取り出せる、一列の入れ物だ。addLastが積む、removeLastOrNullが、直近に積んだものを取り出す——ただし、中身が空ならnullが返る。
「積んだものが無かったら、どうなる」とうちは訊いた。
「removeLastOrNullは、積んだものが無ければnullを返します。?: return——nullだったら、そのときは何もせず帰る、という書き方です。“魔力が空かもしれない"を先に認めておく、というのと同じ考え方です」
「一度戻したものを、またやり直せるのか」
「はい。redo用にも別に積んでおきます。ただし、戻した後に新しい操作をしたら、やり直し履歴は捨てます。一度枝分かれした先の"もしも"を、また辿れてしまうと、庭の姿がどっちつかずになりますから」
ルークが呪文を書き終えると、机の上には何枚もの呪符と、それらを収める魔導書のような履歴管理の入れ物が整然と並んでいました。 うちは腕を組み、頭の中でそのつながりを反芻してみます。
「……なるほどな。全体の流れがようやく見えてきたぞ。うちが操作をするたびに、この履歴管理の魔導書が呪符を預かり、戻すときはその呪符自身に元に戻す術を実行させる。そしてすべての呪符は、うちの意匠板を裏で操作している……こういうことかい?」
「はい、その通りです」ルークは誇らしげに頷き、一枚の羊皮紙にその関係図を描き出してみせました。

「こうして見ると、実に綺麗だな」とうちはため息をつきました。「魔導書は呪符の共通の約束(インターフェース)しか知らないから、これから新しい操作が増えても、魔導書そのものを書き換える必要がないってわけだ」
紙の庭に、石を置いて
「本物の庭に石を置く前に」とルークは言った。「結界の中に紙の庭を敷いて、一手ずつ確かめます」
模擬詠唱——本番の前に、安全な形で試しに動かして確かめる作法だ。
| |
「assertEqualsは、動かした後の姿が、狙いどおりかを照らします。動かして(13, 5)、戻して、ちゃんと(10, 5)に正確に戻ることを確かめました」
続けて、削除の巻き戻し。
| |
「消す前の姿を、消す前とまったく同じ形で戻す。うちの、あの灯籠のときの悩みに、そのまま応えます」
うちは、あの日のことを思い出しながら、その一行を目で追った。勘で逆算する必要が、もう無い。
さらに、複数の操作を重ねてから戻す検め。
| |
「移動して、削除して、追加して。3回戻せば、最初の庭にきっちり戻ります。3回やり直せば、また同じ庭に」
異常系も、二つ検めた。
| |
「積んだものが無いときに戻そうとしても、何も壊れません」
そして——戻した後に新しい一手をすれば、捨てた"もしも"はもう辿れないことも確かめた。
| |
「一度戻した後に、別の一手をすれば、捨てたやり直し先は、二度と戻ってきません。何度redoを呼んでも、盤の姿は変わらないままです」
結界の中の模擬詠唱は、すべて、狙いどおりに通った。
怖くなくなった、意匠板
書き直された意匠板を、うちは受け取った。石も灯籠も、動かす・消すが、それぞれ戻せる一つの実体になっている。
「……これなら、思い切って動かしても、平気なのか」
「はい」とルーク。「動かしても、消しても、一手ずつ、確実に戻せます」
うちは、あの由緒ある灯籠を勘で戻した半日を思い出した。「勘で戻す必要が、もう無いんだな」
念のため、うちは念を押した。「これで、うちがどんな配置を試しても、絶対に大丈夫なのか」
ルークは正直に答えた。「戻す・やり直すが、ちゃんと動くことは確かめました。ただ、それぞれの操作の"戻し方"を、僕が間違えて書けば、その一手はやはり正しく戻りません。何でも自動で直る魔法、ではないんです」
「……なるほどな」とうちは笑った。「戻れる仕組みがある、ってだけで、十分だよ」
試しに、大胆に動かしてみる
うちは、少し迷ってから切り出した。「試しに、うちの手でやってみていいか」
意匠板の上で、これまで避けてきた大胆な配置換えを試す。最初の一手は、指先がまだ少し強張っていて、動かす手が一瞬迷った。だが、undoで戻せることを確かめると、二手目からは迷いが薄れていく。もう一段大胆にして、またundo。最後に、これまでで一番気に入った並びに、redoで戻す。
「……こんなに、気軽に試せるとは思わなかった」
指の強張りは、いつのまにか消えていた。
帰り支度をしながら、うちはふと、隅の年寄りに目をやった。あいかわらず、砂時計をひっくり返しては戻す、それだけをやっている。ルークに何を言うでもない。
工房を出る間際、ルークがその年寄りに軽く声をかけるのが、うちの耳に届いた。
「お師匠さま、また」
……お師匠さま。うちは、そこで初めて腑に落ちた。ああ、道理で。あの隠居の爺さんだと思っていた年寄りは、名の知れた大魔術師だったらしい。ルークが最後まで、誰の助けも借りず、自分の手だけで戻ってきた理由が、少し分かった気がした。
作業小屋へ戻ったうちは、まず意匠板の新しい仕組みを試しに動かしてみた。動かして、消して、戻して、またやり直して。手が、自然と大胆になっている自分に気づく。
——戻せる、というだけで、うちはこんなに自由になれるのか。
似たり寄ったりだった庭に、今度はうちの気に入った並びを、一つ足してみよう。
📖 見習い魔術師ルークの魔導書メモ(Luke’s Spellbook Notes)
- 修復した呪文(クラス/モジュール名):
Command(execute/undoの約束)、MoveCommand/DeleteCommand/AddCommand(各操作の実体)、CommandHistory(undo/redoの履歴係) - 乱れの要因(アンチパターンの課題):
- 意匠板の
move/removeが、盤のデータをその場で直接書き換えるだけで、動かす前・消す前の姿をどこにも控えていなかった。取り消し・履歴・再実行の仕組みが一切無く、依頼人(リン)は毎回、自分の記憶を頼りに逆算して手作業で戻すしかなく、実際に一度、勘で戻した位置がずれて依頼主に指摘された。
- 意匠板の
- 過剰設計の反省(背伸びの過ち):
- 操作の記録を一つの型(
ActionLog)にまとめ、undo時に種類ごとの分岐で読み解けばいいと考え始めた。だが操作ごとに巻き戻しに必要な情報の"形"がまるで違うため、一つの記録型に無理に詰め込むと、使わないフィールドがどんどん増えて歪んでいくことに、自分で気づいて手を止めた。以前の"巨大な分岐"は、状態の組み合わせを網羅しようとして失敗したが、今回はそれとは別種——一つの入れ物に、形の違う戻し方を詰め込もうとする歪みだった。
- 操作の記録を一つの型(
- 魔術の定石(学んだ設計パターン):
- Command。操作そのものを
execute/undoを持つ一つの実体にし、各操作が自分の巻き戻しに必要な情報だけを持つようにした。履歴はundo用・redo用の2本のスタックで管理し、戻した後に新しい操作をすればやり直し履歴を捨てる、という定石も併せて実装した。
- Command。操作そのものを
- 模擬詠唱の記録(検証のポイント):
- 各Commandのexecute/undoが正確に戻ること、任意の操作列をundo/redoできること、履歴が空のときのundoが安全に何もしないこと、undo後に新しい操作をするとredo履歴が破棄され、繰り返しredoを呼んでも状態が変化しないことを確認した。
- ちなみに:
- 今回のように「何をしたか」を覚えておくやり方をCommandと呼ぶ。一方、「状態そのもの」を丸ごと保存しておいて後で戻す考え方はMementoと呼ばれる別の定石で、何を戻すか=Command、どう戻すか=Memento、と役割が違う。undo/redoの履歴は、2本のスタックでなく、1本のリストとインデックスで管理するやり方でもよい。
- リンの手が覚えたこと(今回、師匠マグナスは終始無言で、砂時計をひっくり返すばかりだった。だから、これは師の格言ではなく、うちがこの日持ち帰った一言):
- 「戻せる、というだけで、うちはこんなに自由になれるのか。」
