Emacsのwidgetについて思ったこと

id-manager.el にて Emacswidget を使ってみました。その感想などです。

widgetとは

widgetEmacs上でダイアログのようなUIを作るライブラリです。CustomizeなどのUIがこれで出来ています。
最近のEmacs(少なくともEmacs23)には標準で入っています。



widgetの例(Infoのサンプル)

どこが良いか

複数の入力項目をユーザーに要求するような場合、widgetを使うことで非常に分かりやすいUIになることが多いです。

grepのUIで具体的に比べてみます。lgrepとezgrepという、Emacsからgrepするプログラムがあります。

lgrepは、「検索したい文字」「どんなファイルを検索するか」「どこのディレクトリを基準に探すか」という情報をミニバッファから順番に入力します。初めて使うときは何を入力しなければならないのか分からなくて不安です。また、入力する順番が決まっているので考えながら入力しなければなりませんし、慣れてても順番を間違えたりして最初からやり直すこともあります。

一方のezgrepは、以下のような画面です。(widgetを使っているわけではないのですが、似たような入力フィールドの機能を独自で実装されています。)



ezgrepの画面(ezgrepのサイトから引用)
入力すべき項目があらかじめ表示されているため、初めて使うときでもユーザーは求められている項目を理解できます。また、好きな順番で入力することが出来るため、思いついた順番で入力できます。最後に重要な点として、入力した内容を確認して必要があれば修正できます。

結果として、ミニバッファの連続入力の代わりにwidgetを使うことによって、ユーザにとって分かりやすく、またストレスが少ないUIを提供することが出来ます。もちろん、widgetを使っても分かりにくいUIが出来る可能性はありますが、おそらく、そのようなプログラムはミニバッファを使ってもさらに分かりにくいインタフェースになるのではないかと思います。

UIとプログラムのしやすさ

UIはwidgetを使うことで使いやすくなるのですが、その代償としてプログラムが遙かに煩雑になります。

ミニバッファで入力させるインタフェースは、入力結果をそのまま変数に代入していくことが出来ます。また、順番もソースコードの順番で決まっていて、後戻りもしません。入力に問題があった場合、再度入力を求めるか、エラーで中断させることが出来ます。シンプルな場合には一つの関数の中で完結させることが出来ます。

一方で、widgetはバッファから作る必要があります。さらに、widgetに入力完了などのアクションを仕掛けていく仕組みになるため、いくつか関数を書く必要があります。ということで書く量は多くなります。

といってもすごく難しいわけではなく、Webのアプリなどで普段やっているようなことですので、他のGUIアプリなどの開発の経験があればすぐに出来ると思います。

情報

使っている人が極端に少ないためか、情報があまりありません。いろいろ探してみたのですが、今のところEmacsに含まれているInfo(英語)しか詳しい資料がないようです。ただ良く書かれていますので、とっかかりとしては十分だと思います。infoのサンプルコードや、EmacsWiki上のサンプルコードを見れば、大体動きそうなものが出来ます。

ただ、エディタの仕組みの中で無理矢理実現している実装なので、動きが微妙だったり、他のGUIプログラミングとは違う部分が多々あります。なのでwidgetのコード(wid-edit.el)を参照しなければならない場面が多々ありました。全体のコードの量は多いですが、大半がwidgetコンポーネントの定義なので、コアな部分はすごく小さいです。多分読みやすい方だと思います。

id-managerでの例

以下の図のような感じで作りました。



id-manager.elでのwidgetの動き

入力欄が4つ並んでいるだけだったらすぐ終わる内容だったのですが、パスワードの表示を切り替えるところではまりました。wid-edit.elのコードを読みながら試行錯誤を繰り返していたため、他の慣れたGUIツールだったら一瞬で終わることが、widgetを使ったダイアログを作るのは丸1日かかってしまいました。多分、次からはもっとうまくやれるとは思います。

はまったところ

構築中にエラーが発生した場合、分かりやすくエラーが出ず、また一見widgetも構築できているように見えることがあります。widgetデバッグ中は toggle-debug-on-error でエラーを表示するなど注意した方が良いです。

widget-create するとその時点バッファにinsertされてしまいます。ですので、レイアウトを考えながらプログラムの順番を考える必要があります。気軽にレイアウトを変更できませんし、コードも長くなりがちです。

ボタンなどのアクション(イベントハンドラ)の中からwidgetの内容を更新する場合は、最後に widget-setup を呼ぶ必要があります。

widget-setup で反映できない属性もあります。今回の例では、チェックボックスのON/OFFでパスワードフィールドの内容の表示・隠すの動きが苦労しました。widgetの機能が構築時に決まってしまうようだったので、バッファごと作り直すことにしました。

バッファを作り直す際に、同じバッファを再利用すると、バッファローカル変数などが残っていてうまく動かないときがあります。自分でいろいろ初期化してしまえばいいのですが、内部の実装に依存するのは良くないと思いますので、素直にkillしてから作り直す方が良いと思います。

widgetの内容の検証は :validation 属性に関数を入れるのですが、全体の設計としてこれをどう使えばいいのか結局よく分かりませんでした。各地のコードを読むと、 widget-apply でここに入れた関数を自前で呼んでいるようなのですが、そうすると実装がややこしくなるだけであまりうれしくないような気がしました。今回は普通に自分で内容チェックをしています。

以上のようなことをしているうちに、widgetでのプログラミングは、ローカルアプリのGUIの設計や考え方ではなくて、Webアプリの考え方が近いのかなと思いました。バッファを毎回生成するところや、エラーメッセージの場所を確保したり、セッション変数のようなバッファをまたがる変数が欲しかったりなど。次回からはうまくやれそうな気がします。

もやもや

今までいろいろなGUIアプリを書いてきた経験から、widgetは非常に使いにくいと感じました。

まず、手続き的構築と宣言的構築が混ざっていることが問題です。通常は、JavaのSwingなどのようにプログラムで手続き的に構築していく方法(オブジェクト生成とレイアウトを繰り返す)が低レベルの層に用意してあり、気の利いたGUIツールキットだと、HTML・FlexあるいはVBなどのようにGUIや宣言的に画面を定義する方法がその上に用意されているという構成になっていることがほとんどです。widgetでは構築した瞬間にフィールドが配置されてしまうため、どちらでもありません(もしかしてTcl/Tkに近い?)。せめて勝手にinsertしないでいてくれれば手続き的に構築していけるのですが、現状そうするためにはさらに関数かマクロでラップする必要があります。

バッファがwidget専用になってしまうことも困ります。実装を見ると、insertしたwidgetバッファローカル変数に入れておいて、 post-command-hook や change hook まわりで頑張っていますので仕方がないのですが、もう少しそのことをドキュメントで強調しても良かったのではないかなと思いました。最初は scratch バッファに貼り付けたり、対話的に構築して実験していたので、widget構築による副作用ではまってしまいました。

あとは、widgetの責任範囲がどこまでかが分からないと思いました。widgetの属性を眺めていると、いろいろと便利そうな枠組みが用意してあるように思えるのですが、実際には枠だけ用意してあって、実際には自分で呼んでその結果を自分でまた判断しなければならなかったりと、どう使えばいいのか、Infoやコードを読むだけでは分からないことが多かったです。そのあたりについて、もう少しwidgetの考え方というかフレームワークとしての哲学みたいなものが欲しかったかなと思います。

widgetのコードは、似た処理やよく似たタイミングの処理をいろいろ集めてきて共通化したもののような形になっているように見えます。コードは綺麗ですし、ボトムアップとしてすごくうまく共通化は出来ているのですが、トップダウン的な設計(API利用者へどう使ってもらおうという方針)が無いような気がしています。

15年以上前からの実装ですし、いろいろな歴史的経緯(w3とかcustomizeとの関係など)から現状の実装に落ち着いたのかなということもコードから分かります。何も知らない新参者の意見です。すいません。

結局

とはいえ、ミニバッファでの連続入力よりはwidgetの方がいいUIを提供できることは確かですし、現状ではダイアログを構築する手段もwidgetしかないことも確かです。

結局、別のGUIツールキットを作るほどの余裕もないですので、必要な場面では今後も頑張って widget を使っていこうかなと思いました。