イベント再考

なんか、ちょっとビビったりした。


というわけで、アプリケーションを生成する関数を書けばいいよ、って話だった。
それはとりあえずひと区切りとして、イベントについて掘り下げる。タイムアウト付きイベントとからめて抽象化の限界の話。


次のようなアプリケーションを考える→「5秒以内に3回クリックしたら赤い円を表示。できなかったら何も表示しない」

こういうタイムアウト付きの処理は、微妙に難しい問題ではないのかと思う。
とりあえずぱっと思い付く解決策としては、WindowsのSetTimerみたいな感じで、時間がきたら、タイマーイベントが飛んでくる感じだろうか

clickCountWithTimeout :: Int -> Application
clickCountWithTimeout n = do
    ev <- getEvent 
    case ev of
       Click -> clickCountWithTimeout (n - 1)
       Timer -> return () -- 時間に間にあわなかったら何もしない

clickCountWithTimeout 0 = do -- n回クリックしたら表示
    draw

app = do
  setTimer 5
  clickCountWithTimeout 3

こんな感じにすればできる。
まあ、いいんだけど、これだとタイマーが変数っぽい動きをしてるので気持ち悪い。これを、「5秒以内に2回クリックしたら、3秒停止して、そのあと残り時間内にもう一回クリックできたらいい」みたいなのに変更しようとしたら、その瞬間にどろどろとした世界へ行って返ってこれなくなってしまうだろう。


はたして、こういうのは綺麗に書けるのだろうか?
色々と考えた結果、イベント取得を次のようにするのがよいのではないのかという結論に至った

getEventWithTimeout :: Int -> (Int->Event->Application) -> Application

以下、説明。


まず、コンピュータの中(あっち側)とコンピュータの外(こっち側)の世界の通信について考える。どうやってこっちの世界の状態をコンピュータに送っているのか。


こっち側の世界の状態をコンピュータの中に送るときには、ひとつの問題が起こる。こっち側の世界の状態というのは、莫大すぎる、ということだ。コンピュータ以外の全ての情報をコンピュータの中に入れないといけない。そんなことは不可能だ。


これをなんとかするために、コンピュータは、こっちの状態を一定時間ごとにサンプリングするのである。んで、一回前のこっち側の状態と、今のこっち側の状態の差分だけを扱うようになっている。ここで、この差分というのが、すなわち、ユーザー入力であり、イベントなのである。

# こんな感じ
イベント = こっち側(t-Δt) - こっち側(t)

イベントを貰ってくる、というのは、つまり、コンピュータの外側の差分をもらってくる、ということなのだ。

getDiff :: Time -> IO Maybe Event -- Time時間待って、そのあいだに発生した差分をもらってくる。変化しなかった場合、差分は無い

app = do
   ev <- getDiff (1[sec])  -- 1秒間待って、その時間で変化した差分をもらってくる
   case ev of
     Nothing -> ... -- 1秒間で何も変化しなかった 
     Just Button - > ... -- 1秒間でマウスのボタンの状態が変化した

ここで、重要なことは、差分は発生しないかもしれないということ。
タイムアウト処理とはすなわち、差分が発生しなかった場合の処理だと言えるのではないだろうか。


これを考えると、「3秒以内にクリックされたら、Helloって表示」というのは次のような感じになる。

-- n秒間クリックされるのを待つ
waitClick n = do
   ev <- getDiff (1[sec]) -- 1秒間待って、発生した差分を取得
   case ev of 
     Nothing    -> waitClick (n-(1[sec])) -- 1秒間何もなかった
     Just Click -> putStrLn "hello"       -- クリックされた

waitClick 0 =  return ()

app = waitClick (3[sec])

これで、副作用無し(あったとしてもgetDiff内だけ)でタイムアウト付き処理が実現できる。


ただしこのままでは問題がある。差分を拾ってくるのが1秒間隔なので、1秒以内に二回以上クリックされたら正しい動きをしないということだ。でも、これの対処法は簡単。差分を拾う間隔を小さくしていけばいいのだ。

delta -> 0 -- delta は 0 に近い値

-- n秒間クリックされるのを待つ
waitClick n = do
   ev <- getDiff delta -- delta 待って発生した差分を取得
   case ev of 
     Nothing waitClick (n-delta) -- delta 間何もなかった
     Just Click -> putStrLn "hello" -- クリックされた

waitClick 0 =  return ()

app = waitClick (3[sec])

これでよい。正しく、副作用も無く美しく解決することができた


……本当か?
この解決方法には次の問題がある。

  • 入力デバイスの解像度に限界があるので delta は一定以上0に近付かない
  • 計算には時間が必要なので、delta は一定以上0に近付かない
  • ジーループでぶんまわすことになるので、他のプロセスに優しくない


当たり前の問題なのだが、この問題がこれまでの問題と違うのは、「コンピュータは計算にコストがかかる」等の、「ハードウェアの限界」から来てる、ということだ。
どうすればいいだろうか。


いや、こんな問題はどうしようもないよ。ハードウェア屋の問題だね。富豪的プログラミングの時代だし。ハードウェアに優しいプログラムよりも正しく、わかりやすく簡単なプログラムを書くようにしましょう。
以上、上の問題はハードの問題でソフトウェア屋にはどうしようもない、という話でした。操作がもっさりになってもそんなのソフト屋の責任じゃない!