Featured image of post コードドクター【Memento】多重記憶喪失症候群〜消えた最高の一杯と記憶カプセルの処方箋〜

コードドクター【Memento】多重記憶喪失症候群〜消えた最高の一杯と記憶カプセルの処方箋〜

金曜日の夜。私はキッチンのテーブルに置いたノートPCの前で、途方に暮れていた。

画面には、ずらりと並んだYAMLファイルの名前。roast_profile.yamlroast_profile_backup.yamlroast_profile_v2.yamlroast_profile_old.yamlroast_profile_final.yamlroast_profile_best_maybe.yaml——。

「……どれだっけ」

私の名前は深川陽菜。28歳。元スペシャルティコーヒー専門店のバリスタで、今はフリーランスのWebエンジニア1年半目。勤めていたカフェが閉店して、独学でプログラミングを始めた。最初の個人プロジェクトとして作っているのが「焙煎プロファイル管理ツール」。焙煎の温度曲線、時間、ファンの回転数——そういうパラメータを記録して、最高の一杯を再現するためのツールだ。Perlで書いている。理由は聞かないでほしい(最初に買ったプログラミングの本がPerl入門だっただけだ)。

3日前、奇跡が起きた。温度を少し下げて、時間を短くしたら、今まで飲んだことのないような透明感のある味になった。エチオピア・イルガチェフェの花のような香りが、くっきりと立ち上がった。

でも、その後に3回パラメータを変えて実験してしまった。そして気づいた——あの時の設定がどれだったか、もう分からない。

「バックアップ取ってたのに……」

取ってた。cp コマンドで。roast_profile_backup.yamlroast_profile_v2.yamlroast_profile_final.yaml。でも、どれがいつの時点のコピーなのか、ラベルも日付も何もない。中身を開いても全部同じに見える。

ネットで「コード 設計 相談」と検索していたら、不思議なページがヒットした。「コード診療所」——コードの病気を治す、とだけ書いてある。レビューも口コミもない。住所は雑居ビルの2階。

胡散臭い。でも、もう3日間、この問題をどうにかしようとして堂々巡りしている。意を決して、翌朝そのビルに向かった。

来院

磨かれたリノリウムの床。蛍光灯の少し古めかしい光。廊下の奥に、重い鉄の扉があった。白地に黒文字のプレートに、飾り気のないゴシック体で「コード診療所」とだけ記されている。

恐る恐る扉を 引いた

最初に目に入ったのは、壁一面のO’Reilly本だった。ラクダ、犀、蝶——動物たちがぎっしりと詰まった本棚が、入口のすぐ横まで侵食している。足の踏み場もないほどの技術書の山。そしてその奥に、整頓された受付カウンター。さらにその背後には、トリプルディスプレイに何かを映した男の背中。

「あの……すみません、予約とかなくて大丈夫ですか——」

男は振り向かない。

「大丈夫ですよ、ここはコード診療所です」

声は男の背中からではなく、横から聞こえた。受付カウンターの奥から、助手のナナコと名乗ったその人が穏やかに微笑みながら現れた。白いブラウスの上に薄いカーディガン。眼鏡の奥の目が、安心させるように細められている。

「どうぞ、お掛けになってください」

促されるまま、用意されていたパイプ椅子に座る。ノートPCをぎゅっと胸に抱いた。


主訴

「あの……見てもらいたいコードがあるんですけど……」

ナナコさんが穏やかに頷いた。「もちろんです。どんな症状でしょうか?」

私はノートPCを開いて、画面を見せた。ターミナルに映る ls の結果。

1
2
3
4
5
6
roast_profile.yaml
roast_profile_backup.yaml
roast_profile_v2.yaml
roast_profile_old.yaml
roast_profile_final.yaml
roast_profile_best_maybe.yaml

「3日前に焙煎した豆が、今までで一番おいしかったんです。でも、その後3回パラメータを変えて上書きしちゃって……あの時の設定がどれだったか、もう分からないんです」

言いながら、目頭が熱くなった。たかがコードの話で泣きそうになるのは恥ずかしかったけど、あの一杯は本当に特別だったのだ。

奥のトリプルディスプレイの前の男——先生、と呼ぶべきなんだろうか——が、初めてこちらを向いた。正確には、私のノートPCの画面を向いた。目線が私の顔をすっ飛ばして、直接画面に吸い込まれていく。ちょっと怖い。


触診

先生が無言で私のノートPCの前に来た。眉間にしわを寄せて、ディレクトリの ls -la の結果を見つめている。

長い沈黙。

「……6つ。全部、同じだ」

え?

ナナコさんがすかさず通訳してくれた。「先生がおっしゃっているのは、これらのファイルが全部同じ時点のコピーではないか、ということです。いつの時点を保存したか、ラベルが貼られていないんですね」

「え……でも backup って名前に……」

先生は私のPerlスクリプトを開いた。RoastManager.pm——焙煎プロファイルを管理するモジュールだ。先生の指が、ある行の上で止まった。

 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
28
29
30
31
32
33
34
package RoastManager;
use v5.36;

sub new($class) {
    bless {
        profile => {
            temp_start  => 180,
            temp_peak   => 220,
            duration    => 720,    # 秒
            fan_speed   => 65,     # %
            bean_type   => 'Ethiopia Yirgacheffe',
        },
    }, $class;
}

sub profile($self) {
    return $self->{profile};
}

sub update_profile($self, %params) {
    # 直接上書き——前の値は永遠に消える
    $self->{profile}{$_} = $params{$_} for keys %params;
}

# バックアップ? cpで十分でしょ……
sub save_backup($self, $suffix = 'backup') {
    # 本当はYAMLに書き出してcpしてた
    # でもどれがいつの状態かもう分からない……
    my $filename = "roast_profile_${suffix}.yaml";
    # system("cp roast_profile.yaml $filename");
    return $filename;
}

1;

先生の指が update_profile を指した。ずっと無言。

「コーヒーで例えていいですか?」ナナコさんが私に聞いた。え、私に聞くの? 「もちろんです——あ、はい、お願いします」

「深川さん、テイスティングノートってつけますよね?」

「はい、毎回つけてます」

「このコードは、テイスティングノートに上書きで書き続けているようなものなんです。新しいメモを書くたびに、前のメモが全部消えている。どの豆がどの味だったか、分からなくなっている状態ですね」

——あっ。

「それ、まさにそれです!!」

思わず声が大きくなった。先生が少し目を細めた。ナナコさんがかすかに微笑んでいるように見えた。


診断

先生がホワイトボードに一言だけ書いた。

「多重記憶喪失」

ナナコさんが補足する。「先生の診断は多重記憶喪失症候群です。過去の記憶——つまり状態のスナップショットが、無秩序にコピーされた偽の記憶カプセルとして散乱しています。記憶の保護膜が欠損していて、どれが本物か判別できない状態ですね」

「偽の記憶カプセル……roast_profile_best_maybe.yaml のことですか」

「はい。maybeがついている時点で、もう誰にも分からないんです」

グサッときた。

ホワイトボードの前で診断を告げる先生と、それを説明するナナコ、衝撃を受けてノートPCを抱えたまま固まる陽菜


処方箋

先生がおもむろにキーボードの前に座った。新しいファイルを作り始めている。驚くほど速い。

記憶カプセルの作成

ナナコさんが画面を覗き込んで、私にも見えるように少し角度を変えてくれた。

「先生が作っているのは記憶カプセルです。焙煎プロファイルの状態を丸ごと、密封した瓶に詰めるイメージですね」

密封した瓶。

「豆を保存する密閉キャニスターみたいなものですか!」

ナナコさんが一瞬、目を丸くした。「……まさにそうです」。予想外に的確な比喩が出たことに、少し驚いているようだった。

ドクターがコードを書く横で、豆の入った密閉キャニスターのホログラムをナナコから説明されて驚く陽菜

先生が書き上げたコードが画面に映った。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package RoastMemento;
use v5.36;
use Storable 'dclone';

sub new($class, $state, $label = '') {
    bless {
        state     => dclone($state),
        label     => $label,
        timestamp => time(),
    }, $class;
}

sub state($self)     { dclone($self->{state}) }
sub label($self)     { $self->{label} }
sub timestamp($self) { $self->{timestamp} }

1;

「あ、dclone……深いコピーですか?」

先生がかすかに頷いた——ように見えた。

ナナコさんが説明する。「そうです。 Storable::dclone で深いコピーを取っています。キャニスターに豆を入れるとき、袋ごと移し替えるのではなく、豆そのものを別の容器に詰め替えるイメージです。元の袋を動かしても、キャニスターの中身には影響しないんですね」

なるほど。浅いコピーだと、同じ豆袋を参照しているだけだから、元が変わったら中身も変わってしまう。深いコピーなら、完全に独立したスナップショットになる。コーヒーで考えると、すんなり分かる。

記憶の持ち主の改修

先生は次に、私の RoastManager.pm を書き換え始めた。

 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
28
29
30
31
32
33
34
35
36
package RoastManager;
use v5.36;
use Storable 'dclone';
use RoastMemento;

sub new($class) {
    bless {
        profile => {
            temp_start  => 180,
            temp_peak   => 220,
            duration    => 720,    # 秒
            fan_speed   => 65,     # %
            bean_type   => 'Ethiopia Yirgacheffe',
        },
    }, $class;
}

sub profile($self) {
    return $self->{profile};
}

sub update_profile($self, %params) {
    $self->{profile}{$_} = $params{$_} for keys %params;
}

# --- Memento 対応 ---

sub save_to_memento($self, $label = '') {
    RoastMemento->new($self->{profile}, $label);
}

sub restore_from_memento($self, $memento) {
    $self->{profile} = $memento->state;
}

1;

「あ、save_backup が消えた……cpじゃなくなった……」

先生が一言。「カプセル化」

ナナコさんが微笑んで補足する。「先生が追加したのは save_to_mementorestore_from_memento の2つです。状態を保存したいときはカプセルに詰める。復元したいときはカプセルから取り出す。cpmvbackup も、もういらないんです」

記憶の番人

先生はさらに、新しいモジュールを作った。

 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
package RoastHistory;
use v5.36;

sub new($class) {
    bless { stack => [] }, $class;
}

sub save($self, $memento) {
    push $self->{stack}->@*, $memento;
}

sub undo($self) {
    return undef unless $self->{stack}->@*;
    pop $self->{stack}->@*;
}

sub history($self) {
    return $self->{stack}->@*;
}

sub count($self) {
    scalar $self->{stack}->@*;
}

1;

「これは記憶の番人です」とナナコさん。「カプセルを棚に順番に並べて管理する役目ですね。何番目のカプセルでも取り出せます」

棚に並べたキャニスター。ラベル付きで、日付も入っている。roast_profile_best_maybe.yaml のような混沌とは、まるで違う世界。

「あの日の味に戻れる……?」


術後経過

先生が黙々とテストコードを書いていた。そして、おもむろにテストを実行した。

 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
28
29
30
31
32
33
34
35
#!/usr/bin/env perl
use v5.36;
use Test::More;
use FindBin;
use lib "$FindBin::Bin/../lib";
use RoastManager;
use RoastHistory;

subtest 'Mementoで状態を保存・復元できる' => sub {
    my $mgr     = RoastManager->new();
    my $history = RoastHistory->new();

    # 初期状態を保存
    $history->save($mgr->save_to_memento('初期設定'));

    # 最高の焙煎設定を発見!
    $mgr->update_profile(temp_peak => 215, duration => 660);
    $history->save($mgr->save_to_memento('最高の一杯'));

    # さらに実験……
    $mgr->update_profile(temp_peak => 225, duration => 780);
    $history->save($mgr->save_to_memento('実験3回目'));

    is $mgr->profile->{temp_peak}, 225, '現在は225';

    # 「あの時の215に戻したい!」
    $history->undo();  # 実験3回目を捨てる
    my $best = $history->undo();  # 最高の一杯を取り出す
    $mgr->restore_from_memento($best);

    is $mgr->profile->{temp_peak}, 215, 'あの日の設定に戻れた!';
    is $mgr->profile->{duration},  660, '焙煎時間も復元された';
};

done_testing;

ターミナルに結果が流れた。

1
2
3
ok 1 - 現在は225
ok 2 - あの日の設定に戻れた!
ok 3 - 焙煎時間も復元された

全部グリーン。

roast_profile_backup.yamlroast_profile_final.yaml もいらないんです」ナナコさんが履歴スタックの中身を見せてくれた。「全部ここにあります。ラベル付きで、順番に」

私は画面を食い入るように見つめた。「最高の一杯」というラベル。ピーク温度215、焙煎時間660秒。

「あの日の……あの日の設定が、ちゃんとある……!」

先生はもう自分のトリプルディスプレイに向き直っていた。私の感動などお構いなしだ。でも——ほんのわずかに、口角が上がっていた気がした。気のせいかもしれない。


勘違い

お礼を言おうとして、私はバッグから持参していた自家焙煎のコーヒー豆の小袋を取り出した。エチオピア・イルガチェフェ。前日に丁寧に焙煎した、自信作だ。

「先生、これ、よかったら——」

先生が振り返った。そして、私の差し出した豆の小袋に——ではなく、私の背後の本棚に手を伸ばした。

一瞬の沈黙。

先生の手が引き抜いたのは、ラクダの表紙の分厚い本。「Programming Perl」。

(え……私の豆より本の方が大事なの……? いや、でも先生だし……コードへの愛が深すぎて、コーヒーなんて眼中にないのかも……)

先生は本をパラパラとめくり、あるページを私に見せた。Storable モジュールの解説ページだ。

「深いコピー。これで」

ナナコさんが引き取った。「先生は、深川さんのプロファイルを本当の意味で完全にコピーするには、ネストされたデータ構造も含めて deep copy が必要だとおっしゃっています。 Storable::dclone を使うといいですよ」

先生はもう本棚に戻っていた——というか、別の本を探し始めていた。私のコーヒー豆のことなど完全に忘れている。

私はそっとコーヒー豆の小袋を受付カウンターに置いた。ナナコさんが小さく「ありがとうございます」と受け取ってくれた。その微笑みだけが、このクリニックの唯一の温もりだった。


退院

「これで、あの日の味を取り戻せます。……先生、ありがとうございました」

先生はトリプルディスプレイから目を離さず、一言だけ言った。

「感謝は、このコードに」

私は重い鉄の扉を 押して 開け、廊下に出た。リノリウムの床が、蛍光灯の光を静かに反射している。ビルの外に出ると、どこかの店からコーヒーの香りがした。

「……今日焙煎して、あの設定で淹れよう。今度はちゃんと、記憶を保存しながら」

帰り道、スマホでPerlのドキュメントを開いた。Storable::dclone。深いコピー。あの密閉キャニスターの秘密。

もう roast_profile_best_maybe.yaml なんて作らない。


処方箋まとめ

症状適用すべき経過観察
状態を上書きしており、以前の値に戻す手段がない
cp やファイルコピーでバックアップを取っているが、どれがいつの時点か不明
浅いコピーで履歴を取っているが、元の変更に引きずられる
単一の設定値を変更するだけで、履歴管理の必要がない
状態が巨大すぎて、スナップショットのメモリコストが問題になる

治療のステップ

  1. Memento(記憶カプセル)を定義する: 保存したい状態を dclone で深いコピーし、ラベルとタイムスタンプを付けてカプセル化する
  2. Originator(記憶の持ち主)にカプセル操作を追加する: 既存のクラスに save_to_memento / restore_from_memento メソッドを追加する
  3. Caretaker(記憶の番人)で履歴を管理する: スタック構造で Memento を保存し、undo 操作で任意の時点に復元できるようにする
  4. 深いコピーを保証する: ネストされたデータ構造がある場合、Storable::dclone 等で deep copy を取り、参照共有による偽の保存を防ぐ
  5. ファイルベースのバックアップを卒業する: cp コマンドや _backup _final 接尾辞によるバックアップをやめ、構造化されたスナップショット管理に移行する

助手より

深川さん、今日はお疲れさまでした。「あの日の最高の一杯」を取り戻したい——その気持ち、とてもよく分かります。先生の治療はいつも言葉少なですが、今日のコードには深川さんの焙煎への情熱がちゃんと組み込まれていましたよ。dclone で包んだ記憶カプセルは、密閉キャニスターよりもずっと確実に、大切な味を守ってくれます。これからは安心して実験を重ねてくださいね。素敵な一杯が生まれることを、心から楽しみにしています。

——ナナコ

comments powered by Disqus
Hugo で構築されています。
テーマ StackJimmy によって設計されています。