Help writing function for Signal repeat after delay

156 views
Skip to first unread message

Paul Chiusano

unread,
Oct 10, 2014, 8:37:04 AM10/10/14
to elm-d...@googlegroups.com
Here is the signature:

{-| Repeat updates to a signal after it has remained steady for `t`
    elapsed time, and only if the current value tests true against `repeatable`. -}
repeatAfterIf : Time -> number -> (a -> Bool) -> Signal a -> Signal a
repeatAfterIf delay repeatFps repeatable s = ...

Use case is that user has just pressed a key (say, an arrow key), and I want to pause for a split second, then repeatedly refresh the signal at some rate. I'm not in a text box or anything though, so I don't just get to use the default keyboard repeat rate.

Here was my attempt (which failed):

repeatAfterIf t repeatFps repeatable s =
  let repeatRegion : Signal Bool
      repeatRegion = lift not (since t (lift repeatable s |> keepIf identity False))
      repeats = fpsWhen repeatFps repeatRegion
  in sampleOn repeats s

It doesn't quite work though - I get two events during the delay window.

And it took me like 15 minutes to puzzle that out. I figured I should just post here because someone might be able to whip something up in like 30 seconds. :)

Paul :)

Jeff Smits

unread,
Oct 10, 2014, 9:55:14 AM10/10/14
to elm-discuss
I think this should do it: http://share-elm.com/sprout/5437e4aee4b00800031fdcfd

import Maybe

filterMap : (a -> Maybe b) -> b -> Signal a -> Signal b
filterMap filter d input =
  filter <~ input
  |> keepIf Maybe.isJust (Just d)
  |> lift (\(Just v) -> v)

getTime = lift fst << timestamp


{-| Repeat updates to a signal after it has remained steady for `t`
    elapsed time, and only if the current value tests true against `repeatable`. -}
repeatAfterIf : Time -> number -> (a -> Bool) -> Signal a -> Signal a
repeatAfterIf time fps repeatable s =
  let interruption = always False <~ s
      startRepeat = repeatable <~ s |> keepIf ((==) True) False |> delay time
      checkDelay t last = t >= last + time
      checkedStartRepeat = checkDelay <~ getTime startRepeat ~ getTime s |> keepIf ((==) True) False
      repeats = fpsWhen fps (merge interruption checkedStartRepeat)
  in sampleOn repeats s


There may be a more elegant solution (or at least less event/timestamp based) using since, but I'm not sure

--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Jeff Smits

unread,
Oct 10, 2014, 10:09:03 AM10/10/14
to elm-discuss
Ok, new and improved:

{-| Repeat updates to a signal after it has remained steady for `t`
    elapsed time, and only if the current value tests true against `repeatable`. -}
repeatAfterIf : Time -> number -> (a -> Bool) -> Signal a -> Signal a
repeatAfterIf time fps predicate s =
  let repeatable = predicate <~ s
      delayedRep = repeatable |> keepIf identity False |> since time |> lift not
      resetDelay = merge (always False <~ s) delayedRep
      repeats = fpsWhen fps <| (&&) <~ repeatable ~ (dropRepeats resetDelay)
  in sampleOn repeats s


Be sure to check both of these implementations for any event that shouldn't be there!

Paul Chiusano

unread,
Oct 10, 2014, 11:14:17 AM10/10/14
to elm-d...@googlegroups.com
Wow, awesome! Thank you. :)

I'd appreciate any tips on how to conjure up these sorts of implementations.

Jeff Smits

unread,
Oct 10, 2014, 11:49:49 AM10/10/14
to elm-discuss
My process usually involves mind-leaps based on intuition. I don't know how to condense that to concrete advice. But I can try to describe how I got to these solutions:

I read you description and attempt at writing the code. I ran the code using the example `main = asText <~ timestamp (repeatAfterIf second 30 (((==) 1) << .x) Keyboard.arrows)`. I noted that it updated even when the right arrow key wasn't pressed. I figured that sampleOn and fpsWhen were good components, so I started manipulating your code. That didn't get me anywhere.
Then I decided to start drawing. I started by drawing some timelines for the desired input/output signals and tried to decompose it into something in between. I knew I wanted a boolean signal for the fpsWhen. I found it hard to think of the problem in terms of `since` because of edge cases like pressing a repeatable key, and within the delay window releasing and pressing it again (which should reset the delay window but might not if you're not careful). So I decided to use `delay` instead, and figured I could use foldp to match delayed key presses and non-delayed releases. That took more code than I was willing to write and half-way there I figured I could use timestamps to validate delayed repeatable events. That's how I got my first solution.
As I wrote my email with the comment that maybe using `since` could be more elegant, I started thinking it over again. In my head I saw these two boolean (high-low) signal timelines, one for repeatable and one for the delayed one. I figured that when they were both `True` the fpsWhen should run (therefore use combine the two with (&&)), but the edge case would be to check both `True` values would have been caused by the same event (I imagined events having distinct colours in the timelines). So I added that any update from `s` would reset the delayed repeatable signal to `False`, and added the dropRepeats because I don't know how fpsWhen handles repeated `False` values that come from the resets and `since`.

I hope you can find something in there. Sorry I can't give you any better tips.

Max Goldstein

unread,
Oct 11, 2014, 11:49:10 AM10/11/14
to elm-d...@googlegroups.com
Jeff, this is really great, slightly mind-bending stuff. It might be worth making a third-party library of fancy signal combinators (debounce, throttle). That said, I feel that filterMap has come up enough times that it should be added to the standard lib. Here's another implementation, from the link:

justs : a -> Signal (Maybe a) -> Signal a
justs a sma = lift (maybe a identity) (keepIf isJust Nothing sma)

Notice that this version does not include the implicit lift.

<thread hijack> I was trying to solve a similar problem the other day: tooltips. Forget collision detection for a moment, just say we have a Signal Bool and we want to to create the tooltip half a second after a True, but remove it as soon as we see a False. Additionally, if the signal goes False before that half-second, the tooltip should be cancelled. After some thought, I realized the correct abstraction is

delayBy : (a -> Time) -> Signal a -> Signal a

I'm not sure how to implement this because of canceling scheduled events. delayBy preserves the ordering of events (except for dropping): when event2 fires (after its delay), event1 is cancelled if it has not already fired. But it would make tooltips easy: delayBy (\b -> if b then 0.5*second else 0) sigBool

Jeff Smits

unread,
Oct 11, 2014, 12:17:38 PM10/11/14
to elm-discuss
Max, good to hear it helps you. I have been collecting signal combinators for a while, but don't get around to publishing it as library. Part of the delay is that I want to extend the other built-in libraries and make a sort of add-on library that more easily allows API additions, so that the most used/useful ones can later be merged into the standard libraries. Another part is that I really want to play around with the current Signal implementation and the exposed primitives.
Maybe I should just put it online as-is so people can use it already while I think about better APIs for the future.

As for delayBy, the biggest problem is that the current delay function doesn't allow a dynamic delay. So unless you want hacks using every millisecond (and I really dislike those), you'll probably need a Native implementation.

Max Goldstein

unread,
Oct 11, 2014, 4:48:18 PM10/11/14
to elm-d...@googlegroups.com
Maybe I should just put it online as-is so people can use it already while I think about better APIs for the future. 

Up to you. As I said above, the most painfully missing one is filterMap or justs or whatever.

As for delayBy, the biggest problem is that the current delay function doesn't allow a dynamic delay. So unless you want hacks using every millisecond (and I really dislike those), you'll probably need a Native implementation.

Hmm. I'm not an expert here, but looking at the implementation of delay, and assuming signal events have strictly increasing identifiers (I think they do?), it doesn't look to hard to implement. In the callback, you'd only fire the new event if its identifier was greater than the that of the current value.

Paul Chiusano

unread,
Oct 14, 2014, 4:19:06 PM10/14/14
to elm-d...@googlegroups.com
So, Jeff, let me see if I can summarize your thought process:

> ... decided to start drawing... timelines for the desired input/output signals... decompose it into something in between... boolean signal for the fpsWhen... two boolean (high-low) signal timelines, one for repeatable and one for the delayed one... both `True` the fpsWhen should run (therefore use combine the two with (&&)) ... edge case ... check both `True` values ... caused by the same event ... distinct colours in the timelines....

In other words, basically pretty much this, exactly: https://www.youtube.com/watch?v=8deYjcgVgm8

I'm just joking. :) That was actually kind of insightful. Kinda.

Paul :)

Jeff Smits

unread,
Oct 15, 2014, 11:22:14 AM10/15/14
to elm-discuss
haha, I'll take it as a compliment that you think of sci-fi scenes when reading a description of my thought process.
We should distill a clearer set of tips from this approach, but for now I'm happy it was a little insightful ;)
Reply all
Reply to author
Forward
0 new messages