SodiumFRP / sodium

Sodium - Functional Reactive Programming (FRP) Library for multiple languages
http://sodium.nz/
Other
848 stars 138 forks source link

Cell and stream loops #88

Closed ziriax closed 8 years ago

ziriax commented 8 years ago

I find the CellLoop construction very handy, but a bit ugly, as one could forget the Loop call, and it feels imperative. And maybe it allows infinite cycles, not sure.

Would the following construction be an improvement?

var loop = Cell.feedback(initialValue, cell => ...)

So Cell.feedback creates a CellLoop, passes that as a Cell to the lambda argument, and expects that lambda to return a Stream. Then it calls Loop with the resulting stream that starts with the initial value using Hold.

Overloads could be made that create more than a single cell loop, to avoid nested calls.

A transaction is automatically started by the feedback method.

jam40jeff commented 8 years ago

I do like this idea. I'm not sure how it would work for StreamLoop, but it'd be nice to have a similar construct. Maybe it would just be a less safe version of the same thing, since the Stream.feedback lambda would need to return a Stream, opening the door for users to accidentally return the same stream that was passed to the lambda.

This also would mean that the CellLoop class (as well as the StreamLoop class if it was implemented also) could be made internal.

jam40jeff commented 8 years ago

Actually, I believe the Cell.feedback lambda would need to return a Cell rather than a Stream, as there are cases where the CellLoop needs to be looped over the result of a Cell.lift.

I believe the problem could be solved by the type system (introduce a base classes of Cell, maybe CellBase or NonLoopableCell, that cannot be looped, and only primitives like hold would produce the loopable Cell), but I'm not sure it's worth it for the complexity it would add to the API.

ziriax commented 8 years ago

Another overload of Cell.feedback could have no initial value, and a lambda that must return a cell. Maybe that method should be called Cell.loop instead of Cell.feedback, making the intent more clear.

So Cell.feedback, Cell.loop and Stream.loop

the-real-blackh commented 8 years ago

I considered doing it this way, but the problem is that in Java (and this may not be so true in C#), you're fighting both the way people customarily do things, and the language. The language, because you often need to output a number of values from inside the loop. If you're using a lambda, this is problematic. In Java you would either need to define an array and poke them into the array (!), or have some mechanism where extra values can be returned from the lambda.

So I did this to make the interface as simple as possible, sacrificing the semantic cleanness that you would like. Of course if you can find a nicer way to do it in C#, then go for it.

EDIT: I should add that I am trying to make this library appeal to and not seem difficult to non-functional programmers. Every extra method that is added is a potential source of confusion and gives the appearance of a complex API. Many FRP/FRP-like systems suffer from this sort of off-putting feature bloat.

jam40jeff commented 8 years ago

Yes, you're right. I didn't think about that. The same problem would exist in C#.

Even in a functional language with Tuple decomposition, it would be risky, as any streams or cells with the same type could easily be tuple in one order and untupled in another.

ziriax commented 8 years ago

Point taken, very good argument! It indeed keeps the API simple.

Now as far as I understand, one must nest all the loops in a Transaction.Run statement, and that method only returns a single value anyway. So the problem also exists here (but might be less intrusive)

PS: David Sankel seems to have taken a similar but not exactly the same approach with his 'Wormhole': https://github.com/camio/sbase/blob/master/include/sfrp/wormhole.hpp

the-real-blackh commented 8 years ago

Yeah, I noticed David's wormhole was similar. Yes, Transaction.run() is a bit inconsistent with this thinking. The reason why I chose to use a lambda for Transaction.run() is because the consequences of forgetting to close the transaction are a bit more severe because with a lot of them, it could be difficult to debug, and also because the need to return multiple values is generally not as great. Of course arguments could be made against this.

ziriax commented 8 years ago

It all makes sense. IMO you are a great API designer, it is a delicate balance to get that right.

the-real-blackh commented 8 years ago

:)

the-real-blackh commented 8 years ago

Then we'll call this closed, but feel free to open the issue if a good solution comes up.

jam40jeff commented 8 years ago

I did implement loops this way in the F# version, but it works much better with the F# syntax than it would with Java or C#.

the-real-blackh commented 8 years ago

I think that's the right thing to do (design the API to be natural in each language).

ziriax commented 8 years ago

I can't wait to play with the F# version, thanks a lot!