第8章:増殖したFSMを解体する

同じ「温まっているかどうか」を、四つのファイルがばらばらに覚えていた
GJI(Google Japanese Input、Google 日本語入力)が入力を受け付けられる状態(warm)か、まだ準備中の状態(cold)かは、awaseの出力層にとって特に重要な判定でした。ところがこの判定は、最初から一つの場所にまとまっていたわけではありません。2026年6月時点で、WarmthContext.warmというbool値、CompositionStateというenumの値、gji_long_idleというbool値、gji_last_io_msという時刻の記録という、四つの別々のフィールドが、それぞれ別のファイルに散らばって存在していました。
GJIが本当にwarmになったかどうかは一度の確認だけでは決まらず、フォーカスが移った瞬間・その少し後・さらに時間を置いた後と、同じ対象を何度も観測して情報を足していく必要がありました。1回ごとの観測の呼び出しを「tick」と呼ぶなら、この判定は複数のtickにまたがる判定でした。単純なbool値は「いまどの段階まで進んだか」を覚えておくのには向いておらず、新しい観測が来るたびに別々の場所を書き換える必要があったため、更新が一部だけで止まってしまう不整合が起きやすくなっていました。
この不整合は、実際に繰り返しバグとして現れました。2026年6月時点で、warm/cold判定に起因するバグは少なくとも7件記録されており、中にはWarmthContext.warmのように、値が書き込まれるだけでどこからも参照されていなかったフィールドさえありました。idle時間の分類も同じ限界を抱え、当初はgji_long_idleというbool一つで「長時間放置された後かどうか」を表現していましたが、後になって7〜10秒の「中程度のidle」と10秒を超える「長いidle」を区別する必要が生じ、boolを1個増やすだけでは足りず参照するすべての箇所を洗い出す必要が生じました。
2026年6月20日から22日にかけて、この四つの断片を一つの状態機械(FSM、finite state machine)に統合しました(GjiFsm、ADR-046)。状態機械とは、プログラムが取りうる状態をあらかじめ書き出し、次に何が起きたらどの状態へ移るかを表にしておく設計です。GjiFsmは、OffCold(未活性)・OnCold(活性化待ち)・OnWarm(準備完了)・OnComposing(変換中)という状態と遷移をすべて書き出し、idle時間の分類もColdKind(Short/Medium/Long)という一つの型にまとめました。四つに散らばっていたbool・enumを一つの状態機械に集約したことで、「一部だけ更新される」という種類の不整合は、構造的に起こらなくなりました。
移行は一度に切り替えたわけではありません。まず新しいGjiFsmを追加し、旧来の判定結果と新しい判定結果が一致するかを実行のたびに突き合わせ、一致し続けることを確かめてから正式な判定基準に切り替えています。動いているものをいきなり置き換えず両方を並走させてから切り替える慎重さは、状態管理そのものを作り直すという、後戻りの利きにくい変更だったことの表れです。
こうしてGJIという一つの観測対象については、状態機械による一元管理が実現しました。しかしawaseが観測しなければならない対象はGJIだけではなく、Chrome向けの確認処理、TSFの初期化待ち、リテラル化の検出――どれも同じように複数回のtickにまたがる状態を必要としていました。2026年6月22日から23日にかけて、この共通の形をTickableFsmという一つの取り決め(トレイト、複数の型に共通の振る舞いを約束する仕組み)として抜き出し(ADR-047)、時刻またはイベントを受け取って内部状態を進める操作さえ用意すれば、どんな観測対象でも同じ枠組みで扱えるようにしました。
この抽象化は狙いどおりに機能し、GJI用のGjiWarmupFsm、Chrome用のChromeProbe、リテラル検出用のLiteralDetectFsmなど、観測対象ごとに専用の状態機械が次々と追加されていきました。ばらばらのbool・enumを一つにまとめる最初の判断は正しい判断でしたが、この成功が少し違う形の問題を生み出すことになります。増えたのはFSMの型の数だけでなく、フェーズをまたいで持ち越す値もenumのバリアントも、FSMが増えるたびに一緒に増えていったのです。
「-1055行」は一つのコミットの数字だった
2026年06月27日07時29分35秒、f01d401というコミットが7個のファイルにまたがる差分を残しました。差分の内訳は「25 insertions, 1055 deletions」です。単日のdiffstatとしては、awaseの開発の中でも際立って大きな数字でした。
ただし、この-1055行という数字を「大規模な整理」の証拠としてそのまま読むのは早計です。削除された行の大半、969行はtsf/gji_warmup_fsm.rsというファイル1本の全削除によるものであり、あちこちを少しずつ削った結果ではなく、一つの型を丸ごと消したことで生まれています。何が起きて1本のファイルが丸ごと不要になったのかが、この章の主題です。先に答えを言うと、これは単なる整理ではなく、状態機械という表現方法をさらに選び直した結果でした。
前節で見たTickableFsmは、増殖そのものを防ぐ仕組みではありませんでした。散らばったbool・enumを一元管理する問題は解いていましたが、観測対象が増えるたびにFSMの型も増える設計だったからです。この章が扱うIMEのコールドスタート手続きは、awaseの中でもとりわけフェーズの多い処理であり、だからこそこの日の変更は単発の掃除では終わらず、状態機械とコルーチンという2つの表現方法を比較する材料になりました。
コルーチンとは、関数の実行を途中で一時停止し、あとで続きから再開できる仕組みです。普通の関数は呼ばれたら最後まで一気に実行されますが、コルーチンは途中まで進んだら制御をいったん呼び出し元へ返し、次に呼ばれたときにその続きから動くという書き方ができます。この章に登場するStepCoroも、この意味でのコルーチンとして実装された、awase独自の仕組みです。
執筆準備の初期には、この変更を「三つのFSMの統合」と整理していましたが、コミットログとADR-053を突き合わせると、正確には五種類のFSM型を棚卸しし、手続き的な処理を三本のStepCoroへ移す一方でLiteralDetectFsmは独立したFSMとして残していた、という経緯でした。この訂正を踏まえ、この章の問いを「状態機械とコルーチンを、何を基準に使い分けるべきか」と立て直します。
五つのFSMが、SwitchMachineで機械を乗り換えていた
awaseのTSF(Text Services Framework)には、IMEをコールドスタートさせるための一連の手続きがありました。GJI状態のprobe確認・FreshF2キーの送信・名前変更の待機・変換文字列の送信・リテラル(未変換文字列)の検出という、順番の決まった5フェーズです。
2026年06月27日時点で、この手続きは次の5個の独立したFSM型として実装されていました。
| FSM名 | ファイル | 行数 | 生成コミット |
|---|---|---|---|
GjiWarmupFsm | tsf/gji_warmup_fsm.rs | 969行 | fcd1b82(06-22) |
LiteralDetectFsm | tsf/literal_detect_fsm.rs | 132行 | 8608062(06-23) |
SacrificialWarmupFsm | tsf/sacr_warmup_fsm.rs | 299行 | 06-14〜06-24の累積 |
ChromeGjiReinitFsm | tsf/chrome_gji_reinit_fsm.rs | 95行 | 2c8d647(06-24) |
TsfProbeMachine | tsf/probe_fsm.rs | 827行 | 05月来の累積 |
生成コミットの日付が示すとおり、これらは一度に設計されたものではなく、個々のFSMはそれぞれが生まれた時点で発生していた問題への、その時点では妥当な回答でした。増えすぎたのは、後から振り返って初めて分かることです。
これらのFSMは互いに独立してはいましたが、一つのフェーズが終わると次のFSMに切り替えるSwitchMachineというアクションで連鎖していました(GjiWarmupFsmからLiteralDetectFsmへ、というように)。呼び出す側から見れば複数のフェーズが一つの手続きとして流れているように見えても、実装の内部では別々の型を渡り歩く構造になっていました。
この連鎖の構造には、増えるたびに重くなる代償がありました。フェーズを一つ追加するたびに、enumのバリアントとmatchのアームが線形に増えます。ローマ字文字列やバックスペースの回数のような一時的な値は、structのフィールドとして保持するかSwitchMachineのペイロードとして手渡すしかなく、フェーズが進むたびに「もう使わないが、まだstructには残っている」という状態を経由します。読む側は、あるフィールドがいまのフェーズで意味を持つのか前のフェーズの残骸なのかを、コード全体を追わないと判断できません。matchアームが増える負担と、フィールドの寿命が読み取りにくくなる負担は、同じ設計判断から生じた一対の症状でした。
一つの関数に直線で書いたら、969行が消えた
06月27日07時22分02秒、e548e63でtsf/step_coro.rsという新しい基盤が追加されました。StepCoroと呼ばれるこの仕組みは、unsafe(安全性チェックを一部省く書き方)も使わず、nightly(正式リリース前の実験的機能)にも外部クレートにも依存せず、標準ライブラリだけで動く軽量なコルーチンでした。
この形にたどり着くまでに、ADR-053には三つの選択肢が比較され、いずれも却下された記録が残っています。一つ目はtokioのような非同期ランタイムの導入で、awaseはタイマー駆動の単一スレッドモデルで動いており、外部ランタイムを持ち込むには荷が重すぎると判断されました。二つ目はgeneratorクレートやnightlyのコルーチン機能を使う案で、nightly依存か外部クレート依存のどちらかを新たに抱え込むことになり、個人開発で長期に保守する前提とは相性が悪いと判断されました。三つ目は単純に「FSMのまま拡張し続ける」案で、フェーズを追加するたびにstructのフィールドが増え続け、コードを読む側の負担が積み上がっていく欠点を抱えていました。三つとも別の理由で却下された結果、std限定でunsafeもnightlyも使わないStepCoroが選ばれています。
StepCoroの考え方はこうです。フェーズをまたぐ一時的な値は、structのフィールドに昇格させる必要はありません。1本のasync関数の中に、順番通りに書けばよいのです。関数の途中でyield_stepを呼ぶと、その時点までの出力を返しつつ、次のtickの入力を受け取って続きを実行します。
async fn phase_body(ch: Channel) {
loop { // フェーズ1: probeループ
let input = yield_step(&ch, vec![]).await;
if probe.check_outcome(total_max_ms).is_some() { break; }
}
yield_step(&ch, vec![SendFreshF2]).await; // フェーズ2
loop { /* フェーズ3: NameChangeWait、同じ関数内で続けて書ける */ }
}
このコルーチンを、既存のtick駆動の呼び出し側から見て以前とまったく同じ形に見せるには、もう一段の橋渡しが要ります。TickableFsmというトレイトへの適合を、薄いラッパーstructが引き受けました。
impl TickableFsm for GjiWarmupCoro {
fn tick(&mut self, env: &TsfEnvSnapshot) -> Vec<ProbeAction> {
let input = TickInput { env: *env, .. };
match self.coro.step(input) {
CoroStep::Yielded(actions) => actions,
CoroStep::Complete => vec![ProbeAction::Done],
}
}
}
呼び出し側は相変わらずtickを呼ぶだけで、内部がFSMかコルーチンかを意識する必要がありません。変わったのは実装の内側だけで、外側の契約は保たれています。
StepCoro本体は、unsafeもOSへの呼び出しも持たない、入力を受け取って次の出力を返すだけの型です。ADR-053には、この構造によってChromeProbeやLiteralDetectFsmが独立してユニットテスト可能になったと記録されています。
10分後の07時22分12秒、ccd4711が最初の適用例を追加し、GjiWarmupFsmとLiteralDetectFsmのチェーンをGjiWarmupCoroという1本のコルーチンにまとめ、StartLiteralDetect → SwitchMachine → LiteralDetectFsmという機械切り替えをインライン記述に置き換えています。コミットメッセージには「GjiWarmupFsmは削除せず残置」ともあり、この時点ではまだ古いFSMを消す踏ん切りがついていません。
7分後の07時29分35秒、f01d401がGjiWarmupFsm本体を削除し、ProbeAction::StartLiteralDetectバリアントも合わせて撤去しました。これが冒頭の-1055行です。コミットメッセージには「GjiWarmupCoroへの移行完了に伴い不要になったコードを削除する」とあります。フェーズ間の情報を局所変数として持ち運べるようになった結果、恒久的なフィールドとenumバリアントの大半が不要になり、削除できる状態になっていました。
この書き換えで変わったのは行数だけではありません。以前はGJI probeからLiteralDetectまでの手続きを追うのに5個のファイルをまたいでmatchアームとenumバリアントを行き来する必要がありましたが、書き換え後は同じ手続きが1本の関数の中で上から下へ読める順序で並んでいます。フェーズの数が増えても、増えるのは関数の行数であって、ファイルをまたぐmatchアームの数ではありません。
ここまでの経過だけを見ると、5つのFSMをすべて同じ手順でコルーチン化すればよい、という結論に飛びつきたくなります。
消えなかった一つ
ところが、LiteralDetectFsmというファイルは、f01d401のあとも削除されませんでした。crates/awase-windows/src/tsf/literal_detect_fsm.rsは現在も333行のFSMとして存在し続けています。
削除されなかったLiteralDetectFsmは、06月27日の変更で対象から外れた古い実装ではありません。コールドパスでの役割はコルーチンにインライン化されて消えましたが、ウォームパス(変換後にIME側の確定表示を待つ経路)では同じ名前の型が独立したFSMとして今も使われています。tsf/tickable_fsm.rsには「warmパスのpost-transmit composition確認」という注記が残っています。
同じ「リテラル検出」という処理でありながら、コールドパスでは関数の中の一節としてインライン化され、ウォームパスでは独立したFSMのまま残された。ここに「全部コルーチン化すればよい」という単純な結論では説明できない境界線があります。行数を減らすこと自体が目的だったなら、この333行も削除の対象になっていたはずですが、そうならなかったのは、削除できるかどうかとは別の判断基準が働いていたからです。
整理の前後を図にすると、五つのうち一つだけが独立したFSMのまま残ったことが分かります。

仮に、この基準を意識しないままウォームパスのLiteralDetectFsmもコルーチンに書き直していたらどうなったでしょうか。IME側の確定表示がいつ届くか呼び出し側には分からない以上、コルーチンの内部で待たせるだけでは外部から「いまどの段階か」を問い合わせる手段が失われ、結局その状態をコルーチンの外側に別途持たせる必要が生じたはずです。複雑さを消したのではなく、別の場所へ移しただけになっていたことになります。
残す基準は、順番と生存期間だった
コールドパスのLiteralDetectFsmとウォームパスのLiteralDetectFsmは、同じ処理内容でありながら扱いが違いました。両者を分けたのは、処理の内容ではなく、処理の形そのものでした。
コールドパスでのリテラル検出は、GJI probeからFreshF2送信、NameChangeWaitを経て到達する、決まった順序の最後のフェーズです。開始から終了までの道筋があらかじめ決まっており、一度だけ実行されれば役目を終えます。これはStepCoroが得意とする形でした。
ウォームパスでのリテラル検出は違います。IME側がいつ確定表示を返すかは呼び出し側には分からず、外部からのtickを待ち続け、条件を満たすまで生存し続けます。さらにtickable_fsm.rsの登録が示すように、この状態は呼び出し側からtickを通じて問い合わせられる対象でもあり、状態そのものが他のコードにとって意味を持つドメイン上の値になっていたのです。
同じ関数名・同じ処理内容であっても、「いつ終わるか」と「誰がその途中経過を必要とするか」が違えば、向いている表現方法も違ってきます。コールドパスのリテラル検出は完了したかどうかさえ分かればよく、ウォームパスのリテラル検出は完了を待つ間ずっと、いまどの段階にあるかが外部から意味を持ちます。この2つの違いを一般化すると、次のような基準になります。
| 特徴 | StepCoroに向く | FSMとして残すべき |
|---|---|---|
| 開始から終了までの順序 | あらかじめ決まっている | 外部イベントに応じて分岐する |
| 生存期間 | 一回限りの手続きで完結する | 長期間、外部からの入力を待ち続ける |
| キャンセル・タイムアウト | 手続きの一部として組み込める | 状態自体がタイムアウトの基準になることがある |
| 状態の意味 | 呼び出し側には見えない内部の実装詳細 | 呼び出し側が問い合わせて判断材料にする |
ウォームパスのLiteralDetectFsmは右列の条件をすべて満たしていたため残り、左列の条件を満たす手続きだけがコルーチンへの書き換えの対象になったのです。
残り二系統も、同じ基準で割り切れた
この基準が単なる後付けの説明ではないことは、同じ日に行われたもう2つの統合を見ると分かります。08時09分55秒の2c756ddはSacrificialWarmupFsm(299行)とChromeGjiReinitFsm(95行)をSacrificialWarmupCoro(269行)にまとめ、17時03分17秒のd1d6d17はTsfProbeMachine(827行)をTsfProbeCoroに置き換えて237行を削減しています。どちらも、Chrome再初期化前のウォームアップからProbeの3フェーズまで、順序が決まった一回限りの手続きであり、基準の左列にそのまま当てはまりました。
07時22分のGjiWarmupCoroから17時03分のTsfProbeCoroまで、3系統の書き換えは同じ一日のうちに終わっています。これは急いで済ませたということではなく、最初の書き換えで基準がはっきりしたあとは、残り二系統への適用に迷いが要らなかったことの表れです。判断に時間がかかったのは、基準を見つけるまでの最初の一系統だけでした。
3件の変更と1件の非変更をまとめると、次の対応関係になります。
| 変更前 | コミット | 変更後 |
|---|---|---|
GjiWarmupFsm(969行) | f01d401(07:29) | GjiWarmupCoroへ統合、ファイルごと削除 |
LiteralDetectFsm(コールドパスでの利用) | ccd4711(07:22) | GjiWarmupCoro内にインライン記述、型としての呼び出しは消滅 |
SacrificialWarmupFsm(299行) + ChromeGjiReinitFsm(95行) | 2c756dd(08:09) | SacrificialWarmupCoro(269行)へ統合 |
TsfProbeMachine(827行) | d1d6d17(17:03) | TsfProbeCoroへ統合、237行削減 |
LiteralDetectFsm(ウォームパスでの利用) | ― | 変更なし。唯一残ったFSM |
5個あったFSM型のうち、順序が決まった一回限りの手続きを担っていた4個(実体としては3系統)はコルーチンへ移り、状態そのものが問い合わせの対象になっていた1個だけがFSMのまま残りました。基準は3回とも同じでした。
境界線は引けたが、動く境界には答えていない
この基準は、StepCoroがFSMより優れているという主張ではありません。三つの代替案の却下は、あくまで「一回限りの手続きをどう書くか」という場面に限った判断でした。この基準が答えているのは、「手続きの形をした処理」か「状態そのものに意味がある処理」かの一点だけです。両方の性質を併せ持つ場合や、一回限りの手続きが後から外部に問い合わせられる対象へ変わっていく場合を、この基準だけでどう扱うかは、この章の範囲では答えが出ていません。確認できたのも、一つの手続き群における3+1の内訳だけであり、他の箇所で同じ比率になるとは限りません。境界線を引く基準は手に入りましたが、その手続きや状態がそもそも何の遷移を表しているのかという問いには、まだ答えていませんでした。
個別の対策を積み重ねてから根本原因に置き換える、という展開は、FSM以外の場所でも起きています。2026年8月、起動時のキー配送とエンジンスレッドへの通知経路を洗い直した際(ADR-102/105)、最初に固まったのは19件のコードレビュー指摘に対する、Opus(Claude Opusモデル)2体に互いの設計を批判させ合う敵対的レビュー・4ラウンドでした。OS所有マスク、フェーズ表、Altなりすましラッチの発火順序繰り上げなど、症状ごとに対策を積み重ねた設計で、それ自体は手堅くまとまっていました。
しかし著者が「もっと根本的に良い設計はないか」と問い直したことで、Opusによる根本原因分析が行われました。対策の大半——最もリスクが高い変更と評価されていたものを含みます——が、たった1つの構造的原因への対処で丸ごと不要になると分かりました。原因は、キーボードのフック処理を担当する、エンジンとは別の実行の流れ(ワーカースレッド)からエンジンスレッドへの通知に、PostThreadMessageWという関数を使っていたことです。この関数の宛先は、ウィンドウではなくスレッドID(実行の流れそのものを識別する番号)でした。

Windowsの標準的なメッセージ配送手順(DispatchMessageW)は、メッセージに付いたウィンドウハンドル(hwnd、生成したウィンドウを指す番号)を手がかりに、そのウィンドウ用に登録された処理関数を呼び出す仕組みです。ところがスレッドID宛のメッセージにはウィンドウハンドルが付きません。ウィンドウの無いメッセージに対してDispatchMessageWは何もしないため、これまでのawaseは、メッセージの中身を自前でmatch分岐する受け口をループの中に直接書いていました。この受け口は、メニューやダイアログの表示中には機能しません。表示中はWindows自身が持つ別のメッセージポンプ(ネストしたモーダルポンプ)が一時的に主導権を握り、このポンプは自前の受け口の存在を知らないからです。スレッドID宛のメッセージはこのポンプにも取り出されますが、DispatchMessageWが何もしないため、どこにも処理されずに消えます。取り出された記録は残らず、エラーも出ません。
解決策は、宛先をスレッドIDから、実在する専用ウィンドウのハンドル(hwnd)へ変えることでした。画面には表示されない「メッセージ専用ウィンドウ」(HWND_MESSAGEという特殊な親を持つウィンドウ)を起動直後に1つ作り、以後はそのhwnd宛にPostMessageWで送ります。自前のmatch分岐もやめ、このウィンドウ専用の処理関数(ウィンドウプロシージャ)として登録し直しました。DispatchMessageWは、メッセージのウィンドウハンドルから登録済みの処理関数を探して呼ぶ、という同じ手順を毎回踏みます。この手順は、呼び出し元が自分のループであっても、メニュー表示中のネストしたポンプであっても変わりません。ネストしたポンプは自分のアプリの都合を何も知らないままでも、OSの標準的な仕組みだけを頼りに、メッセージを正しく届けてくれます。
この根本原因が正しいという確信は、机上の議論ではなく的を絞った実験から得ています。ネストしたモーダルポンプの最中に150ミリ秒間隔でhwnd宛メッセージを60件送り、全件が欠落なく届くことを実機で確かめました。969行が消えた本章の経験と同じ形が、通知配送という別の場所でも起きたことになります。個別の症状に対策を積むほど、対策自体が複雑さの発生源になっていないかを、ときどき疑う必要があります。ただし今回実機で確かめられたのは、この配送保証という一点だけです。起動から動作までの通しのソークは、まだ行われていません。
他分野への転用
一直線の手続きと、問い合わせられる状態を分ける発想は、他の場所にもある
StepCoroが「フェーズをまたぐ一時的な値を、structのフィールドではなく関数の中の変数として書く」ためにしていたことは、実は多くのプログラミング言語がすでにコンパイラの機能として提供しています。JavaScriptやPython、そしてRust自身が持つasync/awaitという構文は、見た目こそ普通の関数のように上から下へ書けますが、内部ではコンパイラが同じ中断・再開の変換を自動で行っています。プログラマが手でenumのバリアントを書き並べる代わりにコンパイラが肩代わりしているだけで、発想は同じです。awaseのStepCoroは、この変換を外部クレートやnightly機能に頼らず自前で小さく実装したものだと言えます。
一方、「長期間生存し、外部から問い合わせられる状態」の側にも、身近な対応物があります。フロントエンド開発でよく使われるRedux(状態管理ライブラリ)は、ボタンを押した瞬間だけ実行される一回限りの処理と、アプリ全体が常に持ち続ける状態とを意図的に別の仕組みとして扱い、後者だけがstoreと呼ばれる専用の置き場所を持ちます。ウォームパスのLiteralDetectFsmが独立したFSMとして残ったのは、この置き場所を必要としていたからであり、選んでいる基準はReduxのstoreと同じです。
この対応関係は、awaseがこの区別を発明したことを意味しません。一回限りの手続きと長期間問い合わせられる状態という2つの性質は、プログラミング言語の機能としてもライブラリの設計としても繰り返し発見されてきました。3回とも同じ基準で割り切れた本章の経過は、その一例にすぎません。
設計原則
原則: フェーズ間の一時的な値は、structフィールドではなく局所変数で表現できないか、まず検討します。
適用条件: 処理の開始から終了までの順序があらかじめ決まっており、一回実行されれば役目を終える手続きに対して適用できます。フェーズが増えるたびにenumバリアントとmatchアームが線形に増えていく設計は、この条件に当てはまる兆候です。
実装の形: 1本のasync関数の中に手続きを直線的に記述し、フェーズの区切りごとにyield_stepで外部との入出力を受け渡します。フェーズ間で持ち越す値は、恒久的なフィールドではなく、関数スコープに閉じた局所変数として自然に表現できます。呼び出し側から見た契約は、薄いラッパーを介して以前と変えずに保つことができます。
限界: 外部イベントに応じて長期間分岐し続ける処理や、状態そのものを呼び出し側が問い合わせて判断材料にする処理には、この原則は当てはまりません。そのような処理は、局所変数に閉じ込めずに、FSMとして状態を外部から見える形のまま残すべきです。