Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

7GUIs Timer #96

Closed
stevekrouse opened this issue Dec 15, 2018 · 11 comments
Closed

7GUIs Timer #96

stevekrouse opened this issue Dec 15, 2018 · 11 comments

Comments

@stevekrouse
Copy link
Contributor

It seems like changes(time) or changes(f(time)) for any f doesn't work and fails silently. Is there a problem with changes() and continuous time?

I'm trying to do this problem and not sure how to do it without this. Should I use when()?

@paldepind
Copy link
Member

Is there a problem with changes() and continuous time?

Yes. changes can never work on continuous time because it changes infinitely often so it would result in a stream an infinitely dense stream of occurrences.

I'm trying to do this problem and not sure how to do it without this. Should I use when()?

Which part exactly is the problem? when could in theory work with time and I've actually had the need for that myself. But implementing it is a bit tricky and not done yet. One stopgap solution may be to have something like sampleEvery(100, time) to turn continuous time into a stream of a fixed resolution.

@stevekrouse
Copy link
Contributor Author

stevekrouse commented Dec 18, 2018

Yes. changes can never work on continuous time because it changes infinitely often so it would result in a stream an infinitely dense stream of occurrences.

Ok. Then shouldn't it error and not fail silently?

Which part exactly is the problem? when could in theory work with time and I've actually had the need for that myself. But implementing it is a bit tricky and not done yet. One stopgap solution may be to have something like sampleEvery(100, time) to turn continuous time into a stream of a fixed resolution.

Maybe we can stop with this XY Problem and you can just help me solve for X 😛 It looks behaves like this: https://andreasgruenh.github.io/7guis/#/timer

The key difficulty is that the timer stops when it's reached the end, but it can restart if you change the length of the timer. That is, the length of the timer is a Behavior (the scrubber below).

Start with the length of time at 5 seconds. Wait 9 seconds. The timer is stuck at 5 seconds. Then increase the timer to 7 seconds. It should now count up from 5 to 7 and then pause again. It shouldn't count the 4 seconds in between hitting the 5 max and you changing the max to 7.

It's a lot easier to get if you play with it via the live example link shared above.

@stevekrouse
Copy link
Contributor Author

stevekrouse commented Dec 23, 2018

Figured it out! The key insight was that whenever the scrubber changes the maxTime, I should "create a new timer" from the previous value of the timer. The key data structure is a Stream<Behavior<Float>>, which represents the creation of "new timers" on every occurrence of the outer stream. The inner stream is the new timer. This was quite tricky!

https://codesandbox.io/s/o9p4j4p759

const initialMaxTime = 10;
const initialStartTime = Date.now();
const timer = loop(
  ({ timeChange, maxTime, elapsedTime }) =>
    function*() {
      yield h1("Timer");
      yield span("0");
      yield progress({
        value: elapsedTime,
        max: maxTime
      });
      yield span(maxTime);
      yield div(
        elapsedTime.map(s => "Elapsed seconds: " + Math.round(s * 10) / 10)
      );
      const { input: timeChange_ } = yield input({
        type: "range",
        min: 0,
        max: 60,
        value: 10
      });
      const maxTime_ = yield liftNow(
        sample(stepper(initialMaxTime, timeChange.map(e => e.target.value)))
      );
      const newTimers = snapshotWith(
        (a, b) => [a].concat(b),
        lift((e, t) => [e, t], elapsedTime, time),
        timeChange_.map(e => e.target.value)
      ).map(([newMaxTime, elapsed, newStart]) =>
        time.map(currentT =>
          elapsed > newMaxTime
            ? elapsed
            : Math.min(elapsed + currentT / 1000 - newStart / 1000, newMaxTime)
        )
      );
      const elapsedTime_ = yield liftNow(
        sample(
          switcher(
            time.map(t =>
              Math.min((t - initialStartTime) / 1000, initialMaxTime)
            ),
            newTimers
          )
        )
      );
      return {
        timeChange: timeChange_,
        maxTime: maxTime_,
        elapsedTime: elapsedTime_
      };
    }
);

So while I don't need changes(time), like #91, I'd like it to error if we know it will never return anything useful.

@paldepind
Copy link
Member

Hi @stevekrouse. That solution looks nice. I think we need to add something to Hareactive to make this part less awkward:

const newTimers = snapshotWith(
  (a, b) => [a].concat(b),
  lift((e, t) => [e, t], elapsedTime, time),
  timeChange_.map(e => e.target.value)
).map(([newMaxTime, elapsed, newStart]) =>
   time.map(currentT =>
    elapsed > newMaxTime
      ? elapsed
      : Math.min(elapsed + currentT / 1000 - newStart / 1000, newMaxTime)
  )
);

In Hareactive PureScript that can be done a lot easier. We'll need an easier way to snapshot several behaviors at the same time.

@stevekrouse
Copy link
Contributor Author

Thanks! Could you show me what this would look like in Hareactive Purescript? (Or is the short answer that it would look like it would in Haskell?)

Also, I don't want us to loose track of the fact that changes(time) fails silently. Throwing an error would be fine, but sampling every X time would work too like discussed in #91

@paldepind
Copy link
Member

Thanks! Could you show me what this would look like in Hareactive Purescript? (Or is the short answer that it would look like it would in Haskell?)

If f3 is a function from three arguments it can be applied to two behaviors and a stream like this:

f3 <$> b1 <*> b2 <~> s

The <~> operator is an alias for applyS. It is documented here.

For TypeScript/JavaScript I was thinking something likes this:

liftFoo(f3, [b1, b2], s);

That is, liftFoo (working title 😉) takes a function, a list of behaviors and a stream and lifts the function over them.

Then this code

const newTimers = snapshotWith(
  (a, b) => [a].concat(b),
  lift((e, t) => [e, t], elapsedTime, time),
  timeChange_.map(e => e.target.value)
).map(([newMaxTime, elapsed, newStart]) =>
   time.map(currentT =>
    elapsed > newMaxTime
      ? elapsed
      : Math.min(elapsed + currentT / 1000 - newStart / 1000, newMaxTime)
  )
);

would become

const newTimers = liftFoo(
  (elapsed, newStart, newMaxTime) =>
    time.map(currentT =>
      elapsed > newMaxTime
        ? elapsed
        : Math.min(elapsed + currentT / 1000 - newStart / 1000, newMaxTime)
    ),
  [elapsedTime, time],
  timeChange_.map(e => e.target.value)
);

which I think is a lot easier to read.

@stevekrouse
Copy link
Contributor Author

It's so easy and natural to apply a map to a stream, that I prefer it just giving me all the data and allowing me to do that myself. In other words the current snapshot removes too much data, and snapshotWith makes me apply a function when I just want to retain the data. Ditto for liftFoo

Here's my proposal: instead of snapshot or snapshotWith, I want a function snapshot<A,B,C,D>([b1: Behavior<A>, b2: Behavior<B>, b3: Behavior<C>], s: Stream<D>): Stream<[A, B, C, D]> (but you can also pass it just one Behavior instead of a list and it'll work, and you can also pass it a list of more than 3 Behaviors and it'll also work).

Which would produce:

const newTimers = snapshot(
  [elapsedTime, time],
  timeChange_.map(e => e.target.value)
).map(([newMaxTime, elapsed, newStart]) =>
   time.map(currentT =>
    elapsed > newMaxTime
      ? elapsed
      : Math.min(elapsed + currentT / 1000 - newStart / 1000, newMaxTime)
  )
);

@paldepind
Copy link
Member

paldepind commented Jan 13, 2019

@stevekrouse I've been thinking a bit about the 7GUI timer a bit. I've created an implementation that I think is very elegant. It features a "Reset" button as well. The logic is pretty much only 2 lines of code and two very simple reusable functions.

The secret sauce is H.integrate. The implementation relies heavily on recursively dependent behaviors and I had to fix a bunch of bugs in Hareactive before it worked 😅

For some reason that I do not understand I couldn't get the code working on Codesandbox, but it works flawlessly on my machine. Edit: I made a silly mistake, @limemloh has created a working sandbox here: https://codesandbox.io/s/48xxz5m889

Here is a picture and the complete code.

image

import * as H from "@funkia/hareactive";
import { lift } from "@funkia/jabz";
import { runComponent, modelView, elements as e } from "@funkia/turbine";

const initialMaxTime = 10;

function resetOn(b, reset) {
  return b.chain(bi => H.switcher(bi, H.snapshot(b, reset)));
}

function momentNow(f) {
  return H.sample(H.moment(f));
}

const timer = modelView(
  input =>
    momentNow(at => {
      const change = lift((max, cur) => (cur < max ? 1 : 0), input.maxTime, input.elapsed);
      const elapsed = at(resetOn(H.integrate(change), input.resetTimer));
      return { maxTime: input.maxTime, elapsed };
    }),
  input =>
    e
      .div([
        e.h1("Timer"),
        e.span(0),
        e.progress({ value: input.elapsed, max: input.maxTime }),
        e.span(input.maxTime),
        e.div(["Elapsed seconds: ", input.elapsed.map(Math.round)]),
        e
          .input({ type: "range", min: 0, max: 60, value: initialMaxTime })
          .output({ maxTime: "value" }),
        e.div({}, e.button("Reset").output({ resetTimer: "click" }))
      ])
      .output(o => ({ elapsed: input.elapsed }))
);

runComponent("#mount", timer());

@stevekrouse
Copy link
Contributor Author

Beautiful! resetOn makes a ton of sense.

I am a bit lost with momentNow. I've never seen H.moment before, and my understanding of sample is shakey enough already.

Integrating over change makes sense in theory but I am surprised that it works in javascript!

@stevekrouse
Copy link
Contributor Author

I found the moment explanation! #51 (comment)

@stevekrouse stevekrouse changed the title changes(time) 7GUIs Timer Jan 29, 2019
@stevekrouse
Copy link
Contributor Author

I changed the name of this issue to reflect that it's just about the 7GUIs Timer. The other part of this issue, that changes(time) fails silently, I will move to #91.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants