Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Why defer and split? #71

Closed
pyrtsa opened this issue Jul 28, 2015 · 3 comments
Closed

Why defer and split? #71

pyrtsa opened this issue Jul 28, 2015 · 3 comments
Labels

Comments

@pyrtsa
Copy link
Contributor

pyrtsa commented Jul 28, 2015

What's the purpose deferring and splitting a Stream?

What are the semantics of those operations?

It all reeks to me as a broken abstraction since every deferred Stream creates its own transaction in some (undocumented) order:

StreamSink<String> sink = new StreamSink();
Stream<String> lower = sink.map(x -> x.toLowerCase());
Stream<String> both = sink.defer().merge(lower.defer(), (a, b) -> a + b);
List<String> out = new ArrayList();
both.listen(x -> out.add(x));
sink.send("A");
sink.send("B");
sink.send("C");
System.out.println(out);

That code prints [A, a, B, b, C, c], which implies that deferred transactions see inconsistent intermediate states when deferred snapshotting and updating of Cells is involved.

Why not [Aa, Bb, Cc], i.e. make every singly deferred Stream firing happen in the same deferred transaction? Or indeed, why have defer in the first place? Does it have a use case?

@the-real-blackh
Copy link

I've defined the semantics in the denotational semantics document at https://github.com/SodiumFRP/sodium/tree/master/denotational so it is actually documented, but I didn't realize that the implementation doesn't actually comply with that definition. The output of your example with the current semantics would be [a, b, c] because both defers will put the value into the same transaction. This is actually problematic. You really want the new transactions to be unique so things don't get dumped into the same transaction - which is not useful if defer()/split() are used more than once in the program. One possibility is to use a Supply to construct a hierarchy of child transaction rankings that determine their relative order.

The next thing I'm going to do when I finish the book is a comprehensive test case generator that will compare every implementation against the denotational semantics.

First use case: Let's say you have an incoming data packet with a number of frames in it, and you want to parse it into frames and process each frame in its own transaction. split() allows you to do that without going outside the FRP paradigm.

Second use case: It can be useful to model I/O as (in Haskell syntax) Stream a -> IO (Stream b). The assumption is usually that the output value is in new transaction. To implement a null IO operation - equivalent to \a -> pure b where b is some constant, you can use defer().

I'd like to keep defer() and split() if possible because they're useful. The Supply-like method looks to me to be the only technically correct solution.

So here's how it would look:

-- This is the rank of the current transaction
rank0 :: Rank
-- Give two child transaction ranks, the returned order corresponding to the
-- temporal order and all before any successor of the input.
splitRank2 :: Rank -> (Rank, Rank)
-- give an infinite list of children, and a second child, the returned order
-- corresponding to the temporal order and all before any successor of the input.
splitsRank :: Rank -> ([Rank], Rank)
defer :: Rank -> Stream a -> Stream a
split :: [Rank] -> Stream [a] -> Stream a

Thoughts?

@the-real-blackh
Copy link

I've decided that the best solution is this:

  1. Leave defer()/split() as defined in the denotational semantics document, and fix the implementation which is currently wrong.
  2. Put a comment in the API docs explaining that if defer() is used twice in the same program, different deferred events can end up sharing a transaction.
  3. Move defer()/split() into the Operational class along with updates()/value() to indicate that they should only be used when they're really needed. This is usually but not always near the interface between FRP and non-FRP.

Whoever disagrees please comment.

the-real-blackh pushed a commit that referenced this issue Aug 6, 2015
2. Java: Move defer() / split() to the Operational class: issue #71
3. Java: Fix defer() / split() so they comply with the denotational semantics: issue #71
4. Rename s1.merge(s2) to s1.orElse(s2) as per issue #69.
@the-real-blackh
Copy link

I've implemented everything I described above. Will close.

newca12 added a commit that referenced this issue Jan 1, 2018
Improve comment for once()
Move defer() / split() to the Operational class: issue #71
Fix defer() / split() so they comply with the denotational semantics: issue #71
Rename s1.merge(s2) to s1.orElse(s2) as per issue #69.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants