How to properly initialize dynamically generated React signals?

I could use some help with how to wire signals in the framework of the React FRP library.

Here is my boiled-down example. A running table of labels with associated counts is constructed; labels are added dynamically; the counts are themselves signals. One ends up with the dynamic table running_table : (label * count signal) list signal. Here, the outer signal is for tracking the builder events. The inner signals track the changer events. (I added a printing function for convenience):

open React

(* utility *)
let unequal _ _ = false
let string_empty s = String.length s = 0

type label = string
type count = int
let (builder: label event), build = E.create ()
let (changer: (label * count) event), change = E.create ()

(* A running table of changeable signals constructed from the labels seen
   so far. The labels act as addresses; changes emitted by the changer
   event will be tracked by the signal with matching address. *)
let running_table =
  let apply_change s i (s', j) = if String.equal s' s then j else i in
  let change_tracker s = S.Special.Si.fold (apply_change s) 0 changer in
  let add_tracker s l = (s, change_tracker s) :: l in
  S.accum ~eq:unequal (E.map add_tracker builder) []

let pp_running_table fmt () =
  let t = S.value running_table in
  let string_rep = List.fold_left (fun ss (s, c) ->
      ss ^ (if string_empty ss then "" else ", ")
      ^ s ^ " " ^ string_of_int (S.value c)) "" t in
  Format.fprintf fmt "%s" string_rep

Up to here this works just fine; try it with

build "apples";
build "oranges";
Format.printf "table:@;%a@." pp_running_table ();

if you like.

Now I also want a running aggregate result. For the purpose of this example, let’s say a table that sums up all the counts for labels beginning with the same letter. I tried to construct running_aggregate_result: (char * count) list signal as follows:


let running_aggregate_result =
  let classify s =
    if string_empty s then None else Some (String.get s 0) in
  let compose f f' = fun a -> f' (f a) in
  let update_tally s i_new i_old aggregate =
    match classify s with
    | None -> aggregate
    | Some cat ->
      match List.assoc_opt cat aggregate with
      | None -> (cat, i_new) :: aggregate
      | Some j ->
        (cat, j + i_new - i_old) :: List.remove_assoc cat aggregate in
  let running_updaters = S.map ~eq:unequal
      (fun l ->
         let updaters = List.map (fun (s, count_signal) ->
             S.diff (update_tally s) count_signal) l in
         E.merge compose Fun.id updaters)
      running_table in
  let transformer =
    (* S.sample (fun _e s -> s) builder running_updaters *) (* alternative *)
    S.changes running_updaters
    |> E.switch E.never in           (* initial signal value lost here? *)
  S.accum transformer []

let pp_running_aggregate fmt () =
  let a = S.value running_aggregate_result in
  let string_rep = List.fold_left (fun ss (cat, c) ->
      ss ^ (if string_empty ss then "" else ", ")
      ^ String.make 1 cat ^ " " ^ string_of_int c)
      "" a in
  Format.fprintf fmt "%s" string_rep

When I then test this with the code below, I find that running_table gets updated whenever a new label is added by build, but running_aggregate remains empty. Only after change is called, running_aggregate is initialized.

let _main =
  build "apples";
  build "oranges";
  Format.printf "table:@;%a@." pp_running_table ();
  Format.printf "aggregate:@;%a@." pp_running_aggregate ();
  Format.printf "now adding an apple.@.";
  change ("apples", 1);
  Format.printf "table:@;%a@." pp_running_table ();
  Format.printf "aggregate:@;%a@." pp_running_aggregate ();
  Format.printf "now adding three almonds.@.";
  build "almonds";
  change ("almonds", 3);
  Format.printf "table:@;%a@." pp_running_table ();
  Format.printf "aggregate:@;%a@." pp_running_aggregate ();

What do I need to do differently? The construction of running_aggregate feels clumsy, and my guess is that in the line with E.switch all builder events before the first changer event get lost…

Thanks for any pointers!

It seems the problem starts already earlier. In the definition of updaters, S.diff ... count_signal is used, and that cannot emit an event at creation of count_signal. So one needs an extra event for initialization. The new version below works as intended afaics:

let running_aggregate_result =
  let classify s =
    if string_empty s then None else Some (String.get s 0) in
  let compose f f' = fun a -> f' (f a) in
  let update_tally s i_new i_old aggregate =
    match classify s with
    | None -> aggregate
    | Some cat ->
      match List.assoc_opt cat aggregate with
      | None -> (cat, i_new) :: aggregate
      | Some j ->
        (cat, j + i_new - i_old) :: List.remove_assoc cat aggregate in
  let running_updaters = S.map ~eq:unequal
      (fun l ->
         let updaters = List.map (fun (s, count_tracker) ->
             S.diff (update_tally s) count_tracker) l in
         let initializer_ = E.map (fun s -> update_tally s 0 0) builder in
         E.merge compose Fun.id (initializer_ :: updaters))
      running_table in
  let transformer =
     S.sample (fun _e s -> s) builder running_updaters
    (*S.changes running_updaters*)
    |> E.switch E.never in           (* initial signal value lost here? *)
  S.accum transformer []

Still not pretty IMO but ok!

As I know what this is for, I won’t give any concrete solutions - but overall FRP is able to solve the problem at hand much simpler - and be within the FRP graph for defining the whole program.

Though you need an extended FRP semantics, which someone just might have implemented for you. And sorry for the vague answer, but you understand (:

Hi @rand you appear to think I’m asking for some homework/contest solution – but this is actually an example I made up myself based on my real-world use case. Anyway, I would be happy to learn about a better solution once the deadline for what you were thinking is over.

In my real example, there actually is a long-running simulation loop. Translating to the simplified example, in each iteration of the loop, the running_aggregate_result is read and based on that value, it is decided which change signal to emit next; this will again update the aggregate result through the FRP graph.
To me, it seemed unwieldy to try and keep the entire simulation loop inside FRP; the fact that I would need to implement a sort of delay to the next iteration in particular, was not obvious to me. Instead I tried to leave a small “gap” and drive the main simulation loop manually.

Weird coincidence. I don’t know when the assignment won’t be used anymore.

I would suggest that you think hard if you really need nested signals. It can be useful in some situations, if you really want a dynamic FRP-graph, but most of the time it’s better to avoid nesting if possible:

  • It brings a lot of unneccesary complexity
  • bind/switch is inefficient - parts of your graph is GC’d on each change
  • bind/switch should be used only when your applications semantics demands that a part of the graph is dynamic

My guess, without reading all the details of your code, is that you could rewrite your type to:

type data_table : (label * count) list 

… and then update the list with E.fold over some input-events, where you then S.hold [] to get your final signal. I find the usage of E.fold simpler relative to S.fix, which is another possibility.

If you wrap your FRP-graph in S.fix you can read the previous values of the output, and coupled together with a tick event, you have your self-running simulation. Personally I use a small Lwt loop to drive my ticks (you’ll need Lwt_react to support this).

Last thing: I don’t recommend you involve your types with FRP types - they can often be kept separate, and it will e.g. be easier to test and reuse your code later this way.

1 Like

Unrelated to the problem sorry ya’ll. Just interested in what class (or type of class) would have this assignment? Some of this stuff feels like black magic to me outside of academia! :cry:

Or type class? (;

I didn’t mention it so it won’t be searchable for people looking for solutions.

Thanks for the insights about nested signals.

I did notice the added complexity;) I actually specifically wanted to implement dynamically added signals, because that’s what’s needed at least in some cases for the real-world application. In an alternative implementation using imperative updates of mutable data, dynamically added data and updates would also be somewhat cumbersome and I wanted to see if FRP can offer an elegant solution. I wasn’t aware that

Is this the case even when the outer “reconfiguration” signals fire only rarely, so that the vast majority of events is in the inner “update” events (changer in my example)? If yes that’s a problem.

Yes, when forgetting about the dynamic reconfiguration this should work and would be quite a bit simpler. However, here my example falls a bit short. In the real application, running_table is big and only a few entries change on each iteration; similarly aggregate_result is big and only a few of its elements would be affected in each iteration. So I thought it may be advantageous to avoid updating the full tables in each step and instead have a way to update only the sparse entries that change. This imagined efficiency gain was the motivation for the nested signals in running_table. If the whole thing is slowed down by bind anyway, that’s misguided.

If it doesn’t conflict with the assignment mentioned, could you elaborate a bit on this? I fail to see how the logic would go. I can imagine one can make a signal previous_aggregate_result using S.fix; then the current aggregate_result would have to be some kind of S.map of this but how exactly? Maybe you could give a minimal example for a loop in FRP without Lwt? Would you expect gains in efficiency doing it this way or “just” cleaner code?

No, you only notice it if you bind at a high rate

Yeah, this can be a tactic - but an alternative is to have a datastructure that is efficient to update. This is the simplest solution - and simple often best.

Some projects use “reactive data” though - there is a ReactiveData library for React and I remember Janestreets Incremental also has something like this. You can also use ReactiveData to bind to the reactive Tyxml_js interface.

No, it’s not about efficiency. One of the several advantages of FRP is that you often can write simple and elegant code. Another is that FRP gives you certain guarantees - so the more you can write your program within pure FRP (using pure FP), the better the guarantees.

You could e.g. write prev_agg_result_s |> S.sample simulate change_e inside S.fix (see React documentation for how to use S.fix). change_e would be a tick event or/and an input-event. I often merge events together:

E.select [
  e1 |> E.map (fun v -> `E1 v);
  e2 |> E.map (fun v -> `E2 v);
]

Beware the difference between E.select and E.merge


Edit:
I forgot you needed to fold over your events, so in this case my example becomes:

S.sample Tuple.mk2 change_e prev_agg_result_s
|> E.fold simulate init
1 Like