Featured image of post コードカーペンター【Seam】「もう、外せる所だったんですか」〜書き換えずに手を入れられる継ぎ目を探す〜

コードカーペンター【Seam】「もう、外せる所だったんですか」〜書き換えずに手を入れられる継ぎ目を探す〜

店の間の建具を直すついでに、値札の税率計算を確かめたいという依頼が来る回。差し替える場所が無いように見えるが、実はパッケージ境界そのものが継ぎ目(Seam)になっていて、Perlのシンボルテーブルをlocalでテストの間だけ差し替えます。

第1幕: 検分 ── 店の間、まだ切っていない溝

朝、店の間には、もう鉋屑の匂いが満ちていた。表と奥をつなぐこの部屋には、間を仕切る引き戸が立っている。開け閉てのたびに、どこかで木がこすれて引っかかる。もう何年もそうだったと店主は言っていたが、今日ようやく手が入ることになった。

クロベさんは鴨居の溝に指を這わせ、埃を払いながら、溝の続く先を目で追っていた。ナギさんは外した板戸を横に立てかけ、木口の反りを確かめている。二人とも、私が近づいたことにまだ気づいていない様子だった。作業の手を止めさせるのは気が引けたが、今日はもう一つ、別の用件を持ってきていた。

私は野帳を手に、その脇に立った。声をかけるまで、いつもより少し間があった。帳場から出てくるのでも、通り庭へ移動するのでもない。誰かが手を動かしている最中に、こちらから割り込む形は、これまでになかった。

「クロベさん。作業中にすみません」

クロベさんが指を止め、こちらを向いた。

「店主から、また別の頼まれごとを聞いています」

「なんでしょう」

「消費税が、また変わるかもしれない、と。もし本当に変わったとき、値札の計算がちゃんとその通りに動くか、変わる前のうちに確かめておきたい、と言っていました」

まだ何も起きていない。起きるかどうかも分からない。それでも確かめておきたい、というのが今日の頼まれごとだった。二十年前にも一度、税率が変わったことがあったと、店主から聞いたことがある。あのときは値札の貼り替えが間に合わず、しばらくレジと値札の数字が食い違ったままだったらしい。棚の品に古い値札と新しい値札が入り混じり、レジを打つたびに店主が電卓で確かめ直していた、という話だった。今度は、同じ騒ぎを繰り返したくない、ということなのだろう。建具の立て付けとは違って、税率は待ってはくれない。変わると決まってから慌てて確かめるのでは、遅い。

「税率が変わっていないのに、確かめることなんてできるんですか」

私が訊いた。まだ起きていないことを、どうやって確かめるのか、見当がつかなかった。

クロベさんは、溝から指を離し、こちらへ向き直った。

「できます」

短い返事だった。それから、ナギさんに目をやった。

「PriceTag.pmを開いてください」

ナギさんが道具を置き、帳場の隅に据えられた作業机の画面に向かった。立て付けの悪い建具は、そのまま溝に立てかけられたままになっている。まだ直っていない。けれど、今日はこちらが先らしかった。私も後を追い、画面の見える位置に立った。

第2幕: 手が入らない ── ナギの誤手

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package PriceTag;

use strict;
use warnings;

sub calc_price {
    my ($base_price) = @_;
    my $tax = int($base_price * Shop::Tax::rate());
    return $base_price + $tax;
}

1;

画面には、そう長くないコードが表示されていた。値札に載せる税込みの価格を、calc_priceという一つの関数で計算している。Shop::Tax::rate()という呼び出しが、その中に一行だけあった。

「税率は、ここで決まっているんですね」

私が訊いた。

「はい。Shop::Taxという、別の場所に置かれたrateという名前の関数が、今の税率を返します」

ナギさんが、画面から目を離さずに言った。

「じゃあ、これを確かめるには、税率を渡せるようにすればいいんですよね」

言うが早いか、ナギさんの手はもう動いていた。指がキーボードの上を迷いなく滑る。今まで見てきた中でも、いちばん躊躇のない手の動きだった。

1
2
3
4
5
6
7
# ナギが書き始める
sub calc_price {
    my ($base_price, $tax_rate) = @_;
    $tax_rate //= Shop::Tax::rate();
    my $tax = int($base_price * $tax_rate);
    return $base_price + $tax;
}

calc_priceに、税率を受け取る引数を足す。渡さなければ今まで通りShop::Tax::rate()を使う、という形だった。話の道筋としては、おかしなところは無いように思えた。

「待ってください」

クロベさんが言った。ナギさんの手が、キーボードの上で止まった。宙に浮いたままの指先が、次の一打を待っているように見えた。

「でも、これで動くはずです。引数を足すだけです」

ナギさんが言った。声には、まだ迷いがなかった。

「動きはします。ですが、それは今のcalc_priceを書き換えた、ということです」

クロベさんが静かに言った。声を荒げたわけではなかった。それでも、いつもより一段低く、区切るような話し方だった。私は、二人のやり取りを見ながら、どちらに何を言えばいいのか分からずにいた。ナギさんの手を止めさせたのは自分の頼みごとのはずなのに、口を挟む隙が見つからなかった。

「でも、既定値を用意しています。渡さなければ、今まで通りShop::Tax::rate()を使います。今の呼び出しは、どこも変わらないはずです」

ナギさんが、食い下がった。声にはまだ、迷いのなさが残っていた。

「今の呼び出しは変わりません。ですが、確かめたいのはcalc_priceが正しく動くかどうかのはずです。そのcalc_price自体に、今、手を入れようとしています」

クロベさんが言った。

「今日はcalc_priceを疑いたいわけではありません。疑いたいのは、税率が変わったときの振る舞いです。ですが、そのcalc_priceを書き換えてから確かめると、通ったとしても、疑いたかったものが正しいのか、書き換えが正しかったのか、切り分けられなくなります」

クロベさんが続けた。calc_priceを呼んでいるのは、この一箇所だけではない。値札の表示、レジの表示、注文の控え——店の中の何箇所からも、calc_price($base_price)という形で呼ばれている。既定値があるうちは、そのどれも動きを変えない。それでも、確かめる対象そのものに手を加えた、という事実は残る。

「……そうですけど、他に方法があるんですか」

ナギさんの声から、迷いのなさが消えていた。反論の材料が、そこで尽きたようだった。

私は、ナギさんの書きかけの一行を見た。壊れてはいない。動きもする。それでも、クロベさんは「書き換えた」と言った。確かめたい相手と、確かめる道具を、同じ場所に置いてはいけない——そういうことらしかった。

「今日確かめたいのは、税率が変わってもcalc_priceがちゃんと動くか、でした」

クロベさんが言った。

「書き換えずに確かめられるかどうか、というのが、その中身のはずです」

ナギさんの手が、キーボードから完全に離れた。書きかけのコードは、画面にそのまま残っていた。誰も、次にどうすればいいのか、まだ言えなかった。店の間から、木材が擦れる小さな音が、続いていることを思い出した。作業は、まだ途中のままだった。

「では、どうすれば」

私が訊いた。答えは、すぐには返ってこなかった。

第3幕: 手立て ── 建具の溝、パッケージの境界

「継ぎ目、という言葉があります」

クロベさんが、画面から目を離さずに言った。

「その場所を編集しなくても、振る舞いを変えられる場所のことです。今、書き換えずに確かめたいなら、まず探すべきなのは、そういう場所です」

「そんな場所が、どこかにあるんですか」

ナギさんが訊いた。

「あるかどうかを、見てもらったほうが早いです。こちらへ」

クロベさんが立ち上がり、店の間へ戻った。私もナギさんも、後についていった。立てかけられたままの板戸に手をかけ、両手でそっと持ち上げる。鴨居と敷居、それぞれの溝から、板戸がすっと抜けた。切った跡も、削った跡も無い。木肌には、何年分もの手垢と艶があるだけだった。クロベさんはそのまま、また溝へ戻す。木がこすれる、乾いた小さな音がした。

「見ていてください」

それだけ言って、クロベさんはもう一度板戸を持ち上げ、外し、戻した。二度目は、私も動きを目で追った。編集しなくても変えられる場所、という今聞いた言葉と、目の前の板戸が、まだ私の中では結びついていなかった。

ナギさんが、その様子を見て言った。

「……それ、さっき私たちが直そうとしていた所ですよね。切ったりしなくても、外れるんですか」

「切らなくても、もう外せる場所でした」

クロベさんが溝から手を離した。

「もう、外せる所だったんですか」

私は思わず言っていた。立て付けを直すというから、てっきり削るか、新しく仕口を切り直すのだと思っていた。溝は、最初からそこにあった。誰かが、外せるように作っておいたものだった。今聞いたばかりの「継ぎ目」という言葉が、目の前の光景と、ようやく一つに重なった。

クロベさんは帳場へ戻り、画面の前に座った。ナギさんの書きかけの一行は、まだそのままだった。

「今のと、同じことが、ここにもあります」

クロベさんがShop::Tax::rate()という呼び出しを指した。

「Shop::Tax::という、よその場所を指す名前が、ここに書いてあります。前回、$Toriniwa::pending_itemという書き方を見ましたね。あれは変数でしたが、今回は同じことが関数に対しても起きています」

「よその入れ物を指す書き方が、関数にも使えるということですか」

私が訊いた。

「そうです。さっきの板戸と同じで、この呼び出しも、もとから外側に開かれています。今、calc_priceの中身は一切変えていません。それでも、このShop::Tax::rate()という呼び出しの先を、テストの間だけ差し替えることができます」

「シンボルテーブルを触るなんて、危なそうな操作に見えます」

ナギさんが、少し身構えるように言った。

「何か、制限を外さないといけないんじゃないですか」

「思うのは自然です。ですが、今回はそれが要りません。文字列でなく、そのまま名前を書いているからです」

クロベさんが答えた。

「strict 'refs'が制限しているのは、文字列から名前を組み立てて辿る書き方——たとえば*{"Shop::Tax::rate"}のように、パッケージ名や関数名を文字列で作ってから触る場合です。今回は*Shop::Tax::rateと、そのままソースコードに書いています。文字列は使っていません」

「なぜ、文字列だと引っかかって、そのまま書けば引っかからないんですか」

ナギさんが訊いた。

「Perlは、パッケージごとに『シンボルテーブル』という名前の一覧表を持っています。rateという名前は、そこに『型グロブ』という形で登録されています。スカラーや配列、そして今回使う関数の実体まで、一つの名前の下にまとめて束ねられたものです」

クロベさんが、*Shop::Tax::rateという書き方を紙に書いた。

「先頭の*が、型グロブを指す印です。今回、私たちが渡すのは関数の実体(sub {...})なので、この束の中の、関数の実体だけが差し替わります。スカラーや配列の場所は、そのままです」

クロベさんの紙には、*Shop::Tax::rateという文字の下に、四角い枠が四つ並んでいた。スカラー、配列、ハッシュ、そして関数——一つの名前の下に、まとめて束ねられた入れ物だった。指先が、いちばん右の枠だけをとんと叩く。他の三つには、触れなかった。

型グロブという束の図解: nameがGlobにつながり、Globの中にSCALAR/ARRAY/HASH/CODEの4スロットが並ぶ。SCALAR/ARRAY/HASHは灰色破線で触れていないことを示し、CODEスロットだけが代役subに一時差し替えられる。

「要らないのは、今回だけ、ということですか」

私が訊いた。

「はい。パッケージ名が変数に入っていて、実行時にしか決まらないような場合は、その代入の直前だけに絞ってno strict 'refs'を書きます」

クロベさんが、紙の隅に短い一行を書き添えた。

1
2
3
4
# 名前を変数から組み立てる場合(今日は使わない)
my $pkg = 'Shop::Tax';
no strict 'refs';
local *{"${pkg}::rate"} = sub { return 0.08; };

「これなら、$pkgの中身は実行してみるまで分かりません。文字列から名前を組み立てているので、strict 'refs'に引っかかります。だから、その代入の一行だけ、no strict 'refs'で緩めます。今日はShop::Taxという名前が最初から分かっているので、こちらは使いません」

「制限を外すのは、名前が分からないときだけ、ということですね」

ナギさんが言った。

「そうです。今日は要らない、で今日は終わりです」

クロベさんが言った。

「継ぎ目とは、書き換えずに振る舞いを変えられる場所のことでした。ですが、継ぎ目があるだけでは、まだ何も変わりません。どちらの振る舞いを使うか、実際に決める地点が、別にいります」

「決める地点、というのは」

私が訊いた。今の説明を聞いて、もう一つ気になっていたことがあった。

「今回で言えば」

「localと書く、まさにその行です。継ぎ目そのものはShop::Tax::rate()という呼び出しですが、どちらのrateを使うかを実際に決めるのは、テストの中のその一行です」

クロベさんが、テストのファイルに手を入れ始めた。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
use strict;
use warnings;
use Test::More;
use lib 'lib';
use PriceTag;
use Shop::Tax;

subtest '税率が変わったとき' => sub {
    local *Shop::Tax::rate = sub { return 0.08; };
    is(PriceTag::calc_price(1000), 'PLACEHOLDER', 'まだ仮の期待値');
};

done_testing;

「localは、この型グロブへの代入を、動的スコープに限ります。字面のブロックではなく、実行の流れがこのsubtestのブロックを抜けるまで、という意味です。抜けた瞬間、元のrateに戻ります」

「一文で書くのが大事です」

クロベさんが付け加えた。

「local *Shop::Tax::rate = sub {...};と、一つの代入文で型グロブごと置き換えます」

クロベさんが、分けて書いた場合の一例を、紙の隅に書き添えた。

1
2
# 分けて書いた場合(今日は使わない)
local(*Shop::Tax::rate) = (sub { return 0.08; });

「見た目は近いですが、()で囲むと、右側がリストとして扱われる書き方になります。型グロブをリストの側で受け渡しすると、狙った場所だけを差し替えるという保証が失われます。今日のような一つの代入では、上の一文で書く形を、いつも使ってください」

「新しく差し込み口を作ったわけではないんですね」

私が訊いた。ナギさんが書きかけていたものとは、何かが違う気がしていた。

「作っていません。今、このsub { return 0.08; }は、テストの中だけで使う代役です。店のどこにも、差し替えるための場所を新しく組み立ててはいません。板戸を外したときと同じです。溝を新しく彫ったわけではなく、もとからあった溝を使っただけです」

クロベさんが言った。ナギさんが、少し腑に落ちたような顔をした。

湯呑みを一つ、ナギさんが棚から取り出した。値札には千円と書かれている。今日、確かめるのはこの品だった。

第4幕: 検め ── 継ぎ目は、閉じれば元に戻る

動かすと、落ちた。

1
2
3
4
5
not ok 1 - まだ仮の期待値
#   Failed test 'まだ仮の期待値'
#   at t/price_tag_seam_local.t line 12.
#          got: '1080'
#     expected: 'PLACEHOLDER'

got:のところに出ている値を、そのまま書き写す。前回、前々回と同じ手順だった。

1
is(PriceTag::calc_price(1000), 1080, 'local で継ぎ目を差し替えると税率8%の1080円になる');

動かすと、通った。

1
ok 1 - local で継ぎ目を差し替えると税率8%の1080円になる

「税率が変わっていないときも、確かめておきましょう」

クロベさんが、もう一つsubtestを足した。

1
2
3
subtest '通常の税率のとき' => sub {
    is(PriceTag::calc_price(1000), 'PLACEHOLDER', 'まだ仮の期待値');
};

同じ手順で、実測値を書き写す。

1
is(PriceTag::calc_price(1000), 1100, '税率10%で1000円の税込価格が1100円になる');

「まだ、大事なところが残っています」

クロベさんが言った。

「テストのブロックを抜けたあと、本当に元の税率に戻っているか、確かめていません」

1
2
3
subtest 'スコープを抜けたあと' => sub {
    is(PriceTag::calc_price(1000), 'PLACEHOLDER', 'まだ仮の期待値');
};

三つのsubtestを、この順に並べた。通常の税率のとき、税率が変わったとき、そしてスコープを抜けたあと。最後の一つも、同じ手順で実測値を書き写す。

1
is(PriceTag::calc_price(1000), 1100, 'subtest を抜けると税率が元の10%(1100円)に戻る');

動かすと、三つとも通った。

1
2
3
4
ok 1 - 通常の税率のとき
ok 2 - 税率が変わったとき
ok 3 - スコープを抜けたあと
1..3

ok 1、ok 2、ok 3。三つの結果を、私は目で追った。税率が10%のとき、8%のとき、そしてまた10%に戻ったとき。文字にすれば三行だが、頭の中では一続きの絵になっていた。差し替える前、差し替えている間、そして戻ったあと——同じShop::Tax::rateが、時間の中で三つの顔を見せた。PriceTag.pmもShop/Tax.pmも、その間ずっと、指一本触れられていない。

localの動的スコープによる時間軸の図解: 通常運転(rate→10%)からsubtestに入ると差し替え中(rate→8%)になり、subtestを抜けると再び通常運転(rate→10%)に戻る。PriceTag.pmとShop/Tax.pmは全区間で不変であることを灰色破線で示す。

「一つ、面白いものをお見せします」

クロベさんが、localという一語だけを、一時的に消してみせた。同じテストをもう一度動かす。三つ目のsubtestが落ちた。そしてもう一つ、警告が出た。

1
Subroutine Shop::Tax::rate redefined at lib/Shop/Tax.pm line 7.

「localを付けないと、書き換えたままになります。この警告は、rateという名前が、もう一度別のものとして定義し直された、という知らせです。localが付いていれば、この警告も出ません。ブロックを抜けたときに、静かに元へ戻すからです」

クロベさんが、localを書き戻した。もう一度動かすと、三つとも通り、警告も消えた。

「保証できること。テストのsubtestのブロックの中だけ、Shop::Tax::rateの振る舞いが変わります。ブロックを抜ければ、必ず元の税率に戻ります。PriceTag.pmにもShop/Tax.pmにも、一行も手を入れていません」

「保証できないこと」

クロベさんが続けた。

「今日確かめたのは、この一つのテストの中で起きたことだけです。実際に店で動いているレジのプログラムは、また別に動いている一つの流れです。今、この場で差し替えたものが、そちらまで及ぶわけではありません」

私は、そのことを頭の中で確かめた。テストの中で税率が変わっても、今この瞬間に店先で誰かが会計をしていたら、そちらの計算には関係がない。当たり前のようで、はっきり言われるまで気づかなかった。

「Shop::Tax::rateという名前を使わずに、どこかから税率を呼んでいる場所があるかどうかは」

私が訊いた。

「今日は検めていません。今日はこの継ぎ目の話です」

クロベさんは、それ以上は言わなかった。奥にまだ何かありそうな気配だけが残った。

「店主が本当に確かめたかったのは、税率が変わったあとの話ですよね。今日確かめたのは、税率が変わる前提そのものが正しいかどうか、ですか」

私が訊いた。

「はい。税率が本当に変わったときは、Shop::Tax::rateの中身を書き換えることになります。今日確かめたのは、その先でcalc_priceがちゃんと追随するか、ということです」

テストが通ったのを見届けると、クロベさんは立ち上がり、店の間へ戻った。ナギさんもすぐに続いた。立てかけられたままの板戸を、二人でもう一度鴨居の溝に合わせる。今度は、削るためではなく、溝の深さを測るためだった。クロベさんが指先で溝の底をなぞり、埋まった埃を掻き出す。「引っかかっていたのは、溝が痩せたのではなく、埃が溜まっていただけかもしれません」

板戸を数度滑らせると、さっきまでの引っかかりが消えていた。切らずに直る。今日、二度目にそれを見た。

気がつくと、店の間に差す光の角度が、朝より低くなっていた。今日の本題はこちらだったはずなのに、いつの間にか順番が入れ替わっていた。

帳場に戻り、野帳を開く。見立てを書き、手を入れた所を書き、触っていない所を書き終える。ペン先が紙の上を進む間、店の間からはまだ、鑿の音が聞こえていた。

欄が一つ残った。「私が引き受けたこと」。ペンを持ったまま、しばらく考えた。前回は、図を自分で読めるようになった。今回は、それとは違う何かだった。

どこなら触ってよいかの線を自分で引ける。

そう書いてから、もう一行だけ付け加えた。呼び出しに、よその場所の名前が付いているところは、もう外から触れる継ぎ目かもしれない——それが、今日私の手に入った線だった。ナギさんが最初に書きかけたものは、間違ってはいなかった。ただ、探す場所を間違えていた。書き換える前に、まず探す。その順番を、今日ようやく体で覚えた気がした。

継ぎ目は、閉じれば元に戻る。クロベさんがそう言ったとき、私はまだ半分しか分かっていなかった。今、野帳を閉じながら、もう半分が分かった気がした。今日触ったのは、テストの中だけだ。店の間の建具も、値札の計算も、この店を支えている形は、何一つ変わっていない。


普請控

  • 見立て: 消費税が変わっても値札の計算(PriceTag::calc_price)が正しく動くか、変わる前に確かめておきたいが、税率計算(Shop::Tax::rate)を差し替える場所が無いように見えた
  • 手を入れた所: 継ぎ目(Seam)を使い、Shop::Tax::rateをテストの中だけlocalで差し替えた。t/price_tag_seam_local.tを追加した。PriceTag.pm・Shop/Tax.pmは一行も変えていない
  • 触っていない所: PriceTag::calc_price・Shop::Tax::rateそれぞれの中身の計算そのもの。Shop::Tax::rateという名前を直接使わない呼び出し経路があるかどうかは、今日は検めていない。本番で動いているレジのプログラムへの影響(今日の検めは一つのテストの中に閉じている)
  • 次の工程へ送ること: 実際に税率が変わったときのShop::Tax::rateの書き換えそのもの
  • 私が引き受けたこと: どこなら触ってよいかの線を自分で引ける
comments powered by Disqus
システム開発・AIワークフローのご相談は Meetsource へ
Hugo で構築されています。
テーマ Stack は Jimmy によって設計されています。