2021-12-27

transient.elで同じdescriptionを持つ二つの無名コマンドが衝突する件

前回に引き続きtransient.elをいじっているのですが、prefixの定義において別のキーに割り当てたコマンド(関数)が呼ばれてしまう現象に遭遇しました。

再現するコードは次の通りです。

(transient-define-prefix talk ()
  "Let's talk to animal."
  ["Dog" ("d" "Talk" (lambda () (interactive) (message "bowwow")))]
  ["Cat" ("c" "Talk" (lambda () (interactive) (message "meow")))])

(talk)

実行してみれば分かりますが、犬も猫のように鳴いてしまいます(dを押してもmeowと表示されます)。

実際のコードはもう少し複雑で、既存のコマンドを呼び出し規約に合致するようにラッピングする関数が挟まっていてそのあたりを調べたのですが原因が分からず、仕方ないのでtransient.elの中を追ってみたら原因が見つかりました。

原因は transient.el の次の部分です。

https://github.com/magit/transient/blob/51c50d8c828b5fac2878b651e2188ad0c6f44184/lisp/transient.el#L1024

transient.elの中には無名の関数が渡されたときに内部で関数名を付ける処理があって、その関数名が transient:<prefix>:<description> の形式になっていました。上の例だと transient:talk:Talk という名前の関数が定義されます(C-h fでも確認できます)。d(Dog)もc(Cat)もdescriptionが"Talk"で同じです。従って同じ関数名になってしまうので、後に定義するc(Cat)の関数だけが使われてしまうわけです。ちなみにdescriptionが無い場合はkey(割り当てキー)が使われます。

せめて (format "transient:%s:%s:%s" prefix (or (plist-get args :description) "") (plist-get args :key)) くらいだったらなと思いますが、keyも重複が許されている(述語でどれかを無効化するのを前提に)みたいなので、それも完全では無いのかもしれません。gensymみたいにカウンターで数字を割り当てていくというのも手ですが定義のたびにどんどん増えていくのは嫌かもしれません(消せばいいだけ?)。

仕方が無いので自分でfsetで関数名を付けてそれを渡すような実装にしました。上にも書いたようにラッピングする関数を通しているので、元の関数名に何らかのprefixを付ければ大丈夫です。

上の例のような場合、ドキュメントの例にあるようにinfixを使えということになるのかもしれません(最初にdかcを選んでからtを押すというような)。infixについてはまだ理解が十分ではないのですみません。

Pingback / Trackback