hkoba blog

プログラマーです。プログラミング言語ミーハーです。よろしくどうぞ(能代口調)

"--name <SPC> value" style options considered harmful for end-user scripting.

追記20170914朝JST: 以下の議論では簡単のため --name value に話を絞り -o VALUE 形式への言及を省略したが、後者においても辞書が必要となる点は変わらない。自明とは思うが念の為… 20170914昼JST: タイトルtypo 修正 ><

ストーリー

  • 以前あなたはプログラム A を書いた。それは職場で同僚たちが日々使用している。今のバージョンにはオプションが 5 個ほどある。オプションは今後も増え続けるだろう。
  • 同僚たちも、あなた程ではないが、省力化のための簡単なスクリプト程度は好んで書く人たちである。
  • あなたの書いた A は、同僚の書いたスクリプト B(それは例えば日々の仕事を定期実行するためのバッチや、よく渡すオプションを予めまとめたラッパースクリプトである)から呼び出されるようになった。
    • 同僚のスクリプト B を起動する時に、そこからあなたの A へ、いつもと違うオプションを渡したくなる時がある。だから B は殆どのオプションをあなたの A に素通しで渡したい。
    • ただし、 B 独自のオプションも持ちたい。
  • 同僚たちは (B だけでなく) 元々 A のユーザーでもあるので、あなたの A のマニュアルを読んでいる。そこにオプションが --name value (name と value の間のスペースに注意。 --name=value ではない)の形式で書かれている場合、ユーザーのメンタルモデルにはオプションが --name value 形式で刻まれる。だから B も、その仕様に従うだろう。
    • 何より、職場の Wiki に日々書かれるコピペ用のコード例が --name value スタイルで書かれるだろう。

“–name value” オプションの切り出し処理は辞書を要求する

  • あなたの A が、オプションを --name value 形式で受け取る場合、 --name に続く次の引数が、オプションの value か、あるいは無関係な別の引数であるかは、 name 次第となる。つまり任意の引数列からオプション列を切り出すには、 オプションの名前を網羅した 辞書 を用意する必要が有る。
  • つまり同僚のスクリプト B にも、あなたのプログラム A の全オプションに関する辞書が必要になってしまう。

辞書で対応付けを作った箇所には、変更のバケツリレーも付いてくる

  • 同僚のスクリプト B に対して、誰かが更に別のラッパースクリプト C を書く時もある。 C にも A のオプション辞書が必要になる。 D, E, F… 止まらない保証はない。
  • あなたが元のプログラム A を改良してオプションを追加したら? スクリプト BC のオプション辞書にも改良が必要になる。オプションが増減する度に、この改良(?改良なのか?)の連鎖が起こる。これバケツリレーだ、 WWII の空襲記録で見たこと有るやつだ…

誰かが、この無限の連鎖を止めなければならない…誰かが…

一案としては、 A にオプションの辞書一覧を機械可読な形式で出力する機能を用意する。 B を書く人には、それを読み込んで処理する機能を書いてもらう。これなら処理を書くのは一度きりで済む。 ただし…ちょっとスクリプトを書けるようになり始めた位の人に、これを要求するのは、 ハードルが高すぎる。職場全体で少しでもスクリプトを書く人数が増えて欲しい時には、 ハードルを少しでも下げたい。

もっと簡単に書けて、処理も軽く、漏れがなく、一度書けば済む…そんな、貧者のアプローチがないか?

どうすればよい?

既に書かれた 他人のプログラムは、諦める とする。自分たちがこれから書く、職場限定のプログラムに限り、 オプションの与え方に一律の制限を課す(標準化する)。例えば:

  • --name=value
  • --name (value なし)
  • -c (一文字オプション。value なし)
  • オプション列の終わりは -- で与える

このように制限をすれば、辞書抜きで機械的にオプション列を切り出すプログラムを書けるようになる。辞書と異なり、この処理は一度書けばずっと使える。プログラム A にオプションが増えても、 B のオプション切り出し部分に変更は必要ない。 (もちろん、 B 自身のオプション名と被った場合は問題だが、少なくとも切り出しプログラムの問題ではなくなる)

規格化・標準化は自動化・省力化の友

諸君、私は自動化が好きだ。愛していると言ってもいい。 自動化出来るか否かに比べれば、使い方が流行のプログラムに似ているかなど、どうでも良い。 オプションを --name value のように書きたいばかりにあなたを辞書変更の無限地獄へ巻き込もうとする者たちからは、出来るだけ離れていよう。 多分、連中は雇用対策でそうしているのだろう。我々の人生はもっと有益なことに使うべきだ。

オチ

ラッパーから2個以上のコマンドを呼ぶ時、困るよね…段々辞書が欲しくなるよね… -- を書いてもらう?う〜ん…

『実行可能なモジュール』設計パターンについて…あるいはサブコマンドを持つコマンドを私はどう作るか

『実行可能なモジュール』と私が勝手に呼んでいる、ある種の設計パターン/コーディングイディオムについて、 私なりの意見を整理しておこうと思います。

(この設計パターンは Perl 以外の言語でもよく見かけるので、既に名前が付いているのでは?と予想しています。 教えて頂けるとありがたいです)

pm に shbang と unless caller を書く

unless caller

Perl スクリプトで、ファイルの最後にこんなコードを見たことが有る人は、いるでしょうか?

unless (caller) {
  ...何らかの処理...
}

この unless (caller) {...} のブロックは、このファイルをコマンドとして直接起動した時だけ 呼ばれる処理を記述したものです。私が初めてこの種の書き方に触れたのは 1996頃の Perl/Tk の文脈 で、

MainLoop unless caller;

と書いて

  • このスクリプト自体が起動された時は、 Tk::MainLoop() を呼び出す
  • それ以外のケース、例えば上記スクリプトを別プログラムからクリップボード経由で直接 eval したり do などでロードした時は、何もしない。

という動作を実現するために使われていました。

myscript.pl の代わりに MyScript.pm

さて、この unless caller というイディオムは、 Tk に限らず一般の Perl スクリプトでも役に立ちます。 例えばスクリプトを書く時、 myscript.pl の代わりに MyScript.pm という名前にして package 文も書いて、 正当なモジュールとしてロードできるようにします。その上で、最後に unless caller で、コマンドとして 起動された時の処理を書くのです。

例えば以下のように書きます。

#!/usr/bin/env perl
package MyScript;

...

unless (caller) {
   my @opts; 
   push @opts, split /=/, $_, 2 while @ARGV and $ARGV[0] =~ /=/; # XXX:手抜き
   my $app = MyScript->new(@opts);
   $app->main(@ARGV);
}

1;

すると、この MyScript.pm は (chmodして) 直接コマンド行ツールとして起動するだけでなく、 モジュールとしてロードし、一部のメソッドだけを呼び出すことも出来るようになります。

# コマンドとして起動し、 MyScript->new(x=>100,y=>100)->main('foo','bar') を呼ぶ
% ./MyScript.pm x=100 y=100 foo bar

# モジュールとしてロードして new し、メソッド foo() を呼ぶ
% perl -I. -MMyScript -le 'print MyScript->new->foo'

サブコマンドをメソッドに対応付ける

先の例では unless caller 時には MyScript->new->main を呼ぶように決め打ちしてありました。 ここを

  • posix style の long option --name=value の列を new() の引数にする。
    • --name のみなら --name=1 として扱う。 --debug みたいに。
  • 次に来た引数をサブコマンドの名前に使う。

という動作にすればどうでしょう? こんなイメージです。

# 何らかのテキストファイルをパースして、DB にロードする
% ./MyScript.pm  --dbname=foo.db  import journal.tsv

# 上記 DB から特定の条件で検索をする
% ./MyScript.pm  --dbname=foo.db  list_accounts

早速、これを実現する unless caller ブロックを書いてみましょう。 parse_opts() は後で定義することにします。

unless (caller) {
   my @opts = parse_opts(\@ARGV);

   my $self = __PACKAGE__->new(@opts);
   
   my $cmd = shift @ARGV || "help";

   my $method = "cmd_$cmd"; # サブコマンドのメソッド名は cmd_... で始めることにする。

   $self->can($method) or die "No such subcommand: $cmd";

   $self->$method(@ARGV);
}
  • ここでサブコマンドのメソッド名に接頭辞 cmd_ を付けることにしたのは、 例えば import というメソッド名が Perl にとって特別な意味の有るメソッド名で、 これと被ると予期せぬ面倒を生みかねないからです。

発展:任意のメソッドをサブコマンドとして試せるようにする

先の unless caller ブロックで呼び出せるのは cmd_... で始まる名前のメソッドだけでした。 これを任意のメソッドまで呼べるように拡張すれば、内部的なメソッドも CLI から 簡単に呼び出して試せるようになります。特に Perl は REPL が弱いので、 これを使えば好きなメソッドを shell のヒストリ・エディタ上で反復的に試せるようになり、 REPL の弱さを補うことが出来ます。

ただし、普通のメソッドは結果を Perl のスタックに返すだけで画面には何も出しませんから、メソッドの結果を 出力する機能も作る必要があります。また戻り値は undef や [], {} … を含みますから、 出力時はシリアライザーを通したほうが良さそうです。

以上を考えた unless caller ブロックは、例えばこんな感じでしょうか>

use Data::Dumper;

unless (caller) {
   my @opts = parse_opts(\@ARGV);

   my $self = __PACKAGE__->new(@opts);
   
   my $cmd = shift @ARGV || "help";

   # cmd_ で始まるメソッドがあるなら、そちらをそのまま呼び出す。
   if (my $sub = $self->can("cmd_$cmd")) {

     $sub->($self, @ARGV);
   }
   # それ以外でも、メソッドがあるなら、デバッグ目的で実行できるようにしておく。
   elsif ($sub = $self->can($cmd)) {

     my @res = $sub->($self, @ARGV);

     print Data::Dumper->new(\@res)->Dump;
   } 
   else {
     die "No such subcommand: $cmd";
   }
}

もちろん、もっと強化できる所はあります。

  • @res の内容に応じて終了コードを設定すると、シェルスクリプトから使う時に便利になります。
    • スカラーコンテキストとリストコンテキストをオプションで使い分けられると嬉しい人もいるでしょう。
  • 出力時のシリアライザーを JSON にする手もあります
  • 引数の文字列が {...} , [...] の形式の時に JSON としてデコードする手も、ありえます。

この辺りに興味の有る方は MOP4Import::DeclareMOP4Import::Base::CLI_JSON もどうぞ…

この設計パターンの使いどころ

あまり有効でないケース

先に、このパターンがあまり有効でない状況を挙げます。

  • 開発するプログラムの仕様が十分に確定しており、設計に時間を掛ける余裕が有る。
  • 開発者が十分に足りていて、モジュールを分割するほど、手分けして並列で開発を進められる。
  • 作ったけど使わない、という可能性を考えなくて済む。

この記事で挙げたパターンは複数のサブコマンドを一個のスクリプトファイルに書くので、 複数人で並行開発することは困難だろうからです。

有効に働くケース

逆に、

  • 何を作れば『ビジネス上の要求』を満たせるか分からない、探索的な開発をする必要が有る。
  • 顧客が要求仕様をまとめられず、どんなコマンドを何個作ることになるか、全然予測できない。
  • それを作っても使うか分からない、業務に投入してみないと何も言えない時。

という状況下で、それでも前進しないと駄目な時には、以下のメリットがあります。

  • 最初から OOP 用のクラス・モジュールとして書いているので、いつでも継承してメソッドの挙動を変える、など自由自在に OOP の技法を投入できる。
    • もし初手をコマンドとして書き後からクラスへ括り出す場合だと、そこでクラスの命名で悩む時間を取られる。
  • サブコマンドを増やし放題。メソッドへ括り出し放題。あらゆる無茶振りを一旦受け止めるための、汚れ役クラスとも言える。
  • CLI ベースで作って、後から Web 経由で (Fat な) Model として使う、という手も有る。
  • モジュールになっているので、テストも書きやすい。

とは言え、これで作ったものは巨大な一枚 pm になりがちなので、機会を見つけて整理をするのが大事、ではあります。 (…吐血…)

なんでこれ書いたのか

songmu さんの blog 記事

www.songmu.jp

私も UNIX 哲学は好きで、概ね同意できる、と思いつつ…

自分が Perl で仕事のツールを書く時によく使うスタイルの話も書いておいたほうが、誰かの役に立つかもな〜、と思ったからでした。

おまけ

parse_opts() の例です(既存のコードを解説用に簡略化したものなので、動作は未検証です)>

sub parse_opts {
  my ($list, $result) = @_;
  $result //= [];
  while (@$list and my ($n, $v) = $list->[0]
         =~ m{^--$ | ^(?:--? ([\w:\-\.]+) (?: =(.*))?)$}xs) {
    shift @$list;
    last unless defined $n;
    push @$result, $n, $v // 1;
  }
  wantarray ? @$result : $result;
}

業務向け情報共有サービスに私が求めるもの

Markdown Night 2017 Summer - connpass で色んな話を聞けて刺激を受けたものの、その後の懇親会で自分の問題意識・欲求がいまいち上手く伝わらない・伝えられないことに気づいたので、自分なりに整理(?)しておきます。

TL; DR. 社内マニュアル向けシステムには、情報の構造化と新陳代謝を支援して欲しい

10年以上前から Wikiの一つを チーム内マニュアル書きに使ってきたものの、長年不満を感じている。 Wiki も色々有るし、更にもっと良さげな情報共有サービスも色々出てきたので、 実は既にこの問題は解決されているのかもしれないけれど… とにかく自分の問題意識を書き下しておきたい。

オーソドックスな Wiki の特性

  • ページはページの名前で識別されリンクされる (例外: tcler’s wiki)
  • ページの追加は、既存のページに(新ページへの)リンクを書くことで始まる
  • 既存ページと新ページの間には、単にリンクが貼られている以上の(システム的)関係は無い(例えば「このページは元ページの子ページである」という情報は、システム内には記録されない。 (例外: redmine )
  • ページの並び、という概念も存在しない。ゆえに、親ページ上に並んだリンクの順番はあっても、それに基づいて子ページ間に「前へ←、→次へ」のようなページャを配置することは、手作業になる (…sphinx ベースの wiki ってあるのかしら?)
  • ページ内については wiki 記法で章立て・構造化をすることが出来るが、これが検索の単位と連動するとは限らない。 (例外: mediawiki)

要するに、Wiki で書かれたページ群は、(Wiki 奉行でも配して特別な注意と労力を払わない限り) ランダムに更新され成長してゆくグラフ・ネットワーク に近い構造に至る。

ページ群がランダムグラフに至ると、誰がどう困るか

  • 新人教育で、教える側が困る。
    • 指導用の教材は一本道に整理したいが、ページャのリンクを手書きはやってられない。
  • 教わる新人が困る
    • 読むべき資料が一本道の木構造なら、要する努力も見積もれるが、グラフでは底なし。
  • ベテランも日々困る
    • 検索をセクションで部分木に絞り込めないので、全マニュアルから各人が案件ごとに書いたアンチョコが無限にヒットする。が、どれが正しいか、今のオススメはどれか、今見ているページが古くないか…全くわからない。
    • 検索が弱いので、重複した内容の、けど少しずつ違うマニュアルを各人が好きに書くことになり、余計に検索を悪化させる。
    • 事例を既存のどのページにぶら下げるかべきか、どんな名前のページにするべきか、新しい事例ほど迷って時間を食う。けど、書き残さないとあとで困るので、とりあえずの場所にぶち込むことに。そして整理されない。Drag & Drop で移動したい…
    • 更新のタイムラインを見る暇があれば、誰かがマニュアルを書き換えたことは把握できる。が、後で実際にマニュアルを見ている時に、それが参照するに値するのか、判断する基準がない。(例えば業務システム revision X のリリースより前なのか後なのか…) いつ頃誰が書いた記述かわからない(要するに git blame が欲しい)
    • 新機能リリース時に、マニュアルのどこを改訂すればよいのか。自分で書いたマニュアルは改訂すれば良いとして、それに基づいて皆が書いたアンチョコは古いまま。どうするか。(機能とドキュメントの対応関係がシステム化されていない)

理想を言えば php.net (docbook) や amon2 (sphinx) のような一本道の 「本」 として構成された公式マニュアルをオンラインで作ることが出来て、かつ、その特定の章に絞った検索が出来て欲しい。それとは別に、その本という枠組みに縛られずに自由にページを足す機能は欲しいが、未分類なページを似たもの同士集めて分類を育てていく支援も欲しい。タグは…破綻させない仕組みがあれば…。重複マニュアルをどう見つけるか…。そして何より、部分的に古くなったマニュアルを、どう見つけて改定していくか…

おまけ

メモ:mod_fastcgi で FastCgiExternalServer 使いたい時は FastCgiWrapper を Off にしないとダメぽい

Apache2 の mod_fastcgi で 初めてFastCgiExternalServer を使おうとしたらハマったのでメモ。ツッコミ歓迎です。

TL;DR FastCgiWrapper を Off にしないと、他を正しく設定しても 404 Not Found にされてしまう(ぽい)。

プロセスを起動するか、外部で起動したプロセスに繋げるかの判定フラグ

content_handler() を見ると fr->dynamic が肝らしい

fr->dynamic を設定する箇所

create_fcgi_request() のここによれば、 fs == NULL で判定している。

ファイルが有っても fs が NULL になってた…なんで?

create_fcgi_request() のこの箇所が fs を設定している

fcgi_util_fs_get_by_id(const char *ePath, uid_t uid, gid_t gid) は何してる?

ソースのコメントに曰く>

Find a FastCGI server with a matching fs_path, and if fcgi_wrapper is enabled with matching uid and gid.

fcgi_wrapper (suexec の fcgi 版)が有効になっていると uid/gid 検査が走って、それが常に偽なので NULL が返る。

結局、将来 ~user でユーザ権限 suexec な fcgi を使いたくなった時のため…と思って FcgiWrapper を On にしてたのが 引き金でしたと。

かんそう

やぷしー(やっぱち, Y8)2017春に行ってきました感想〜

y8-2017-spring.hachiojipm.org

勉強になったし、なにより楽しかったです!

感想など

V8 for フロントエンドデベロッパー by brn(ブルーノさん)

V8 のソースをこれから読み解こうとする人のための、ガイド的な内容でした。 ぼくもよむー!ってなったです。

(ブルーノさんは中の人ではないとのこと。だけど、めっちゃソースと周辺解説を読み込んでることは伝わってきました)

  • V8 が関数内の AST 作成を最初の呼び出しまで遅延させている、って話を聞いて Tcl を思い出しました。 (V8 の方が PreParser で構文エラーまで検出しているとのことだったので、プログラマーにとっては V8 の方が有り難いですね)
  • V8 のソースが結構グローバル変数に頼ってる、という話を聞いて、V8 もそうなのですか、言語処理系あるあるだなぁと思ったりしました。
  • (それにしても V8 は現在 108万行ですか…煩悩の一万倍ですね…)

ScalaでウェブAPIを書いている人が設計や実装やその他について話そうか by たっくんさん

恥ずかしながら私は DDD をちゃんと勉強しておらず、レイヤーだけは感覚で切り分けて書く、というスタイルです。

で、発表で解説されていた

  • プレゼン層→アプリ層→ドメイン層→インフラ層(データストア)

の問題と、それを解決するための DIP

  • インフラ層→プレゼン層→アプリ層→ドメイン
    (ドメイン層でInterfaceを定義してインフラ層はそれを実装)

の話は非常に分かりやすかったので、この部分だけでも活かせそうだと感じました。

hyperapp – 1kbのビューライブラリ by Jorge Bucaran (ジョージさん)

先日の吉祥寺pm でも発表を聞かせてもらった hyperapp 、 更に サンプルや解説も充実 してきてるんですね。これは流行るかも…

なにより、ソースが短い(約300行)のは、大きな美徳ですね!

.NET Core がLinuxでどのように動いているか、またわれわれはどのようにデバッグするのか by たなかさん

tech.tanaka733.net

RedHat の中の人が .NET Core の話をするという点が、まず興味深かったです。

  • dll のシンボル一覧の件を伺ったのは、先日 fsharp で nuget でライブラリを入れて dll が3つ入った後で、 ある関数を呼ぶのにどの dll への reference を追加すれば良いのか分からなくて、入れては試し、をする羽目になった時に ふと疑問に思ったのです(o)
  • .oO(…本当は Mono の話も聞けたら嬉しいな〜、などと…すみません><…)

MySQLサーバーのパフォーマンスチューニング by まみーさん

もう何年も MySQL 触ってないので、こういう情報はありがたいです。

  • 特に、改善前・後の秒間リクエスト数の実測値こみの発表だったことが、良かったです。
    (効果確認の具体的な数値指標があれば、あとで自分で試した時に設定失敗に気づけるようになるので)

チームで取り組む Singe Page Application by オカムラさん

(うちの仕事ではまだまだ SPA は遠い先なのですが、)

プログラマーとデザイナー?さんとの分業を如何に互いにハッピーな形に切り分けるかは 私もずっと興味を持ってきた領域なので、とても興味深かったです。

  • あと、かつての Flash 屋さん的な人こそが SPA 屋に向いているのでは?、という感覚は、私も薄々感じています〜

謹賀新年/2017目標

あけましておめでとうございます

昨年度仲良くして下さった皆様、ありがとうございました。

本年もよろしくお願いします。

昨年の良かったこと

  • プログラミング方面
    • 吉祥寺pm で、 自分の Perl プログラミング・スタイルの大きな柱である fieldsexporter 周りの知見を発表させてもらえた事。
    • 久々に俺々言語の実装実験に取り組めた事。 自分の構想する VM 実装技法の上に(仮組みレベルでも) lisp を実装できたことは、大きな前進だった。
  • 暮らし
    • 引越し先候補を幾つか下見できた事。
    • 荷物減らしが進んで、部屋のレイアウトを変更できた事。
    • (人生3度めにしてやっと)メガネからコンタクトへの移行が出来た事。

昨年の反省点

  • 俺々言語の実験成果を公開できるレベルにまとめられなかった事。 普通の lisp を作るだけならそう難しくないが、 そこから離れた独自の意味論を作る所をどう実験していくかが今の課題。
  • 結局、引越し先を決めるに至らなかった事。(探す方向性を変える必要があると気付けたのは収穫)

要するに 昨年の目標 と見比べて ほぼ進展が無いこと(><

今年の目標

  • 引っ越ししたい
  • 俺々言語に進捗を
  • fsharp 勉強しなきゃ… docker もそろそろ…
  • 彼女(嫁)探しを諦めない…

Smalltalk の勉強会に参加してきました(ハッカソン編)

smalltalk.connpass.com

私にとって Smalltalk は学生時代から憧れの言語で、にもかかわらず何度挑戦しても (Emacs keybind が使えないがゆえに) 挫折を繰り返していた言語です。何とか突破口を開けられないかと参加させてもらいました。

以下、教えて頂いたこと・調べたことの備忘録です。間違いなど有るかもしれませんのでツッコミ歓迎です。

ハッカソン(午前)

私は Smalltalk 初心者なので、Pharo5 のキーボード・マッピング周りの仕組みを調べて学ぶことにしました。

途中、梅澤さん mumez (Masashi Umezawa) · GitHub に Pharo のショートカット周りのクラスやそのオーバーライド方法、 クラスブラウザの操作方法を色々教えて頂きました。

  • PharoShortcuts current で取り出されるインスタンスを通して、ショートカットが設定されているらしいので、ここをオーバーライドすれば出来そう、とのこと。(ただ、そのために PharoShortcuts クラス自体もいじらなければならない。)
    • この使用箇所を探すとき、クラスブラウザ上でクラスを選んで右クリック→ AnalyzeClass refs... が役に立つことを見せて頂きました。
    • インスタンス変数は大文字はじまり、と。
  • (Web ブラウザの開発ツールに有るような) マウスで画面要素を直接指して中身を Inspect したい時は、 Shift + Mouse MiddleHalo を呼びだせば良いそう。

雑談

お昼ごはんの時にも雑談で色々教えて頂きました。

  • イベントループはどこに有るの? → WorldState にあるよ
  • Pharo にも Object Table は有るの? → ある。 Newspeak, Pharo, SqueakVM (の設計?) が同じ
  • allInstance の逆はあるの? → allOwners というのがある。(Morph のみ?)
  • バージョン管理機能があると聞いたけど、 CompiledMethod はイメージ内に複数持つの?一つだけ? → 一つだけ。

あと、皆さん pharo を一番使ってたのが印象的でした。 pharo のコア開発者はフランス inria 繋がりの人が多いとか…

<keymap> は Pragma だった!

以前の挫折ポイントの一つが、この↓ <keymap>

TxTextEditorMorph>>#buildTextEditorKeymapsOn: aBuilder    
    <keymap> 
     
    {  
        Character home. #moveToLineStart.
        Character home shift. #selectToLineStart.
           ...

これは Pharo の Pragma 構文で、所謂 コード注記 と同じ役割を果たしているそうです。 (メソッドに属性としてぶら下がる)。 Pharo は Pragma を色々活用しているとのこと。

ハッカソン (午後)

ところが PharoShortcuts の中身をよく見たところ、肝心のカーソル移動が定義されていないことに気づきました。 これでは Emacs 風のカーソル移動は定義できません。

そこで方針を変えて、そもそもキーマップ機能と、イベントループ周りがどんな仕組みになっているのかを調べることにしました。

キーマップ関連

TxTextEditorMorph>>#initializeShortcuts: aKMDispatcher
    aKMDispatcher attachCategory: #TxTextEditorMorph

KMDispatcher というのが関係が深そう…

クラスのコメントを順々に抜き書きしてみます。

KMDispatch

I'm an object that saves a buffer of keyevents for the morph I'm attached.
I am the one that dispatches the single and multiple shortcuts.
If the morph has a keymap that matches the keyboard event,
I tell the keymap event to execute with the morph I'm attached.

KMRepository

I have a singleton instance which can be accessed by executing the following:
"self default"
 
I am currently a god object to be refactored =D.
KMRepository>>#reset
   World setProperty: #kmDispatcher toValue: nil.
   self default: self new.
   KMCategory allSubclasses
      select: [ :c | c is GlobalCategory ]
      thenDo: [ :c | c new installAsGlobalCategory ].
   KMPragmaKeymapBuilder uniqueInstance reset.

KMPragmaKeymapBuilder

I am a singleton object, subscribed to system events, to listen to the creation of
methods marked with the <keymap> and keymap:> pragmas.
  
When I listen one of those events, I reinitialize the KMRepository default instance
and reload it with all declared keymaps.

う〜ん、らちがあかない…

イベントループ

下から追うのが難しそうなので、今度はイベントループを調べてみます。

教えて頂いた WorldState と、グローバル変数 World (こちらは WorldMorphインスタンス) が関係が深そう。

WorldMorph>>#doOneCycle
    worldState doOneCycleFor: self.
    
WorldState>>#doOneCycleFor: aWorld
   self interCyclePause: MinCycleLapse.
   self doOneCycleNowFor: aWorld.
WorldState>>#doOneCycleNowFor: aWorld
    "Immediately do one cycle of the interaction loop.
   This should not be called directly, but only via doOneCycleFor:"

    DisplayScreen checkForNewScreenSize.

    "process user input events"
    LastCycleTime := Time millisecondClockValue.
    self handsDo: [:h |
        ActiveHand := h.
        h processEvents.  "…ここが肝ぽい…"
        ActiveHand := nil
    ].

    "the default is the primary hand"
    ActiveHand := self hands first.

    aWorld runStepMethods.      "there are currently some variations here"
    self displayWorldSafely: aWorld.
displayWorldSafely: aWorld
    "Update this world's display and keep track of errors during draw methods."

    [aWorld displayWorld] ifError: [:err :rcvr |
        "Handle a drawing error"
        | errCtx errMorph |
        errCtx := thisContext.
        [
            errCtx := errCtx sender.

                        ....

            "If the morph causing the problem has already the #drawError flag set,
           then search for the next morph above in the caller chain."
            errMorph hasProperty: #errorOnDraw
        ] whileTrue.
        errMorph setProperty: #errorOnDraw toValue: true.
        "Install the old error handler, so we can re-raise the error"
        rcvr error: err.
    ].

↑今読み返すと WorldState>>#doOneCycleNowFor: aWorld の中の ActiveHand processEvent が鍵ぽいのですが、 この時点では読み取れませんでした。

キー入力周り

再び下から、でも目線を変えてキー入力に関係するコードを探してみようと考えました。

Smalltalk なので、メタキーを扱うコードが有るはず… #meta の sender を見る…

SimulateKeystrokesSpecification>>#testSimulateCmdKeystroke の中に self simulateKeyStrokes: ... というコードを発見。

Morph>>#simulateKeyStroke: aCharacter
   |event|
   event := KeyboardEvent new
     setType: #keystroke
     buttons: 0
     position: 0@0
     keyValue: aCharacter charCode
     charCode: aCharacter charCode
     hand: ActiveHand
     stamp: 0.
   self keyStroke: event

KeyboardEvent クラス! この Class ref から、 HandMorph>>#generateKeyboardEvent を見つけました。 後はこれの sender を探すのみ。

HandMorph>>#processEvents
    "Process user input events from the local input devices."

    | evt evtBuf type hadAny |

    [(evtBuf := Sensor nextEvent) isNil] whileFalse: 
            [evt := nil.  "for unknown event types"
            type := evtBuf first.
            type = EventTypeMouse         ifTrue: [recentModifiers := evtBuf sixth. evt := self generateMouseEvent: evtBuf].
            type = EventTypeKeyboard      ifTrue: [recentModifiers := evtBuf fifth. evt := self generateKeyboardEvent: evtBuf].
            type = EventTypeDragDropFiles ifTrue: [evt := self generateDropFilesEvent: evtBuf].
            type = EventTypeWindow        ifTrue: [evt := self generateWindowEvent: evtBuf].                

            "All other events are ignored"
            (type ~= EventTypeDragDropFiles and: [evt isNil]) ifTrue: [^self].
            evt isNil 
                ifFalse: 
                    ["Finally, handle it"

                    self handleEvent: evt. "多分ここで、 HandMorph>>#handleEvent:evt が呼ばれる"
                    hadAny := true.
                                        ...]].
        ...

そして HandMorph>>#handleEvent:anEvent が呼ばれる、のではないか…

github.com

今日たどり着いたのはここまで!

(でもキーマッピングの話はまだ遠いにょ…)

おしまい

これからビールを飲みながら勉強会です。