osiire’s blog

ふしぎなそふとやさん

イベントドリブンな状態遷移をFRPで記述してみる

例題の仕様は次の通り。

  1. b1,b2というボタンがあり、これらはON,OFFがあるトグルボタン。
  2. modeというトグルボタンがある。
  3. modeがOFFの時、b1とb2は排他的な動きをする(ラジオボタンのような動き)。
  4. modeがONの時、b1とb2は独立なトグルボタンとして動作する。
  5. clearボタンがあり、これが押されるとb1,b2はOFFになる。

これを普通(?)のイベントハンドラ風に記述するとこんな感じ。

type on_off = ON | OFF
let flip = function ON -> OFF | OFF -> ON

(* 状態を保存する変数. 初期値は全部OFF *)
let mode = ref OFF
let b1 = ref OFF
let b2 = ref OFF

(* getter, setterの定義. ここにUIへの反映処理なども記述できる. *)
let mode_getter () = !mode
let mode_setter b = mode := b
let b1_getter () = !b1
let b1_setter b = b1 := b
let b2_getter () = !b2
let b2_setter b = b2 := b

(* modeボタンが押された場合。トグルのみ. *)
let on_mode_clicked () =
  mode_setter (flip (mode_getter ()))

(* 
 * トグルボタンが押された時の処理.
 * (getter, setter)はb1もしくはb2のgetter,setter. othersは第一引数のボタン以外のボタンのsetterのリスト.
 *)
let on_b_clicked (getter, setter) others () =
  (* やや複雑な場合分け. *)
  if getter () = ON then
    if mode_getter () = ON then
      setter OFF
    else
      ()
  else begin
    setter ON;
    if mode_getter () = ON then
      ()
    else
      List.iter (fun setter -> setter OFF) others
  end

(* b1クリック時のイベントハンドラ *)
let on_b1_clicked = on_b_clicked (b1_getter, b1_setter) [b2_setter]
(* b2クリック時のイベントハンドラ *)
let on_b2_clicked = on_b_clicked (b2_getter, b2_setter)  [b1_setter]

(* クリアボタンの機能. *)
let on_clear_clicked () =
  b1_setter OFF;
  b2_setter OFF

さて、これと同じ事をFRP(PEC)で記述してみる。

(* おまじない *)
module E = Pec.Event.Make (Pec.EventQueue.DefaultQueueM) (Pec.EventQueue.DefaultQueueI)
module S = Pec.Signal.Make (E)
open S.OP
open Pec.Event

(* イベントの準備 *)
let b1_click, send_b1_click = E.make ()
let b2_click, send_b2_click = E.make ()
let mode_click, send_mode_click = E.make ()
let clear_click, send_clear_click = E.make ()

(* modeはOFFが初期値でクリックされる度にON/OFFが切り替わる *)
let mode = S.fold (fun b () -> flip b) OFF mode_click

let when_on b e = E.filter (fun _ -> S.read b = ON) e
let when_off b e = E.filter (fun _ -> S.read b = OFF) e

(*
 * ON/OFF状態のシグナルを作成する関数.
 *)
let button_state self_clicked others_clicked =
  let state = S.return OFF in
  (* ONに遷移する条件 *)
  let to_on =
    when_off state self_clicked 
    +> E.map (fun _ -> ON)
  in
  (* OFFに遷移する条件 *)
  let to_off = 
    E.choose [
      when_on mode self_clicked;
      when_off mode others_clicked;
      clear_click
    ]
    +> when_on state
    +> E.map (fun _ -> OFF)
  in
  (* ON条件とOFF条件の重ね合わせがボタンの挙動. *)
  state <=< S.fold (fun _ b -> b) OFF (E.choose [to_on; to_off]);
  state

(* b1シグナル *)
let b1 = button_state b1_click (E.choose [b2_click])
(* b2シグナル *)
let b2 = button_state b2_click (E.choose [b1_click])

どちらも記述量は大して変わらない。FRPの方が宣言的で状態遷移図を直接的に実装している感じ。
b1, b2シグナルを使えば後からUIに対する見た目の効果も加えられるので、setter, getterにそれらを埋め込まなければならないスタイルより独立性が高そう。