Featured image of post コードガーデナー【Memento】戻れないと知っていたら、あの一鋏は切れたか〜状態は、外へ漏らさず預けておく〜

コードガーデナー【Memento】戻れないと知っていたら、あの一鋏は切れたか〜状態は、外へ漏らさず預けておく〜

記憶を頼りに逆方向の剪定コードで樹形を戻そうとして、さらに崩してしまう新人ガーデナー。庭師が教えるのは、切る前に状態そのものを控えておくMementoパターンだ。

冬の夜、渋滞の裏道を抜けて

結婚式場「ローズコート」の契約社員として配属されて、もうすぐ一年になる。三ヶ月前、長くローズガーデンを任されていたガーデナーさんが体調を崩して辞めてから、入口アーチのつるバラは私が一人で見ている。来月には大きな挙式が控えていて、アーチはその日、新郎新婦が最初にくぐる場所になる。式場のパンフレットの表紙にも、あのアーチの写真が使われている。失敗は許されない、というより、失敗したら誰も代わりに直せる人がいない。それが今の私の状況だった。

閉店後のガーデンを出て、自家用車に乗り込む。冬の夜は暗くなるのが早く、ヘッドライトの中を細かい雨が斜めに流れていた。裏道は渋滞していて、カーナビの表示を睨みながらのろのろ進む。助手席には、今朝測った枝の長さを走り書きしたメモが放り出してある。数字はいくつか、書いた本人にも読み取れないくらい崩れていた。急いで測ったせいもあるし、正直、もう自分の記憶をあまり信用できなくなっていたせいもある。

前任のガーデナーさんは、勘だけで見事な樹形に仕上げる人だった。メジャーも紙のメモも使わず、指先だけでこの枝は残す、これは落とす、と決めていくのを何度か見たことがある。切りすぎても、感覚でちゃんと元に戻せる人だった。私も同じようにやれば大丈夫だと思っていた。実際には、そうはいかなかった。

工房までの道の途中、外周を回りこむように庭師さんの入口へ差しかかる。枝を大きく広げた古木が、街灯に照らされて黒く浮かんでいた。今日はヘッドライトの端に一瞬映っただけで、速度を落とすことすらせずに通り過ぎた。それどころではなかった。

着くなり、庭師さんは軽く会釈しただけで、あとは無言のまま私のノートパソコンの画面に目を落とした。作業台の隅には、切り分けた挿し穂らしきものがいくつか転がっていて、まだ夜の作業の途中だったことがうかがえた。

「アーチの左右バランスが、また崩れちゃって……」

早口になっているのが自分でもわかった。声が上ずっているのも。ノートパソコンの液晶には、自分でこしらえた剪定記録用の小さなアプリが立ち上がったままだった。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
struct Branch {
    length_cm: u32,
    angle_deg: i32,
}

struct RoseBush {
    name: String,
    branches: Vec<Branch>,
}

// 剪定:指定した枝を根元から切り落とす(Vecから丸ごと除去)
fn prune(bush: &mut RoseBush, branch_index: usize) {
    bush.branches.remove(branch_index);
    // 切り落とした枝のデータはここで失われる。誰も控えていない
}

アーチに絡ませているつるバラには、今は4本の主枝がある。数日前、そのうちの1本がアーチの中心線からはみ出して見えたので、根元近くで切り落とした。branchesから要素を丸ごと取り除くだけの、単純な処理だ。切り落とした枝そのものは、剪定バサミで落とせばそれっきり土の上に転がるだけで、コードの側にも、その枝が本当はどんな長さと角度だったかを覚えておく仕組みは、最初から何もなかった。

「一回は、ちゃんと戻せたんです」

私はそう言い添えた。切り落とした翌日、実は切りすぎだったと気づいて、記憶を頼りに枝を"挿し戻す"関数を自分で書き足した。それがこれだ。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 依頼人が自力で追加した応急処置:記憶の推定値で挿し戻す「逆操作」
fn undo_last_prune(
    bush: &mut RoseBush,
    index: usize,
    estimated_length_cm: u32,
    estimated_angle_deg: i32,
) {
    bush.branches.insert(
        index,
        Branch {
            length_cm: estimated_length_cm,
            angle_deg: estimated_angle_deg,
        },
    );
}

「たしか150cm・角度30度くらいだったはず」と思って呼んだら、見た目はそれなりに整った。ほっとした。写真を撮って、これで大丈夫だと自分に言い聞かせた覚えがある。ところが数日後、今度は別の枝を同じように切りすぎてしまい、同じ関数で「たしか120cm・角度-5度」と挿し戻したところ、今度はアーチ全体の左右バランスがかえって崩れてしまった。一つの角度がわずかに違うだけで、アーチの上でどう見えるかはまるで別物になる。挙式まであと一ヶ月。焦りだけが先に立って、庭師さんの工房まで来た。

沈黙のまま、指が止まった場所

庭師さんの口から、言葉は出てこなかった。指先だけが、画面の上を静かに滑っていく。工房の奥から、夜の冷えた土の匂いがかすかに漂ってくる。窓の外では、雨が硬くなった土を打つ音が、ぽつぽつと途切れずに続いていた。

pruneの中身を追い、それから私が足したundo_last_pruneへと、庭師さんの目が移っていく。私はその隣で、視線がどこで留まるのかを、息を潜めて待った。急かしたくなる気持ちを、なんとか飲み込んだ。

「一回は、ちゃんと戻せたんです」

もう一度、同じ言葉を繰り返した。庭師さんは答えない。undo_last_pruneの引数、estimated_length_cmのところで、庭師さんの口が小さく開きかけて、そのまま声にならずに止まった。何かを言いかけて、やめたように見えた。指はまだそこから動かない。

「……何か、まずかったところ、ありますか?」

「その"たしか"は、どこから来た」

それだけだった。どこから来たも何も、自分の記憶からに決まっている。そう思いながらも、なぜかその一言が、胸のあたりに引っかかって残った。記憶からです、と口の中で答えようとして、でもそう言い切っていいのか、自分でも急に自信がなくなった。

見習いヒバリの一言

庭師さんの一言だけを受け取って、ヒバリさんが私へと向き直る。

「“たしか150cmくらい"って、覚えてただけですよね。実際に切り落とした枝、どこにも残ってないんです」

言われてみればその通りだった。pruneVec::removeで枝を取り除いた瞬間、切り落とした枝の本当の長さと角度は、コードのどこにも残っていない。手元にあるのは、私の記憶だけだった。土に落ちた枝そのものは、もう何日も前にとっくに片付けてしまっている。

「じゃあ、切る前に写真でも撮っておけばよかったってことですか」

一段浅い理解のまま、そう聞き返した。ヒバリさんが答える。

「近いですけど、パソコンの文章とかだと、取り消しボタン一発で戻せますよね。それに近い感じで——」

ヒバリさんは、ソフトウェア一般の取り消し機能を軽い喩えとして持ち出そうとした。でも、その喩えを最後まで聞く前に、思わず口を挟んでいた。

「……近いけど、たぶん違います。文章の取り消しって、多分どこかにちゃんと前の文字が残ってますよね。剪定はそうじゃない。切ったら、その枝の実物はもうこの世に無いんです」

自分でも驚くくらい、すらすらと言葉が出た。何年もバラに触れてきて、身体でわかっていることだった。切り落とした枝は、どんなに悔やんでも、もう同じ場所に生えてはこない。挿し木にして育て直すのだって、何ヶ月もかかる。

「だから、切る前の状態を、まるごと別に控えておくしかないんだと思います」

ヒバリさんが少し目を見開いて、それから頷いた。

「そうです、それです。剪定みたいに、切ったら元のデータが消える操作だと、後から"戻し方"を頑張って書いても意味がないんです。切る前に、状態そのものを丸ごと控えておく——これにはMementoという定石の名前が付いています」

Memento。パターン名として耳にするのは、これが初めてだった。

「Originator・Memento・Caretakerの3つの役割に分けて考えます。Originatorが今回のRoseBush——状態そのものを持つ本体です。Mementoは、その状態を切る前の瞬間にまるごとスナップショットしたもの。Caretakerは、そのスナップショットを控えておく係です」

ヒバリさんが、作業台の隅にあったメモ用紙を引き寄せると、鉛筆で四角を三つ描いて、線でつないだ。

「絵にすると、こんな関係です」

バラの木本体(RoseBush)が控え一枚(RoseMemento)をsaveで作って渡し、控え帳(PruningLog)がrecordで綴じ、undoで一番新しい控えを返す円環図

三つの四角と、それをつなぐ線を目で追う。切る・預ける・返す、というだけの単純な形なのに、自分が書いたundo_last_pruneにはこの形がどこにも無かったことに、今さら気づいた。

「じゃあ、私が今書いてるundo_last_pruneは、何が根本的にダメだったんですか」

undo_last_pruneは、切り落とした後で、記憶の推定値から新しく枝を作り直そうとしてます。でも本物の枝のデータは、切った瞬間にもう失われてる。推定が実物とたまたま近ければ誤魔化せますけど、ズレたら終わりです。一回目がたまたまうまくいったのも、正直、運が良かっただけだと思います」

私が最初に切った枝はlength_cm: 150, angle_deg: 30で、その時は記憶がたまたま一致して誤魔化せた。二回目に切った枝は、本当はlength_cm: 125, angle_deg: -8だったのに、「たしか120cm、-5度」と覚え違いをしたまま挿し戻していた。ズレはわずかでも、アーチ全体で見ると左右のバランスを崩すには十分だった。

「一回だけならまだしも、この先も切るたびに"たしか"を積み重ねていくと、だんだんズレが目立つようになると思います。切って、記憶で戻して、また切って、また記憶で戻して……を繰り返すほど、“たしか"の精度は落ちていきますよね」

ヒバリさんの言葉に、頷くしかなかった。実際、二回目の"たしか"は、一回目よりも自信がなかった。切ってから戻すまでの間隔が空いた分、記憶がぼやけていたのだと思う。

「最初は、前任のガーデナーさんの勘を真似すれば大丈夫だと思ってて……」

前任者の話をすると、自分の中でも改めて腑に落ちるものがあった。前任のガーデナーさんの勘は、たぶん本物だった。長年の経験に裏打ちされていて、記憶と実物のズレも小さかったのだろう。何年も同じアーチを見続けてきた人の記憶と、配属されてまだ半年の自分の記憶を、同じものとして扱おうとしていたのが、そもそもの見当違いだったのだと思う。私にはまだその勘がない。勘のない人間が、勘に頼る仕組みを真似ても、うまくいくはずがなかった。

さっきのメモの続きに、ヒバリさんが今度は二本の矢印を描き足した。上下に並んだ、似ているようで途中から枝分かれする矢印だった。

「今までやってたことと、これからやることを並べると、こうです」

Beforeは切ってから記憶で挿し戻すため実物とズレることがあり、Afterは切る前に控えてそのまま正確に戻ってくることを示す上下対比図

上の矢印は、切ってから記憶を辿って、途中で心もとない実線に変わっていた。下の矢印は、切る前に一本、控えへ向かう線が生えている。それだけの違いなのに、自分がこの一ヶ月やってきたことの弱さが、急に形になって見えた気がした。

手入れ ── 逆操作から、控え帳へ

ここでようやく、庭師さんの手がキーボードへ伸びた。まず、BranchRoseBushCloneというトレイトを付ける。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#[derive(Clone)]
struct Branch {
    length_cm: u32,
    angle_deg: i32,
}

#[derive(Clone)]
struct RoseBush {
    name: String,
    branches: Vec<Branch>,
}

Cloneは、値をまるごと複製できるようにするトレイト(型がどんな振る舞いを持つかを表す契約)だ。#[derive(Clone)]を付けるだけで、RoseBushが持つフィールドを一つずつ、律儀に複製してくれるようになるらしい。このRoseBushが、状態そのものを持つ本体——Originatorだ。

「これ、branchesの中のBranchも、ちゃんと1本ずつ複製されるんですか? リストの入れ物だけコピーされて、中身は元のと同じもの、みたいなことにはならないですか」

自分でも意外なほど鋭い質問が出た。ヒバリさんが頷く。

「いい質問です。derive(Clone)は、フィールドを一つずつ、それぞれの型のCloneを使って複製します。BranchにもCloneが付いているので、branchesの中身も1本ずつちゃんと複製されます。手を抜いて"入れ物だけ"みたいなことはしません」

ヒバリさんが手元を覗き込みながら言う。

「これでRoseBushをまるごと複製できます。次は、この複製を外から中身を触れない形に包みます」

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// Memento: RoseBushの状態を丸ごと不透明に保持する。フィールドは非公開(狭いインターフェース)
struct RoseMemento(RoseBush);

impl RoseBush {
    fn save(&self) -> RoseMemento {
        RoseMemento(self.clone())
    }

    fn restore(&mut self, memento: RoseMemento) {
        *self = memento.0;
    }
}

RoseMemento(RoseBush)は、フィールド名を持たないタプル構造体で、中身にはmemento.0のようにインデックスでアクセスする。ただし今回のように同じファイル(モジュール)の中に書く限り、非公開フィールドでもモジュール内からは触れてしまう——実運用ではRoseMementoを専用のモジュールに切り出して初めて、外から.0へ直接アクセスできない境界になる。中身を取り出したり書き換えたりする手段をsaverestoreの2つに絞る、という設計そのものは変わらない。

ヒバリさんの声がかぶさる。

「これがMementoです。控えを持つ側は、中身がどうなってるか気にしなくていい。ただ、預かって、返すだけです」

「じゃあ、控え帳の方から、控えてある枝の長さを勝手に書き換えたりはできないってことですか」

「今回のサンプルは全部同じファイルにまとめているので、実は触ろうと思えば触れてしまいます」ヒバリさんが頷く。「実際に運用するときは、RoseMementoだけ別のファイルに切り出すんです。そうすると、saverestoreを通さない限り、本当に中身を書き換えられなくなります。控え帳が信用できるのは、まさにその境界のおかげです」

続けて、控えを管理する役割が出てくる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
use std::collections::VecDeque;

// Caretaker: 依頼人の「控え帳」。古い控えは自動的に間引く
struct PruningLog {
    history: VecDeque<RoseMemento>,
}

impl PruningLog {
    const CAPACITY: usize = 5; // 控え帳も、無制限には増やさない

    fn new() -> Self {
        PruningLog { history: VecDeque::new() }
    }

    fn record(&mut self, bush: &RoseBush) {
        if self.history.len() == Self::CAPACITY {
            self.history.pop_front(); // 一番古い控えから間引く
        }
        self.history.push_back(bush.save());
    }

    fn undo(&mut self, bush: &mut RoseBush) {
        if let Some(memento) = self.history.pop_back() {
            bush.restore(memento);
        }
    }
}

VecDequeは、先頭からも末尾からも出し入れできる待ち行列(キュー)の標準ライブラリの型だ。PruningLog——控え帳——は、このVecDequeに控えを積んでいく。

ヒバリさんが言葉を継ぐ。

「控え帳も、無制限に増やしていいわけじゃないんです。控え1件はRoseBushをまるごと複製したものなので、際限なく積み続けるとその分メモリを食い続けます。CAPACITYを超えたら、一番古い控えからpop_frontで間引きます」

「剪定と同じですね。増やしっぱなしにしない」

思わずそう言うと、ヒバリさんが小さく笑った。

「そうです。控え帳は5件まで。それより前の控えは、もうさかのぼって戻す機会もほとんどないので、間引いてしまって大丈夫です」

最後に、pruneそのものが書き換わる。

1
2
3
4
fn prune(bush: &mut RoseBush, log: &mut PruningLog, branch_index: usize) {
    log.record(bush); // 切る前に、必ず控えを取る
    bush.branches.remove(branch_index);
}

庭師さんが、キーボードから手を止めずに短く言った。

「変わったのは、これだけです。切り落とす処理自体は前と同じ」

「じゃあ、undo_last_pruneは……」

「もう要りません」ヒバリさんが引き取る。「控え帳に、切る前の本物の状態がそのまま残ってるので、記憶の推定値を渡す必要がなくなります」

undoは、控え帳から一番新しい控えを取り出して、そのままRoseBushへ戻すだけだ。推定値を計算する処理はどこにもない。

「これ、切る前の状態がそのまま戻ってくるってことですよね。記憶に頼らなくていい」

ヒバリさんの返事は、さらに続いた。

「はい。それに、複数回切った後でも、1回ずつ正確に巻き戻せます。控えは切るたびに積み重なっていくので、二回切ったら二回分、控え帳に積まれています。一番新しい控えから順番に、1本ずつ戻していけます」

「一気に全部を戻すんじゃなくて、1本ずつ、ってところがいいですね。実際、右側の枝は問題なかったので、左側の分だけ戻したかったんです」

「そうです。控え帳は積んだ順に戻すので、途中で"ここまで戻せば十分"というところで止められます」

「つまり、“戻す"を頑張って書くんじゃなくて、“戻れる場所"を先に作っておく、ってことですね」

自分の言葉で、そう言い直してみた。核心を掴めた気がした。

「そうです」ヒバリさんが頷く。「控え帳の中身は誰も覗きません。ただ、預かって、必要なときに返すだけです」

見送り ── もう一度、老木の前で

試しに何度か枝を切って、控え帳から戻してみた。切る前の状態と、フィールド一つひとつまでぴったり一致した。記憶で推定していた頃には無かった手応えだった。角度の数字を見比べて、これなら来月の挙式も任せられる、と少しだけ肩の力が抜けた。

「前任のガーデナーさんの勘は、たぶん本物でした。でも私はまだその勘を持っていない。だから、勘の代わりに控えを持っておくことにします」

そう、自分の言葉で言い切ってみた。ヒバリさんが小さく頷いたのが見えた。

帰り道、もう一度あの古木の前を通る。ハンドルを握る手から、無意識にアクセルの力が抜けているのに気づいた。雨はいつの間にか止んでいて、街灯の光が濡れた幹をぼんやり照らしていた。

「これ、何の木なんですか」

そう聞きかけて、でも口に出す前に、なんとなく「今度でいいか」と自分の中に仕舞った。由来を知りたい気持ちはあったけれど、今夜はそれより先に確かめたいことがあった気がした。控え帳のおかげで、いつでも戻れる場所があるとわかっている今、無理に今すぐ知らなくてもいい。そう思えた。老木の由来も、今ここで急いで聞き出さなくても、また来る機会に聞けばいい。そう思うと、不思議と気が楽になった。

自家用車に乗り込み、渋滞の抜けた裏道を戻る。来月の挙式には、控え帳を持ったままアーチのつるバラを仕上げることになる。今度は、記憶を頼りに切るのではなく、切る前に必ず控えを取ってから鋏を入れられる。そのことだけは、今夜の収穫として、確かに車に積んで帰れる気がした。


手入れ記録

こんな症状が出たら入れるべき手入れ(パターン)まだ様子見でいい
操作前の状態を記録しておらず、失敗時に記憶や推定で復元しようとしている✓ Memento(状態のスナップショット保存)
復元のたびに逆方向の計算・推定値を組み立てるコードを書いている✓ Memento(状態のスナップショット保存)
操作そのものが単純で、いつでも同じ結果に戻せる(副作用がない・べき等)✓ 現状のまま様子見

手入れの手順

  1. 状態を破壊的に変更する操作(今回ならprune)の直前に、状態全体を複製する処理を挟む
  2. 複製した状態を、外部からフィールドへ直接アクセスできない形(非公開フィールドの型)に包む
  3. 複製を溜めておく履歴(スタックやキュー)を用意し、上限を設けて古いものから間引く
  4. 復元処理は、履歴から複製を取り出して丸ごと差し戻すだけにする。推定値や逆算のロジックを書かない
  5. 複数回の操作を1回ずつ正確に巻き戻せることをテストで確認する

庭師の一言

土に根を張ったままなら、思い切り鋏を入れていい。

見習いの手控え

記憶で戻そうとすると、覚えていた数字が本物とズレていたときに気づけません。控え帳は、本物の状態をそのまま預かって、そのまま返すだけです。“戻し方"を頑張って書くんじゃなくて、“戻れる場所"を先に作っておく——今回、ようやくその意味がわかった気がします。

comments powered by Disqus
システム開発・AIワークフローのご相談は Meetsource
Hugo で構築されています。
テーマ StackJimmy によって設計されています。