GUIで学ぶ関数プログラミングで学ぶGUIアプリケーション

昼間なんか色々考えたような気がしたんだけど、どうでもよくなった。
ので、そういう仕事辞めたくなった話題(またか)とは全然関係無く、関数型言語GUIアプリケーションも美しく書けるのではないかというような話。


とにかく、Monadっていうのは、コマンドのことだと理解しておく。

main = putStrLn "nanika"

こういうのがあった場合、mainというのは"nanika"を表示するコマンドである、とする。端末に対して、「"nanika"を表示するコマンド」を送ると、端末は、"nanika"を表示する。


あと、前も書いたけど、

main = putStrLn "nanika" >> putStrLn "are"

コマンドは繋げてもコマンド。と、いうのが重要。


そこまで頭に入れておいて、まず、GUIアプリケーションというのは何なのかについて考えてみる。
とりあえず、絵を描くコマンドのことをGUIアプリケーションとしとこう。GUIアプリケーションっていうと、絵が出るわけだし。
HGL的に言うと、Graphic。

type GUIApplication = Graphic

そうなると、まず、線を引くアプリケーションは、こんな感じ

app::GUIApplication
app = line (3,3) (100,150)

なんてことだ。まったく美しい。これが、純粋で参照が透明な世界に生きるGUIアプリケーションなのである。


ウィンドウを作って、その上でこの非常に美しいGUIアプリケーションを動かしてみる。

module Main where

import Graphics.HGL.Window
import Graphics.HGL.Draw.Monad
import Graphics.HGL.Draw.Picture
import Graphics.HGL.Run

type GUIApplication = Graphic

app1 = (line (3,3) (100,150)) >> (ellipse (0,0) (200,200))
app2 = (line (100,0) (0,100)) >> (polygon [(20,20), (80,20), (80,80), (20,80)])

app::GUIApplication
app = app1 >> app2

-- winの上でappを実行する
runAppOnWin :: Window -> GUIApplication -> IO()
runAppOnWin win app = setGraphic win app

main::IO()
main = runGraphics $ do
         win <- openWindowEx "Hello" Nothing (400,400) DoubleBuffered Nothing
         runAppOnWin win app
         closeWindow win

いくつかの致命的な問題が。

  • 見えない


…ここで、「一個」は、「いくつか」に含まれるのかそうでないかという疑問があるんだけど、そういうのはいいとして、こんなものがアプリケーションと呼べるかどうかだ。どこに間違いがあるのだろうか。これはGUIアプリケーションとは呼べないような気がする。


何が違うんだ!っていうか、つまり、ユーザーからの入力を全く受け付けないというのが問題なのだけれど。
と、いうわけで、イベント入力について考える。が、しかし、ちょっとその前に、ここで、ひとつだけ、重要なことがある。それは、上のGUIApplicationは結合して、新しいGUIApplicationを作ることができる、という点だ。
「線を引いて、丸を描くアプリケーション」は、

app1 = (line (3,3) (100,150)) >> (ellipse (0,0) (200,200))

「線を引いて、四角を描くアプリケーション」は、

app2 = (line (100,0) (0,100)) >> (polygon [(20,20), (80,20), (80,80), (20,80)])

「線を引いて、丸を描いて、線を引いて、四角を描くアプリケーション」は、

app = app1 >> app2

純粋な世界でつくったアプリケーションは、他のアプリケーションとくっつけて新しい別のアプリケーションを矛盾無く作れるかもしれない。と、いうのを意味するかもしれない、し、しないかもしれない。


そういうのをとりあえず頭に入れておいて、イベント入力について考えよう。
サンプルプログラムとして「マウスクリックしたところに円を描く」アプリケーションを考える。名前は適当にdrawBallとでもしとこうか。


この場合アプリケーションとは何だろうか。とりあえず、昨日までの僕は次のように考えていた。「イベントの無限リストを画面描画コマンドに変換する関数」、つまり、イベントを入力として絵を出力とする関数

type GUIApplication = [Event] -> Graphic

この場合、コンピュータの前に座ってる人間はイベントジェネレータだ。ひたすら、無限に続くイベントを生成してくれる物体。
GUIアプリケーションというのは、そのイベントジェネレータが作ったイベント列を、画面の絵へと変換していく関数である、というわけだ。
まあ、なかなかに良い定義のような気がする。


ぱぱー、っと書いてみよう。

module Main where

import Graphics.HGL.Window
import Graphics.HGL.Draw.Monad
import Graphics.HGL.Draw.Picture
import Graphics.HGL.Run

type GUIApplication = [Event] -> Graphic -- GUIApplicationはイベント列を絵に変換する

drawBall :: GUIApplication -- drawBallはGUIアプリケーション
drawBall (x : xs) = 
    case x of
      -- マウスボタンが押されたらそこに丸を書いてイベントの残りをなんかする
      Button {pt=(x,y)} -> (ellipse (x-10,y-10) (x+10,y+10)) >> (drawBall xs)
      _ -> drawBall xs

drawBall [] = return () -- イベントが尽きたら終了
      
eventSeq::[Event] -- 二回ボタンを押す様子を想像しつつ、入力イベント列
eventSeq = [ Button{pt=(50,30),isLeft=True,isDown=True}, Button{pt=(18,300),isLeft=True,isDown=True} ]

-- winの上でイベントを引数として、appを実行する
runAppOnWin :: Window -> GUIApplication -> [Event] -> IO()
runAppOnWin win app e = setGraphic win (app e)

main::IO()
main = runGraphics $ do
         win <- openWindowEx "Hello" Nothing (400,400) DoubleBuffered Nothing
         runAppOnWin win drawBall eventSeq
         closeWindow win

相変わらず、画面は見えないけど、とりあえず、イベント列を絵に変換してるように見える。
さて、あとは、コンピュータの前に座っている人間をイベントジェネレータとみなして、そこからイベント列を生成すればいいだけ…


のはずだけど、ここでうまくいかない。やってみればわかるんだけど、行き詰まるはずだ。
いやー、ここで昨日は20秒ぐらい悩んだよ。とりあえず、20秒悩んだあと、諦めてエロゲを買いに行ったのだった。


のはまあいいとして、その理由は、今日考えてわかったよ。何故なら「イベントジェネレータは、アプリケーションを生成した絵を見るから」なのだ。


次の場合を考える。

  1. 僕と全く同じ姿形、思考を持った人間がもう一人いたとする。
  2. それは大変気持ち悪い
  3. のはいいとして、そいつと僕を同時に全く同じ環境のコンピュータの前に座らせる
  4. 僕の前ではアプリケーションAを、もうひとりの前では、アプリケーションBを起動させる
  5. アプリケーションAはボタンを押すと、でかでかと、「画面の下半分をクリックしてください」と表示される
  6. アプリケーションBはボタンを押すと、でかでかと、「画面の上半分をクリックしてください」と表示される

さて、僕が入力するイベントは?


もちろん、この質問の答えは、「両方とも同じようにひねくれてるので画面の右半分をクリックする」なのであるが、まあ、それはいいとして、ここで、二人の行動が分かれる可能性があることがわかるはずだ。
つまり、「アプリケーションを起動した時点では、どんな入力が来るかは決定していない」と、いうことであり、「アプリケーションの出力は、イベントジェネレータの状態を変化させる」と、いうことだ。GUIApplicationはただ単に、イベントを絵に変換するだけの関数ではない。


さて、では、GUIApplicationとはいったいなんなのか。とりあえず、次のように考えてみたらどうだろうか。「GUIApplicatinとは、イベントジェネレータに見せるプレゼンテーションである」と。
このプレゼンでは、次のことができる

  • イベントジェネレータに絵を見せる
  • イベントジェネレータから反応(Event)を貰う
type PresentationForEventGenerator = ...
type GUIApplication = PresentationForEventGenerator -- GUIアプリケーションとは、イベントジェネレータに見せるプレゼンのこと

dispGraphic g::Graphic -> PresentationForEventGenerator -- 絵を見せるのもプレゼン
getEvent :: PresentationForEventGenerator   -- 反応をもらうのもプレゼン

そして、こまごまとしたプレゼンは、くっつけて大きなプレゼンにできる、と

app::GUIApplication

app = do 
   dispGraphic nanika  -- 絵を見せる
   ev <- getEvent -- 反応をもらう
   case ev of 
     ... -> dispGraphic a  -- 反応によって見せる絵を変える
     ... -> dispGraphic b

なかなかそれっぽいような気がしないでもない、けど、モナドの書きかたがわからないので、明日に続く。