Loop possibilities using records

61 views
Skip to first unread message

Evan Czaplicki

unread,
Apr 24, 2013, 2:07:27 PM4/24/13
to elm-d...@googlegroups.com
So far we have discussed two kinds of loops: unsafe and safe. Here are two ideas to rephrase the proposals so far with records.

Unsafe Loops

This is just a simple way to create loops with no safety guarantees at all.

loop : a -> { send : Signal a -> Signal a,
              recv : Signal a }

First look at the record returned.
  • `send` takes in a signal and returns the same signal. The trick is that it also pipes the values through to `recv`.
  • `recv` is the "looped signal". The "looped signal" are the values that have been sent with the `send` function.
The argument to `loop` is the base case for the "looped signal". An example usage would be:

playing = loop True

game = foldp step startState (30 `fpsWhen` playing.recv)

programSaysPlay = playing.send (dropRepeats (.play <~ game))

main = display <~ game

In this example, game is the thing we'd like to show on screen. It has a field game.play which indicates whether the it should keep updating. We want to only give FPS when game.play is true. So we send game.play through our loop and give it to fpsWhen. This example is weird in that there is no way to unpause, but I wanted to keep it simple.

This is unsafe so you must personally add some kind of filter. I chose to use dropRepeats, but any other filter would work.

Jeff's Loop without special syntax (Safe Loop):

I think there is a way to allow safe loops without special syntax. As I understand, the key features are: allow arbitrary loops, block infinite loops by tying updates to another signal.

loop : a -> { send : Signal a -> Signal a,
              sampleOn : Signal b -> Signal a }

First look at the record returned.
  • `send` takes in a signal and returns the same signal. The trick is that it also pipes the values through to the `sampleOn` function.
  • `sampleOn` takes an arbitrary signal (as a trigger) and returns the "looped signal" which only updates when the trigger is updated. The "looped signal" are the values that have been sent with the `send` function.
The argument to `loop` is the base case for the "looped signal". An example usage would be:

myLoop = loop True

playing = myLoop.sampleOn Mouse.clicks
game = foldp step startState (30 `fpsWhen` playing)

programSaysPlay = myLoop.send (.play <~ game)

main = display <~ game

This demonstrates the issue that events only come through if the user clicks the mouse, but I think it illustrates the idea. Does this miss any key aspect of Jeff's proposal? If so, I probably have misunderstood something, so sorry in advance.

Other thoughts:

It seems like there may be many filtering techniques that could be useful for loops. One idea is to only provide safe loops, as in Jeff's proposal. I like this, but I also think there would need to be a family of looping constructs to permit all of the kinds of safe loops you'd want. (loop + dropRepeats, loop + sampleOn, loop + ?)

It also seems like there may be some restrictions we can impose on the Unsafe Loop. Something like: no loop update can trigger another loop update (e.g. loops only go around once). I think there are problems with that particular restriction, but this general idea may be have nice results.

John Mayer (gmail)

unread,
Apr 24, 2013, 2:22:05 PM4/24/13
to elm-d...@googlegroups.com
I don't understand the rationale behind the switch to records. Higher order signal functions made more sense to me. Can I use the send twice for the same loop "instance"?

Personally I'd prefer exposing both the unsafe "loopUnsafe", as well as some pre-made safe loop variants like "loopWhen" and "loopUnique", which are pure elm implementations in terms of the unsafe loop.

Sent from my iPhone
--
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/groups/opt_out.
 
 

Evan Czaplicki

unread,
Apr 24, 2013, 3:03:38 PM4/24/13
to elm-d...@googlegroups.com
I like the record approach because is orthogonal to the rest of the signal primitives. You can write your programs exactly the same way, and then use loops if you need to.

The idea of signal nodes in signal nodes ("higher order signal functions") feels very suspicious to me: it overlaps with Automaton and edges into signals-of-signals territory. In my mind, a loop is a wire, not a new signal node. I think Jeff's proposal captures that as well.

More context: Elm's FRP model is a well-behaved subset of message passing concurrency. Signals are a high-level way of creating a well-behaved concurrent system. Loops are just one of the many things that can go wrong with a full version of concurrency, so they were disallowed.

Longer term I was thinking of adding an explicit message-passing concurrency library where it is obvious that you have no safety guarantees. If talking between signals and threads is permitted, you could implement any of these proposals in Elm code.

John Mayer (gmail)

unread,
Apr 24, 2013, 3:29:57 PM4/24/13
to elm-d...@googlegroups.com
So specifically, what's the behavior of 

a = loop init
b' = a.send b
c' = a.send c

I don't think passing a higher order signal function is scary. I agree that it looks like an Automaton, but that's partly by design. It certainly does not cross into (Signal (Signal a)) territory. It just a higher order function like any other. I'll continue on a fresh thread, though I may first just try and implement; not sure about your thoughts on the famous phrase "code wins arguments" ;-)

Sent from my iPhone

Jake Verbaten

unread,
Apr 24, 2013, 3:39:36 PM4/24/13
to elm-d...@googlegroups.com
+1 for rephrasing Jeff's loop structure. Your examples are concise and readable.

I'm not familiar with the `.play` syntax though is that `\a -> a.play` ?

Evan Czaplicki

unread,
Apr 24, 2013, 4:28:09 PM4/24/13
to elm-d...@googlegroups.com
Jake, thanks :) And yes, that is correct: (.play == \r -> r.play)

John, I guess that would have to work like a merge. When would this be a problem? (Question, not a challenge.)

I wrote up a big thing about all the ways that signals-of-signals leads to bad results, so I'll try to share that online soon.

More generally, this approach is a way to create arbitrary wires between signal nodes. That can be used for loops, but it can also be used in the forward direction. So maybe it makes sense to think of what existing operators could be defined with this approach. Merge and merges would nearly work. Foldp seems doable:

foldp f b signal =
  let myLoop = loop b
      next = myLoop.send (lift2 f signal (sampleOn signal myLoop.recv))
  in  myLoop.recv

If this really provides the expressiveness of full message-passing concurrency, everything should be expressible in terms of loop, including additional, dangerous stuff. My intuition would be to work out how message-passing concurrency would work in Elm before moving too far ahead with this idea.

Jeff Smits

unread,
Apr 28, 2013, 8:10:22 AM4/28/13
to elm-d...@googlegroups.com
It's an interesting proposal Evan. "no loop update can trigger another loop update" is certainly what I am after as functionality. It's really closest to my intuition of how signals work in Elm. But I don't see how you can add that functionality to these kinds of wires as easily as any of those filters you named. 
I also dislike that the merge behaviour of using .send twice in your code is undefined when two signals update at the same time. 
My last and I think for me most important concern is that it looks like you're exposing the inner workings, whereas I really like the way Elm works now with the send/receive being implicit. This way of loose wires is too powerful (imho), even if you only permit ones which are completely safe. I'm afraid they will promote a haphazard, messy code style. 

But it does looks really easy to use and is probably easy to explain. 
Reply all
Reply to author
Forward
0 new messages