読者です 読者をやめる 読者になる 読者になる

osiire’s blog

ふしぎなそふとやさん

関数型言語はGUIが苦手?

副作用を極力排除しようとするfunctionalな方向性の言語においては、GUIのような副作用の塊は扱えないという直観を持っている人も多いことでしょう。確かにfunctionalな言語でunit型を返す関数ばかり扱っていると、"普通に手続きを書いているのと何が違うの?嬉しくない!"という結論になるのは至極当然の事と思います。
でも、副作用を持つ部分と純粋関数の部分を切り分けて考えれば、意外とGUIの世界でもfunctionalなスタイルでプログラムが書けるのです。わかりやすい所でいうと、例えば、データベースを扱うプログラムも副作用の塊のように見えますよね?でも、SQL文を作成する部分は文字列を扱う純粋関数ですし、取得したデータがnullじゃないか、有効かどうかを判断しながら処理する部分もpureです。そうやってどんどん切り分けていくと、最終的にかなりpureになります。しかもpureになるとテスト・検証がやりやすくなるという、こっちが目的なんじゃないかというくらい大きな"副作用"がついてきます。
どいう訳で、本題のGUIではどうやったら副作用を切り分けられるか、考えてみたいと思います。そこで、ボタンが押されたら表示されている数字が増える、みたいな簡単なプログラムを考察してみます。次のプログラムはVB.NETで書いたプログラムです。(コンパイルしてないので、詳細が間違っているかも。あとで直します。)

Dim counter as Integer = 0

Private Sub B1_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles B1.Click
   counter = counter + 1
   Num.Text = counter
End Sub

簡単ですね。これ以上どうしろというのでしょうか?でもこのプログラムはよく見ると、.NETライブラリも含めて、副作用が前提のプログラムになっています。というのも、B1_Click関数が値を返すようにはできておらず、「クリックされたら副作用でなんかする」という考え方に基づいているからです。実際B1_ClickハンドラはSubroutineであり、破壊手代入を二回も行っており、とてもfunctionalとは言えません。

では、どうすればいいのでしょうか?どこをどう変えたらfunctionalに近づけるのでしょうか?

まず変えるのは、「イベントが発生したらいきなりハンドラを呼ぶ」のではなく、その前に「イベントそのものをfunctionalにカスタマイズしていく」といいのです。そもそもB1_Clickというイベントは、数あるマウスイベントの中でクリックされたイベントだけ抽出したイベントです。数あるものから"抽出"?それってfilter関数ですよね?

Button.event
+> E.filter (fun e -> if e = MOUSE_PRESS _ then true else false)

クリックイベントは、functionalにこう書けるのです。こうすれば、イベントハンドラを呼ぶ事無く、イベントをカスタマイズできます。例えば「ボタンAとボタンBのどちらかが押されたイベント」はこう書けます。

 let a_or_b = 
   let click e = 
     E.filter (fun e -> if e = MOUSE_PRESS _ then true else false) e
   in
   E.merge (click ButtonA.event) (click ButtonB.event)

どうですか?functionalでしょう?でも、これだけでは足りません。イベントがfunctionalになっても"表示されている値をインクリメントする"事はできません。ここはどうするのでしょう?もう一度よく考えてみます。"0から始まる数字をイベントが挙がる度に前の状態から次の状態に変更する"のです。これって前の状態をイベントで畳み込んでいく訳ですから、foldですよね?はい、そうです。クリックされたらインクリメントされる値はfoldでこう書けます。

  S.fold (fun c _ -> c + 1) 0 click_event

これでイベントと値が繋がりました。最後にこの値を表示します。ここだけは副作用が必要です。以上をまとめて書くと、

Button.event
+> E.filter (fun e -> if e = MOUSE_PRESS _ then true else false)
+> S.fold (fun c _ -> c + 1) 0
+> Num.put_msg (* 副作用. 最後はunit型 *)

のようになり、関数型言語ではお馴染みの述語を使えるかなりfunctionalなプログラムだと言えるでしょう。
ちなみに、こういう仕組みに時間の概念も取り入れたテクニックの事はFRP(Functional Reactive Programming)と呼ばれており、既に幾つかのライブラリがありますし、F#にも採り入れられています。拙作のconcurrent cell(http://ccell.forge.ocamlcore.org/)でもサポートされています。