IMEと確定結果の扱い

はじめに

かな漢字変換の結果を確定した時に確定した漢字を記憶しておき、次回以降の変換時に優先的に出力するのは、IMEのよくある機能です。 今回、この機能の劣化版を個人開発のIMEでも作ってみました。 まだ全然使い物にはならないものなのですが、 機能の作りの方針に関しては今後の開発にも参考にできそうに思ったのでメモしておきます。

既存のつくり

stateful-imeというオブジェクトがあり、ユーザーとの対話を担当しています。 stateful-imeは、state + statelessな機能というつくりになっています。 詳しくは以下にあります。 mhkoji.hatenablog.com

採用した方針

既存のつくりを素直に拡張する方針を採用し、次のように変更しました。 確定結果はhistoryと名付けました (今思うと適当な名前です)。 historyも状態の一つと言えそうに思い、stateのメンバとしました。

stateful-imeの役割は基本的には状態を保持・更新するだけであり、機能自体はstateless側の役割です。 今回は、確定した変換結果もstateful-imeに返却するようにstateless側を修正しました。 stateful-imeは確定した結果をstateless側から受け取ると、その確定結果でもって保持しているhistoryを更新します。

詳しい処理は下のようになります。 キー入力が現在の状態で確定するものだった場合、 senn.win.im.process-input:executeが確定した変換情報 (committed-segments) を返すので、 それでhistoryを更新します。

(defun process-input (stateful-ime key)
  (with-accessors ((history state-history)
                   (input-state state-input-state)
                   (input-mode state-input-mode))
      (stateful-ime-state stateful-ime)
    (let ((result (senn.win.im.process-input:execute
                   stateful-ime input-state input-mode key)))
      (destructuring-bind (can-process view
                           &key state committed-segments)
          result
        ;; update application state
        (when state
          (setf input-state state))
        (when committed-segments
          (dolist (seg committed-segments)
            (history-put
             history
             (senn.segment:segment-pron seg)
             (senn.segment:segment-current-form seg))))
        ...

input-stateの更新とhistoryの更新はどちらもstateの更新ということで兄弟のような関係になっています。

次にeffected-imeを定義しました。 effectedにはstateに影響を受けるという意味を持たせているつもりです。

(defclass effected-ime (senn.im:ime)
  ((kkc
    :initarg :kkc
    :reader effected-ime-kkc)))

メンバのkkcはviterbiアルゴリズムによるかな漢字変換を実行するためのオブジェクトです。

imeのレイヤーではsenn.im:convertメソッドがひらがな列をかな漢字混じり列に変換する機能を提供します。 effected-imeに対してsenn.im:convertメソッドが呼び出されると、 viterbiによるかな漢字変換 (senn.im.kkc:convert) 結果を得た後、 状態を取り出し (stateful-ime-state)、状態のhistoryを参照して上書きします(history-apply)。

(defmethod senn.im:convert ((ime effected-ime) (pron string)
                            &key 1st-boundary-index)
  (with-accessors ((kkc effected-ime-kkc)
                   (state stateful-ime-state)) ime
    (let ((segs (senn.im.kkc:convert
                 kkc pron
                 :1st-boundary-index 1st-boundary-index)))
      (history-apply (state-history state) segs)))))

effected-imeは状態を参照するだけで、状態自体を保持するようにはしませんでした。これはなんとなくです。 状態の保持・参照を同じオブジェクトに定義する必要はないかなとなんとなく思って分離しました。

状態の扱いの定義は分離された一方、実行時には全部入りのオブジェクトが必要です。 なのでstateful-imeとeffected-imeをミックインし、状態を保持・参照・更新できるクラスを定義します。 今後はこのオブジェクトがユーザーとの対話を担当します。

(defclass stateful-effected-ime (stateful-ime
                                 effected-ime)
  ())

採用しなかった他の方針

上述したように、sennのかな漢字変換はviterbiアルゴリズムというよく知られた方法を利用しています。 history-applyをこのviterbiレイヤーに持って来るように修正しても今回の機能を実現することはできるのですが、 この方針は採用しませんでした。

viterbiレイヤーには言語モデルやviterbiアルゴリズムなど、一般的に知られた処理が属しています。 一方、以前の変換を利用するのは、よく知られてはいるものの、 以前の変換の定義については一般的には知られていないため、senn独自のものになります。 実際、今回の実装ではこの以前の変換を定義しているのは、上で述べたようにhistoryになっており、 これは一般的に知られていない、今後変更されそうな、今この瞬間のsenn独自のものです。 したがって、viterbiレイヤーは一般的に知られた処理のためのレイヤーとしてそのままにしておき、 今回の処理はsenn独自のレイヤーに持っていくことにしました。

おわりに

採用した方針と採用しなかった方針、採用した方針における状態の扱い方について書いてみました。 今後の機能づくりの参考になればと思います。