osiire’s blog

ふしぎなそふとやさん

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を挟んでおけばいいような気がする。