はじめに:1日で動いた。しかし、それで終わりではなかった

キーを正しい順序で送りました。IMEの状態も、直前に確認していました。それでも、最初の一文字だけが変換されず、ローマ字のまま入力欄に残りました。

最初に疑ったのは、キー配列を変換するロジックそのものでした。しかし、ログを追っても、変換表にもタイミング判定にも誤りは見つかりませんでした。おかしいのはロジックの中身ではなく、ロジックが前提にしていた「いまIMEはこういう状態のはずだ」という思い込みの方でした。

さらに厄介だったのは、同じ手順を繰り返しても、必ず再現するわけではなかったことです。ある回では正しく変換され、次の回では最初の一文字だけが化けます。原因が入力の順序になければ、残るのは「そのとき何が起きていたかを、こちらが正しく把握できていなかった」という可能性でした。この種の不具合は一度きりではなく、似た症状が違う原因で何度も形を変えて現れました。

これは、Windows用の日本語入力エンジン「awase」(NICOLA親指シフト方式)を開発しているときに起きた出来事です。awaseは著者が個人で、長い年月をかけて作り続けているソフトウェアです。最初の動くプロトタイプは、開発を始めたその日のうちに完成しました。記録を見る限り、この初日の実装を担ったのはAIモデルのClaude Opus 4.6でした。翌日未明からは早くも、動いた経路の内部で型や資源の境界を引き直す作業が始まっています。しかし、その後の道のりは、1日で動いたことからは想像できないほど長いものになりました。

本書は、その長い道のりで繰り返し出会った「一見同じに見える不具合」を、症状・仮説・実験・発見・設計というひとまとまりの物語として記録したものです。理想的な設計を最初から思いついていた話ではありません。壊れ方を見て、そのつど設計を発見し直してきた記録です。

変換器ではなく、相手のいるソフトウェアだった

awaseは、見た目にはキー配列を別のキー配列へ変換するだけの、単純なフィルタのように見えます。しかし実際には、awase自身が制御できない相手を常に観測し続けるソフトウェアでした。

相手とは、Windowsが管理するIMEの状態であり、フォーカスがどのウィンドウにあるかであり、他のアプリケーションが送ってくる非同期のイベント(こちらの都合を待たずに届く通知)です。これらはどれも、問い合わせても正直に教えてくれるとは限らず、古い状態を返したり、一度返した答えを覆したりしました。

具体的にはIMEの開閉状態、変換モードの向き、フォーカス中のウィンドウといった情報です。awaseはこれらを直接読み出せず、遅れて届いたり順序が入れ替わったりする断片的な信号から推測するしかありませんでした。

Windowsは、IMEとやり取りするための仕組みを公式に二つ用意しています。古くからあるIMM32は、IMEのON/OFF状態や変換モードをアプリケーション側から尋ねたり指示したりするためのAPI(プログラム同士が情報をやり取りする窓口)群です。より新しいTSFは、IMEだけでなく手書き入力や音声入力も含め、アプリケーションと入力サービスの間を仲介する仕組みです。アプリによってどちらを使うかは異なりますが、awaseにとっては事情は変わりませんでした。仕組みを通じて値は返ってくるものの、それが「いま」か「少し前」かは、仕組み自体からは分かりませんでした。

この構造は、日本語入力という領域に固有のものではありません。協調してくれない外部プロセスを、一方向のキー送信と断片的な観測だけで制御しようとする場面は、GUIの自動化ツールにも、分散システムのクライアントにも、外界をセンサーで認識するロボットにも共通しています。awaseの不具合の多くも、日本語入力の癖ではなくこの共通構造から生まれていました。

この経験から、本書を貫く一つの問いが生まれました。外の世界を相手にするソフトウェアを設計するとは、どこまで「同じ」とみなしてよいかを決める仕事だ、という問いです。

同じという扱いが崩れる境目は、大きく二つありました。一つは時間をまたぐ境目で、遅れて届いた証拠をいま実行中の処理のものと見なしてよいかが争点でした。もう一つは空間をまたぐ境目で、IMM32を使うアプリとTSFを使うアプリのように性質の異なる複数の相手を、同じ一つのロジックで扱ってよいかが争点でした。中には、一度返した答えをあとから読み戻して確かめる手段を持たない相手もいました。そうした相手については、「同じ」とみなしてよい根拠が比較のしようがないという意味で、最初から存在しませんでした。

この区別を必要とした場面は、awaseの内側だけにとどまりませんでした。何を疑い、いつ疑いを解くかという同じ判断は、コードを書く過程そのものでも、開発の終盤に問われています。本書の終盤(第VII部)では、この境目にも触れます。

本書の大半では、まず時間をまたぐ境目を扱います。

外の世界を相手にするソフトウェアの設計とは、どこまで「同じ」とみなしてよいかを 決める仕事である。空間をまたいでは、性質の異なる相手を一つのロジックで扱ってよいか。 時間をまたいでは、遅れて届いた証拠を、いま実行中の試行のものと見なしてよいか。 そして、相手を読み戻せないなら、同じとみなす根拠は最初から存在しない。 そしてこの区別は、コードの中だけでなく、コードを書く過程そのものにも要る。

このうち、時間をまたぐ境目については、本書はすでに一つの答えを出しています。観測したことと、いま実行してよいことを分けなければならない、という考え方です。

観測は、常に少し過去のものです。実行は、常にいまこの瞬間に対して行われます。この二つを同じ値として扱ったとき、awaseは何度も壊れました。型として分けたときにだけ、壊れ方は減りました。しかし、この型分けだけでは説明のつかない壊れ方が、後になってさらに見つかりました。本書の後半では、もう一つの境目——空間をまたぐ境目——もあわせて追いかけます。

観測と実行を分けるまでの記録

本書に登場する事件や数値は、記憶だけを頼りに書き起こしたものではありません。コミット・ADR(設計判断の記録)・当時のコードそのものを一次資料として、そこから再構成しています。

調査の過程で、著者自身の当初の記憶と、記録に残っていた事実がずれている箇所もいくつか見つかりました。そうした箇所は、本文中で「当時のADRにはこう書かれていた」「現在振り返るとこう見える」というように、記録に基づく記述と著者の解釈とを書き分けています。

読者がこの本から得られるのは、完成した設計の説明書ではありません。ある不具合がなぜ起きたのか、最初の仮説がなぜ外れたのか、次にどんな実験をしたのか、という試行錯誤の順序そのものです。設計は、その順序の先にしか現れませんでした。

日本語入力エンジンの内部実装に詳しくなくても、本書は読み進められるように書いています。必要な前提知識は、話が必要とする箇所でそのつど説明します。読者に求めているのは、外部システムを相手にするソフトウェアを書いた経験、あるいはこれから書く関心の方です。

本書の終盤(第VII部、第13章から第15章)では、この境目をコードを書く過程そのものに向けた経緯を扱い、終章では著者が別に取り組んでいるロボット向けランタイムの設計にも軽く触れます。

まずは、最初の一文字がローマ字のまま残った、あの日の話から始めます。