3
votes

For fun and to learn I'm trying to implement an undo system in my app using functional reactive programming. I have a stream of state changes, which need to be saved onto the undo stack. When the user clicks undo, I take a value from the stack and update application state accordingly.

The problem is that this update itself also generates an event in the state change stream. So what I would like is to derive another stream from state changes, which ommits state change right after undo.

A simple diagram:

states   ----S----S----S----
undos    -------U-----------     
save     ----S---------S----

The first line is the stream of application state changes, the second line are undo events triggered by the user and the third line is the stream I'd like to implement and listen to instead of the first stream.

What is the best way to express such intent in FRP?

2
but your state would look different . If you add indexes to your updates, you will notice that S1-U1-S0-S2 vs S1-S2 -> i.e. when you are doing your Undo_1 (U1), your state would efectively change to State_0 (not shown in your stream #3), and thus you MUST either have that state change in that place, OR show State0, and remove State1 from stream #3. // hope you understand what i mean.c69
Not sure what you mean. Please, don't dwell too much on the logic of the undo functionality, I was giving it mostly as context in which I stumbled upon a problem I wasn't able to solve using streams. Now I'm interested not so much in the undo aspect, but in general solution to this particular stream scenario.VoY
if you want to merge two streams, think of merging two arrays - i.e.: [{a:1}, {a:2}, {x: !0}, {a:3}, {a:4}, {a:5}].reduce(function(a,v){ if (v.x) {a.pop()} else a.push(v) ; return a; }, []). --> yourMergedStream.reduce(function removePreviousUndo(a,v) {/*...*/}) see: baconjs.github.io/…c69

2 Answers

6
votes

In RxJS:

var state = Rx.Observable.interval(2000)
  .map(s => ({type:'s', value: s}))
  .take(3);
var undo = Rx.Observable.interval(3000)
  .map(u => ({type:'u', value: u}; }))
  .take(1);

var save = state.merge(undo).scan((prev, curr) =>
  if (prev.type === 'u') {
    return {type: 'removed'};
  } else {
    return curr;
  }
}).filter(x => x.type !== 'removed' && x.type !== 'u');

See this JSBin. The trick is merge is a vertical combinator (vertical with regards to the streams diagram), and scan is a horizontal combinator. Using both, first merge, then scan, you are able to build more powerful combinators like the one that solves your problem. The solution is similar in Bacon and Kefir.

3
votes

With Bacon.js, I would use the Bacon.update function to maintain a property with the whole undo stack. This way, we can support multiple successive undo steps.

var state = Bacon.sequentially(1000, [1,2,3])
var undo = Bacon.later(2500, "undo")

var stateStack = Bacon.update(
[],
state, function(prev, s) { return prev.concat(s) },
undo, function(prev, u) { return prev.slice(0, prev.length - 1) }
)

var currentState = stateStack.changes().map(function(stack) {
  return stack[stack.length-1]
})

stateStack.log()
currentState.log()

http://jsbin.com/boharo/2/edit?js,console