Featured image of post コードガーデナー【RAII】当番は決めたのに、鉢だけがびしょ濡れか枯れかけになる〜世話係を一人に絞り、離れる瞬間に後片付けまで背負わせる〜

コードガーデナー【RAII】当番は決めたのに、鉢だけがびしょ濡れか枯れかけになる〜世話係を一人に絞り、離れる瞬間に後片付けまで背負わせる〜

訪問介護士の私が、家族で分担する冬越し観葉植物の水やり当番アプリで二重水やりと放置を繰り返す。水やりと記録が別々の関数呼び出しになっているせいだと知り、庭師が見せてくれたRustのDropトレイトで、当番の値が役目を終える瞬間に後片付けまで背負わせる手入れ。

12月、日が落ちるのが早い。今日は早番で、まだ空が明るいうちに介護の仕事を終えられた。訪問先を3軒回り、最後の利用者さんの布団を整え終えたときには、指先の感覚が半分なくなっていた。玄関を開けると、暖房の効いた空気がふわりと頬に当たる。手袋を外しながら、いつもの癖でリビングの隅——ビニールカバー付きの簡易温室——に目をやった。ビニールがわずかに結露して曇っている。冬は寒さに弱い鉢をここに集めて越冬させているのだ。一番手前のガジュマルの鉢に、しゃがんで指を土に触れた。利用者さんの肌や体温を確かめるときと、同じ手つきだった。一日中誰かの世話をしていると、こうして自分の手で何かを世話する時間だけが、なぜか一番ほっとする。

水やり当番アプリを開くと、昨日の欄は空欄。「今日はまだ誰もやってないのね」と独りごちて、水差しを持ってくる。3年前、家族4人(夫、中学生の娘、同居の母、私)で分担するようになったとき、揉め事にならないようにと自分で書いた小さなコマンドラインツールだ。water関数で水やりを、mark_watered関数で記録を、それぞれ別々に呼ぶ設計にしていた。

水をやり終えたところで、廊下から母が顔を出した。

「あら、ガジュマル。さっき私も水あげておいたわよ」

手が止まる。よく見ると、土は確かに、私が触れる前から湿っていたような気もする。気のせいだと思い込もうとしていただけかもしれない。アプリを開き直しても、母の記録はどこにもなかった。

「記録の付け方、まだよく分からなくて」と母が申し訳なさそうに笑う。

私は少し困りながら、先週のことを思い出した。多肉植物のエケベリアが、記録上は「済み」になっていたのに、実際には3日間、水をもらえずカラカラになりかけていたのだ。あのときは母が「今日水やらなきゃ」と思って先に記録を付け、水差しを取りに行く途中で来客対応に呼ばれ、そのまま忘れてしまった。

仕事なら、こんな行き違いは許されない。訪問先での申し送りは、ケアを終えたその場で記録用紙にサインしてから次の担当へ渡す決まりになっている。家のこのアプリには、そういう仕組みがまるでない。

同じ職場の後輩が、以前ふと漏らしていたことを思い出す。「コードの相談にも乗ってくれる庭師さんがいるらしいですよ」。半信半疑のまま、教わった道順だけを頼りに、コートを羽織り直した。

訪れ ── 記録は「済み」なのに、鉢は乾いていた

マフラーに顎を埋めながら、住宅街の路地を早足で抜ける。いつもは素通りする庭師の工房の前を、今日は立ち止まる余裕もなく、そのまま戸を叩くことになった。

ノックに応えて戸が開く。姿を見せた庭師は、低い声で「どうぞ」とだけ言って、先に工房の奥へ戻っていった。年齢も性別も、後ろ姿からは読み取れなかった。中へ入ると、外の冷たさが嘘のように、小さな薪ストーブの熱が体を包む。土間には鉢がいくつも並び、湿った土の匂いに、微かな煙の匂いが混じっていた。私はノートパソコンを開いて、water関数とmark_watered関数をそのまま差し出した。

「家族で水やり当番を分担してるんですが、記録の付け忘れと、付けたのに水をやってない、が両方起きてしまって……」

庭師の後ろから、若い女性がひょいと顔を出す。「見習いのヒバリです」と、明るく自己紹介された。作業台を手で示され、私は言われるままそこに腰を落ち着け、ノートパソコンを2人の間に置いた。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
struct Plant {
    name: String,
    last_watered_by: Option<String>,
}

fn water(plant: &mut Plant, by: &str) {
    // 実際の潅水処理(本記事では省略。水やり自体は必ず成功する前提とする)
    println!("{}{}に水をやりました", by, plant.name);
}

fn mark_watered(plant: &mut Plant, by: &str) {
    plant.last_watered_by = Some(by.to_string());
}

「水をやる処理と、記録を付ける処理を、別の関数に分けてあります」と私は説明した。「両方呼べばちゃんと動くんですが、今日みたいに、母がwaterだけ呼んでmark_wateredを呼び忘れると、記録上は『誰もやってない』ままになってしまって」

症状観察 ── 庭師の肩から、力が抜ける

庭師は無言のまま、画面を覗き込む。water関数を読み終えると、そこで一度、視線が止まった。mark_watered関数へは、すぐには移らない。2つの関数の間、何も書かれていない空白の行に、しばらく目が留まっていた。

沈黙が続くほど、私は落ち着かなくなる。「別々の関数に分けたのが、良くなかったですか」——気づけば、そう口にしていた。庭師は答えない。ただ、肩に入っていた力が、ふっと抜けて止まった。それが良い兆しなのか悪い兆しなのか、判じようがなかった。

庭師は次に、watermark_watered、どちらの引数にもあるby: &strという部分を、指でなぞるように——実際には指は動かさず、視線だけで——たどった。名前を渡すだけの、ただの文字列。それを止める仕組みも、確かめる仕組みも、コードのどこにもない。そのことを確かめるように、視線がそこでしばらく止まった。ストーブの薪が、時折ぱちりと音を立てる。それ以外、工房の中は静かだった。ヒバリさんも、口を挟まずに庭師の手元を見つめている。

見立てと翻訳 ── 「water と mark、別の仕事だな」

長い沈黙のあと、庭師が短く口を開いた。

watermark、別の仕事だな」

ヒバリさんが、私にではなく、まず庭師の方を向く。「それって……水やりと記録が、コードの上で切り離されてるってことですか?」。庭師が小さく頷いた。それを受けて、ヒバリさんが私の方へ向き直る。

「水をやる作業と、記録をつける作業が、コードの上で別々の“お願い”になってるんです。誰かが片方だけやって、もう片方を忘れても、コードはそれに気づけません」

私は思わず「それって……」と自分の仕事を思い浮かべた。「うちの職場だと、ケアが終わったら、その場で記録用紙にサインしてから次の担当に渡すんです。サインを忘れたケアは、記録の上では、なかったことになってしまうので」

ヒバリさんが頷く。「まさにそれです。サインする、しないは人間の規律なので、忘れることがありえます」

庭師が短く言葉を継いだ。

「その場でしか、できないようにする」

私は「え」と聞き返す。ヒバリさんが噛み砕いた。「水やりと記録を、後から思い出して別々にやるんじゃなくて、その場——1つのまとまりの中——でしか完結できない形にする、ということです」

私は少し考えて、自分で言葉にしてみた。「じゃあ、水やりをした事実と、記録をつける操作を、切り離せないようにすればいい、ってことですか」

ヒバリさんが感心したように頷いた。「はい、まさにそれです。Rustには、値がスコープ——ブロックや関数の終わりのことです——を抜けるときに、自動的に後片付けの処理を呼んでくれる仕組みがあります。Dropというトレイトです」

Dropトレイト——値がスコープを抜ける瞬間に、コンパイラが自動的に呼び出す後片付け用のメソッドを定義するトレイトのことだという。

「『水やり当番中』であることを表す値を1つ作って、その値が役目を終える瞬間に、記録更新を自動で行わせるんです」とヒバリさんが続けた。「こういう考え方自体には名前があって、RAIIと呼ばれています。Resource Acquisition Is Initializationの略で、資源の確保を値の生成に、資源の解放を値の破棄に結びつける、という考え方です」

「介護の申し送りだと、サインを忘れないように、みんなで意識して守ってますよね」とヒバリさんが言う。「実はPythonにも近い仕組みでwith文というのがあるんですが、あれも、書くのを忘れたらそれまでなんです。でもRustのDropは、値がスコープを抜けたという事実だけで、型に紐づいて必ず動きます。書き忘れようがないんです」

私は「サインが要らない仕組み、ってことですか」と聞き返した。

「そうです」とヒバリさんが言う。「正確には、サインすることが、当番の値を手放すこととイコールになってる、という感じです」

手入れ ── 当番の値を、一人分だけ作る

庭師が、無言でキーボードの方へ手のひらを向ける。譲ってほしい、という意味だと分かるまで少し間があった。場所を空けると、庭師はまずWateringDutyという構造体を書いた。plantというフィールドとbyというフィールド。ヒバリさんが実況する。

「水やり当番中であることを表す値です。beginで作られて、DerefDerefMut——参照外しの挙動をカスタマイズするトレイトです——で、中のPlantに直接触れるようにします」

 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
struct WateringDuty<'a> {
    plant: &'a mut Plant,
    by: String,
}

impl<'a> WateringDuty<'a> {
    fn begin(plant: &'a mut Plant, by: &str) -> Self {
        WateringDuty {
            plant,
            by: by.to_string(),
        }
    }
}

impl<'a> std::ops::Deref for WateringDuty<'a> {
    type Target = Plant;
    fn deref(&self) -> &Plant {
        self.plant
    }
}

impl<'a> std::ops::DerefMut for WateringDuty<'a> {
    fn deref_mut(&mut self) -> &mut Plant {
        self.plant
    }
}

impl<'a> Drop for WateringDuty<'a> {
    fn drop(&mut self) {
        self.plant.last_watered_by = Some(self.by.clone());
    }
}

DerefDerefMutは、ラップした値をあたかも中身そのものであるかのように直接操作できるようにするトレイトだ。WateringDuty越しにgajumaru.nameを読んだり書いたりできるのは、この2つのおかげだという。そしてDropの中身は、たったの1行。self.plant.last_watered_byを、当番を持っていた人の名前で更新するだけだった。

画面の中の変化を、頭の中でもう一度なぞってみる。これまではwatermark_watered、二枚の別々の付箋をそれぞれ剥がして貼るような仕事だった。剥がし忘れも、貼り忘れも、誰にも咎められない。今はbeginで受け取った一枚の当番札を懐に入れたまま鉢の世話をして、その札を手放す瞬間——Drop——に、札自体が記録を書き込んでくれる。まるで、申し送り用紙へのサインが、ケアそのものと同じ一枚の紙に、最初から刷り込まれているみたいだった。

Before/After構造対比のインフォグラフィック。Beforeはバラバラの当番札としてwater(plant, by)「水をやる」とmark_watered(plant, by)「記録をつける」の2枚の木製プラントタグがそれぞれ別の紐で吊るされ、破線で「呼び忘れ・順番違いを誰も縛らない」と結ばれている。AfterはWateringDutyという一枚札として、begin(plant, by)「当番札を1枚だけ発行」・Deref / DerefMut「札ごしに世話をする」・Drop(スコープを抜ける)「記録更新は自動で一度だけ」の3区画が1本の連続した麻紐で縫い綴じられている。両者は「同じ2つの仕事を1枚の札に縫い合わせる」という矢印で接続されている

「これ、家族みんなが自分のスマホで持てたら便利じゃないですか」と私は提案してみる。「Copyにできませんか」

庭師が黙って#[derive(Copy)]WateringDutyに試しに書き加える。画面に赤い文字が並んだ。コンパイルエラーだ。

「同じ理由を、もっと単純な形で確かめてみましょう」とヒバリさんが言い、Dropを実装しただけの小さな構造体を別に書いて見せる。

1
2
3
4
5
6
#[derive(Copy, Clone)]
struct Foo; // error[E0184]

impl Drop for Foo {
    fn drop(&mut self) {}
}

今度はerror[E0184]という短いエラーが1つだけ出た。

1
error[E0184]: the trait `Copy` cannot be implemented for this type; the type has a destructor

Dropを実装した型は、Copyにはできないんです」とヒバリさんが説明する。Copyトレイトとは、値を代入・関数渡しする際に、所有権を移動せず、暗黙にビット単位で複製するトレイトのことだという。「もしできてしまったら、家族の人数分、当番の値が複製されて、記録更新——Drop——もその数だけ走ってしまいますから」

私は「一人分の水やりが、記録の上では何人分にもなっちゃう、ってことですね」と自分で言い換えた。

庭師が実装を書き終えたところで、私はキーボードを引き取り、使う側のコードを書いてみようとした。「変数名、長いから_でいいですよね」とlet _ = WateringDuty::begin(&mut gajumaru, "私");と打つ。庭師の手が止まる——さっきの脱力とは違う、止め方だった。

ヒバリさんが横から「あ、それだと」と声を上げる。「_は"受け取らない"っていう意思表示になってしまって、その行が終わった瞬間に、当番の値がもう手放されちゃうんです」

私は「え、名前をつけないと、意味がなくなるんですか」と驚いた。

「はい」とヒバリさんが頷く。「let mut dutyって名前をつけて初めて、そのブロックが終わるまで当番を持ち続けられます」

私は書き直した。

1
2
3
4
5
let mut gajumaru = Plant::new("ガジュマル");
{
    let mut duty = WateringDuty::begin(&mut gajumaru, "私");
    water(&mut duty, "私"); // duty は DerefMut 経由で &mut Plant として渡せる
} // ここで duty がスコープを抜け、Drop が自動的に last_watered_by を更新する

water関数そのものは書き換えずに残した、と庭師が言葉少なに補足する。mark_watered関数はもう使わない。呼び出す場所自体が、コードのどこにも無くなっていた。

「もう一つ確かめたいんですが」と私は聞いた。「このコードの中で、dutyを持ったまま、うっかりもう一つ当番を始めようとしたら、どうなりますか」

ヒバリさんが「試してみましょう」と、dutyが生きている行の直後に、もう一つWateringDuty::begin(&mut gajumaru, "母")を書き加える。今度は借用に関するコンパイルエラーが出た。

1
error[E0499]: cannot borrow `plant` as mutable more than once at a time

gajumaruへの書き込み可能な参照は、同時には1つしか存在できないんです」とヒバリさんが説明する。「ただし、これは同じプログラムが1回動いている間の話です。母が別のタイミングで別のスマホからアプリを起動する場合、ここまでの保証が直接効くわけではありません」

私は少し拍子抜けした。「じゃあ、結局、母と私が別々にアプリを開いたら、また同じことが起きるんじゃ……」

ヒバリさんが首を振る。「そこは大丈夫です。母がアプリを起動して水やりを始めたら、その1回の実行の中では、必ずbeginからDropまでが一続きで走ります。記録の更新を忘れる、という状態そのものがもう作れません。だから、私が後からアプリを開いたときには、母がやった水やりの記録が、途中で欠けることなく残っているはずなんです」

私は「同時に触ってる場合の話と、別々のタイミングで触る場合の話が、別々にあるんですね」と自分の言葉で確かめた。

「はい」とヒバリさんが頷く。「今のコンパイルエラーは前者——1つのプログラムの中で、当番を同時に二重に持とうとした場合の話です。後者——別々のタイミングで使う場合——を守っているのは、Dropが記録更新を必ず1回、確実に行うという保証の方です」

さらに付け加える。「今のコンパイルエラーが出るのも、実はdutyDropを持っているからこそなんです。Dropを持たない値なら、Rustは"もう使われない借用"をその場で早めに手放してくれるので、後でdutyを使わなければ、母の呼び出しも通ってしまったかもしれません。Dropがある値だけは、スコープが終わる——後片付けが呼ばれる——その瞬間まで、借用を手放させてもらえないんです」

私は「後片付けをする約束があるから、最後まで独り占めできる、ってことですね」と頷いた。

言われてみれば、さっきから守られているものが二種類あった。一つは、同じ瞬間に当番札を二人が同時に持てないこと。もう一つは、たとえ持つタイミングが私と母とでばらばらでも、それぞれの当番が終わるたびに記録だけは必ず一度書き込まれること。似ているようで、守っている場所がまるで違う。頭の中で、その二つを別々の箱に並べ直してみた。

2種類の保証の切り分けを示すインフォグラフィック。保証その1「同時の二重取得を防ぐ」では、duty(私)が当番札を持っている間に、もう一枚beginしようとすると2本の園芸支柱に阻まれコンパイルエラーE0499「二重に借用できない」が起きる様子を示す。保証その2「別々の時でも記録が欠けない」では、私の当番(begin→世話→Drop)と母の当番(後で、begin→世話→Drop)がそれぞれ独立に、同じlast_watered_byの記録タグへ根で繋がっている。両者は中央の「守っている場所が違う」という札へ、それぞれ「同じ瞬間の話」「別々の瞬間の話」というラベルで接続されている

見送り ── 帰り際、幹の傷跡に触れて

「これで、母が記録を忘れることも、先に記録だけ付けちゃうことも、なくなりますか」と私は聞いた。

庭師は頷く。「ああ」

ヒバリさんが「申し送りを"やってください"ってお願いする代わりに、水やりと記録が、同じ一つの値の中に閉じ込められてるんです」と言う。「ただ、正直に言うと」とヒバリさんが付け足した。「water関数自体は、まだコードの中に残ってます。WateringDutyを経由せずwaterを直接呼んでしまえば、この保証は効きません。『必ずbeginから始める』というのは、家族みんなで守ってもらうしかない部分です」。それでも、記録を付け忘れる余地そのものが無くなるだけで、だいぶ違う気がした。詰めていた息を、私はゆっくり吐き出した。荷物をまとめ始める。

庭師が戸口までついてきて、去り際にぽつりと言葉を残した。

「水は、離れる者にしか、始末できない」

短い言葉なのに、意味を掴むまで少し時間がかかった。

工房を出ると、夕暮れの冷たい空気が肺の奥まで入ってくる。路地を戻る途中、あの古木の根元に差しかかった。今度は歩みを緩め、手袋を外して、指先で幹に触れてみる。根元に近い場所に、古い傷跡があった。触れても、段差がほとんど分からないほど、樹皮がきれいに塞がっている。いつ、何があったのかは分からない。ただ、そこだけ樹皮の色がわずかに違うことに、指先で気づいた。かじかんだ指先に、幹のひんやりとした感触だけが残った。

凍える息を白く吐きながら、口の中だけで呟く。

「水やりと記録を、別々にお願いするんじゃなくて、一つの値に閉じ込める、か」

家に着いたら、まずガジュマルの鉢を覗くだろう。母がまた水をやっているかもしれない。それでも今日までとは、少し違う気持ちで土に触れられる気がした。家族に新しいツールの使い方を説明しよう。申し送りと同じで、“やってください"じゃなくて、“そうするしかできない"形にする——そう思いながら、明日のシフトのことを、少しだけ考え始めていた。


手入れ記録

こんな症状が出たら入れるべき手入れ(パターン)まだ様子見でいい
「実際の作業」と「記録・後片付け」が別々の関数呼び出しになっていて、片方だけ呼ばれることがある✓ RAII(ガード型+Dropによる自動後片付け)
複数の呼び出し元(人・処理)が同じリソースへ無秩序にアクセスし、「今誰が担当中か」が型として存在しない✓ RAII(所有権の一意性で排他制御)
後片付け処理が1箇所からしか呼ばれず、呼び忘れの心配がない✓ そのままの構成で様子見

手入れの手順

  1. 「実際の作業」と「後片付け・記録更新」が別々の呼び出しに分かれていないか、コードを見渡して確かめる
  2. 「作業中であること」を表す値(ガード型)を1つ作り、作業対象への排他参照(&mut)を持たせる
  3. DerefDerefMutを実装し、ガード型経由で対象に直接触れられるようにする
  4. Dropを実装し、後片付け・記録更新の処理をそこに書く
  5. ガード型はCopyを実装できない(DropCopyは共存不可)ことを確認し、複数箇所に配ろうとしていないか点検する

庭師の一言

水は、離れる者にしか、始末できない。

見習いの手控え

師匠の言葉、要は「関わった人にしか、後片付けはできない」ってことです。今日は、水をやる作業と記録をつける作業をコードの上でも同じ一つの区切りに閉じ込めることで、誰かが片方だけやって終わり、みたいなことが起きなくなりました。

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