ajnsit / concur-documentation

Documentation for Concur
https://ajnsit.github.io/concur-documentation/
66 stars 10 forks source link

"Merging" signals? #15

Closed drewolson closed 4 years ago

drewolson commented 4 years ago

Given two signals of the same type, is there a way to merge them into another signal that emits events from both?

I'm attempting to reimplement https://gist.github.com/drewolson/607c7e742c471400e0e61c5068e0728d using signals, but I'm not sure what the equivalent of this line is with signals:

action <- counterButtons int <|> incrementTicker

Signal doesn't seem to have an instance of Alt, so I'm not sure what I should do here.

drewolson commented 4 years ago

I ended up writing a helper function called race that, I think, does what I want. It takes a starting value and a Traversable of Signals. It returns the value from any of the signals that differs from the starting value. Let me know if this seems crazy.

race
  :: forall a v f
   . Eq a
  => Monoid v
  => Traversable f
  => a
  -> f (Signal v a)
  -> Signal v a
race val signals = do
  values <- sequence signals
  let match = find ((/=) val) values

  pure $ fromMaybe val match
ajnsit commented 4 years ago

I'm not sure how race helps here. Signals are combined using monadic bind, and you always have access to all the output vals from signals. There's no point in only returning the first value that is different from the starting value.

I reimplemented your gist using Signals. Hope that helps clarify things -

mainWidget :: forall a. Widget HTML a
mainWidget = do
  dyn $ loopS 0 \n -> do
    display $ D.text (show n)
    n' <- incrementTicker n
    counterSignal n'

-- Counter
counterSignal :: Int -> Signal HTML Int
counterSignal init = loopW init $ \n -> D.div'
  [ n+1 <$ D.button [P.onClick] [D.text "+"]
  , D.div' [D.text (show n)]
  , n-1 <$ D.button [P.onClick] [D.text "-"]
  ]

-- Timer
incrementTicker :: Int -> Signal HTML Int
incrementTicker init = loopW init $ \n -> do
  liftAff $ delay $ Milliseconds 1000.0
  pure (n+1)
ajnsit commented 4 years ago

Some notes -

  1. A Signal is basically a Widget + looping. So loopW, which provides stateful looping, is the easiest way to generate individual signals.

  2. Signals are "continuous", i.e. they always have a current value. So the notion of "merging" signals does not arise except as a zipping operation, where you provide a continuous function which combines individual values from the constituent signals. This "zipping" is possible using monadic bind like so -

do
  a <- signal1
  b <- signal2
  c <- signal3
  pure (someFunction a b c)
  1. If a signal depends on the value of another upstream signal, you can just express that dependency with parameter passing. For example, if signal2 depends on the output from signal1, and signal3 depends on the output of both signal1 and signal2 then -
do
  a <- signal1
  b <- signal2 a
  c <- signal3 a b
  pure (someFunction a b c)
  1. A slight complication occurs when a signal depends on the value of a downstream signal, we need to use the loopS combinator which "loops" the value of the last signal back up to the top so that the upstream signals can use it. Note that you would need the last signal to carry all the values that are needed by the upstream signals.

Usually you would have some sort of a state that is accessible to all the constituent signals, and loop over that state. In the counter+timer example in my previous comment, the current value of the timer as well as the counter depended on the current count. So we need to loop that over.

  1. Note that sometimes this changes the final return value of the signal away from what you want. So you may need to map the final composition function on top of the signal value.

To take a pedantic example - if signal1 also depended on the values of signal2 and signal3, we can have signal3 return those values and make them available to signal1 with loopS. Let's assume that the initial values for signal2 is initialB and for signal3 is initialC.

loopS {b:initialB, c:initialC} \{b, c} -> do
  a <- signal1 b c
  b' <- signal2 a
  c' <- signal3 a b'
  pure {b:b', c:c'}

But now the final value of the signal is Tuple b c. So we need to fix that. We need to add the value of signal1 to the output since that's needed in the final output, and then map over with someFunction -

s = g <$> innerSignal
  where
    g {a,b,c} = someFunction a b c
    innerSignal = loopS {a:initialA, b:initialB, c:initialC} \{b, c} -> do
      a' <- signal1 b c
      b' <- signal2 a'
      c' <- signal3 a' b'
      pure {a:a', b:b', c:c'}

Not pretty, but usually you won't have such complicated state and dependencies.

drewolson commented 4 years ago

Thanks, this is hugely helpful. I think you could take this answer and put it directly into the Signals section of the documentation, it's great.

A quick clarifying question, regarding your solution to my problem:

  dyn $ loopS 0 \n -> do
    display $ D.text (show n)
    n' <- incrementTicker n
    counterSignal n'

In this case, counterSignal depends on the output from incrementTicker because it will have to update if incrementTicker ticks. But, just to be clear, this doesn't mean that counterSignal waits for the tick, correct? The bind essentially just "wires them up" but doesn't have any composition in time (as you noted in your documentation). So, intuitively, the bind onincrementTicker always produces some value, but that value would just be the initial n unless the tick has fired.

Is my understanding correct?

Thanks again for taking the time to put together this fantastic explanation.

ajnsit commented 4 years ago

Yup, correct. Both the widgets run simultaneously. Until incrementTicker fires, the initial value is used, and when the incrementTicker value changes, the counterSignal is restarted with the new value so it's always up to date.

I will put this in the documentation. Thanks for the feedback!