signalは使うときに工夫しないと使い物にならない
我ながら意味不明なタイトルだが気にしない。もう少しきちんとかくと、「FRPで言うところのsignalとかbehaviorとかは、そのままでは使いにくい。ちょっと工夫しないとすぐに困ったことになる」という事。
例えば単純化して次のようなSignalモジュールがあったとする。
module type Signal = sig type 'a t val make : 'a -> 'a t * ('a -> unit) val map : ('a -> 'b) -> 'a t -> 'b t val read : 'a t -> 'a end
これを使って親のGUIコンポーネントから子供のコンポーネントの大きさを定義するとする。
let child1 = { (* 子供コンポーネントのサイズを親の半分と定義 *) size = Signal.map (fun (x,y) -> x/2, y/2) parent.size; }
signalは依存関係を自動的に更新してくれるから、子供のsizeをいつreadしても親のsizeの半分になっている訳。signalはこういう定義に便利。
加えて、先ほどの子供コンポーネントの大きさに依存して、さらに別の子供コンポーネントの大きさを定義しよう。
let child2 = { (* 別の子供コンポーネントのサイズ *) size = Signal.map (fun (x,y) -> x, y/2) child1.size; }
これでchild1の大きさに依存してchild2の大きさも決まる。何の問題もない。
ところが、上記定義の後、やっぱりchild1のサイズを親のコンポーネントの大きさに依存させるのではなく、window全体のサイズに依存させるよう変更したいとなったら、どうすればいいか?
child1.sizeは既にparent.size依存のsignalである。もうこれはどうしようもない。変えられない。
じゃぁ、sizeレコードをmutableにしておいて破壊的代入したらどうか。
(* 子供コンポーネントのサイズをウィンドウの大きさに依存させる *) child1.size <- Signal.map (fun (x,y) -> x/2, y/2) window.size;
これはアウト。なぜならchild2.sizeは相変わらず前のchild1.sizeに依存したままであり嬉しくない。解決策はない。
ただし、Signalモジュールにjoin : 'a signal.t signal.t -> 'a signal.tがあれば解決する。子供コンポーネントのsizeを定義するときに、予めjoinを挟んでおくのだ。
let ss, sender = Signal.make () in let child1 = { size = Signal.join ss; } in sender (Signal.map (fun (x, y) -> x/2, y/2) parent.size);
上記のように定義しておくと、
sender (Signal.map (fun (x, y) -> x/2, y/2) window.size);
とすれば、child1.sizeはウィンドウの大きさに依存し直され、child1とchild2の依存関係も保たれる。
こう考えると、joinもしくはbindのないsignalは実用上使えない代物である。あと、全てのsignalには予めjoinを挟んでおけばいいような気がする。