GUI のイベントスクリプティング

http://subtech.g.hatena.ne.jp/secondlife/20070813/1186999047 より

まずイベントドリブンなプログラミングに慣れてないのが一つで。Flex のイベントや自前イベントやをただ単に投げまくってると、とりあえずは動くけど後からメンテし辛いスパゲッティコードができあがる。このスパゲッティコードは goto 文が乱立するコードよりも酷く、goto だったら割と行き先は把握できるけど、イベントを投げまくってるだけだと、どこでどのオブジェクトがこのイベントを受け取るかが解らない。解りづらい。いちいちソースコード grep ですね、おめでたいですね。あのイベントが発生してから、そのイベントが終了したら発生するイベントが終了したらウィンドウ閉じて、その間は別のイベントはブロックして/発生しないようにして、とかもうわけわかんない。これも GUI プログラミングをしたこと無いからのような気もしなくもないけど。

たしかに、そこらへんが GUI プログラミングは難しい。
イベントハンドラの中でネットワーク待ちとか時間がかかる処理を書くと、UI 全体が固まってしまうから、処理を細切れにして非同期で呼び出してつなげていかないといけないところとか。
ここらへんは、C# + WindowsForms でも、Mac OS X + Cocoa でも、Flex でも一緒。

A,B,C のデータ三つのロードが終わったら処理をしたい場合のパターンとか。もちろん安易に直列でやれば楽なんだろうけど、直列も並列もどちらでも同じように書いて動かしたいときや、統一された書き方で書きたいときのパターンが解らない。

自分もそういうことやりたいときあるなぁと思ったので、ruby で作ってみた。

http://limechat.net/sample/eventscript.rb

自前でイベントループ回してるので、イベントループの部分を捨てて ActionScript で書きなおせば Flex でも使えるパターンになってると思う。

直列に実行したい場合には、

scenario = sequence(self) << [
  get('http://www.google.com/'),
  get('http://www.yahoo.com/'),
  get('http://www.apple.com/'),
  get('http://www.mozilla.org/'),
]
scenario.start

という感じで、並行実行したいなら、

scenario = concurrent(self) << [
  get('http://www.google.com/'),
  get('http://www.yahoo.com/'),
  get('http://www.apple.com/'),
  get('http://www.mozilla.org/'),
]
scenario.start

こんな感じに書ける。

実行結果を見ると、

scenario start
  url get start: http://www.google.com/
  url get success: http://www.google.com/
  url get start: http://www.yahoo.com/
  url get success: http://www.yahoo.com/
  url get start: http://www.apple.com/
  url get success: http://www.apple.com/
  url get start: http://www.mozilla.org/
  url get success: http://www.mozilla.org/
scenario end
concurrent scenario start
  url get start: http://www.google.com/
  url get start: http://www.yahoo.com/
  url get start: http://www.apple.com/
  url get start: http://www.mozilla.org/
  url get success: http://www.google.com/
  url get success: http://www.apple.com/
  url get success: http://www.yahoo.com/
  url get success: http://www.mozilla.org/
scenario end

concurrent のほうは並行実行できてることがわかる。

シナリオを入れ子にすることもできる。

scenario = sequence(self) << [
  lambda { puts '* start' },
  concurrent << [
    get('http://www.google.com/'),
    get('http://www.yahoo.com/'),
    get('http://www.apple.com/'),
    get('http://www.mozilla.org/'),
  ],
  sequence << [
    get('http://www.cnn.com/'),
    get('http://del.icio.us/'),
    lambda { puts '* waiting for 2 seconds' },
    wait(2),
    concurrent << [
      get('http://tumblr.com/'),
      get('http://flickr.com/'),
    ],
  ],
  lambda { puts '* end' },
]
scenario.start

実行結果は、以下のような感じ。(見やすいように整形済み)

scenario start
  * start
  concurrent scenario start
    url get start: http://www.google.com/
    url get start: http://www.yahoo.com/
    url get start: http://www.apple.com/
    url get start: http://www.mozilla.org/
    url get success: http://www.google.com/
    url get success: http://www.apple.com/
    url get success: http://www.yahoo.com/
    url get success: http://www.mozilla.org/
  scenario end
  scenario start
    url get start: http://www.cnn.com/
    url get success: http://www.cnn.com/
    url get start: http://del.icio.us/
    url get success: http://del.icio.us/
    * waiting for 2 seconds
    concurrent scenario start
      url get start: http://tumblr.com/
      url get start: http://flickr.com/
      url get success: http://tumblr.com/
      url get success: http://flickr.com/
    scenario end
  scenario end
  * end
scenario end

全部非同期で、内部ではスレッドを起こして実行してるので、

scenario = sequence(self) << [
  lambda { sleep 100000 },
]
scenario.start

とか書いても、イベントループは回ったままなので、UI は固まったりしない。

あと、直列で実行するときには、前の処理の結果を使いたい場合が多いと思うので、

scenario = sequence(self) << [
  lambda { 2 + 3 },
  lambda {|v| puts v },
  get('http://tumblr.com/'),
  lambda {|v| puts v },
]
scenario.start

みたいに書けるようにしておいた。

scenario start
  5
  url get start: http://tumblr.com/
  url get success: http://tumblr.com/
  <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
  <html><head>
  ...
scenario end

(追記: 8.18)

flashrod さんが AS3 版を作られた。
http://d.hatena.ne.jp/flashrod/20070815#1187191083