turion / rhine

Haskell Functional Reactive Programming framework with type-level clocks
http://hackage.haskell.org/package/rhine
117 stars 21 forks source link

Simplify `initClock`? #304

Open turion opened 3 months ago

turion commented 3 months ago

Since long, I wanted to simplify the type signature of initClock from

  initClock ::
    cl ->
    m (MSF m () (Time cl, Tag cl), Time cl)

to

  initClock ::
    cl -> MSF m () (Time cl, Tag cl)

There are several reasons for the more complicated type signature:

But there are also downsides to this complicated type signature:

With #299 (automata) there would be a new downside: The state of the running clock is typically not known statically (because it is hidden behind a monadic action), therefore GHC cannot optimize the whole Rhine further after clock erasure. This causes a performance degradation that is not easily justifiable.

Overall I believe that after #299, a serious attempt at simplification should be made.

turion commented 1 month ago

The extra m action can be used to initialise a resource.

That's actually the wrong approach: The clock value should already contain the initialised resource! So if anything, there should be an action m cl that initialises the resource, and then the clock value can be used. Although that will still have the same problem: The Rhine is then not statically known.

turion commented 1 month ago

There is a big semantic change that I'm unsure about. On master the first tick is always a certain time after the initial time. Also, the duration sinceLast on the first tick is the time from the initial time the first tick. With this change here, we would have the awkward situation that sinceLast on the first tick is always 0! This is especially awkward for a fixed rate clock where we expect sinceLast to be largely constant. Instead, it is now 0 and then afterwards largely constant.

I'm not sure how to proceed then. I see several different stances:

  1. There should be some initialisation step that measures the initial time, and there shouldn't be any data processing on that step yet. This could be implemented in the original way, but there are a few other ways:
    class Clock m cl where
    initialTime :: cl -> m (Time cl)
    initClock :: cl -> Automaton m () (Time cl, Tag cl)

    or even

    class Clock m cl where
    initialTime :: cl -> m (Time cl)
    initClock :: Automaton m cl (Time cl, Tag cl)

    In latter case, the running clock is completely known statically, which is great.

  2. On the first tick, sinceInit and sinceLast have no meaning and could have any values, e.g. 0 or 'Nothing'. This may be right, but is unhelpful in many cases and breaks with the established semantics. On the other hand, for CSV clocks (#226) and similar batch processing scenarios, there is no sensible start time other than a manually supplied one or the first timestamp.
turion commented 3 weeks ago

In latter case, the running clock is completely known statically, which is great.

But this also means that many clocks like RescaledClock or SelectClock cannot be implemented anymore.