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

Proposal: Disallow multiple simultaneous events #25

Closed
HeinrichApfelmus opened this issue Jan 3, 2012 · 47 comments
Closed

Proposal: Disallow multiple simultaneous events #25

HeinrichApfelmus opened this issue Jan 3, 2012 · 47 comments

Comments

@HeinrichApfelmus
Copy link
Owner

When merging two event stream, what should happen to simultaneous event occurrences? As of reactive-banana version 0.4, they will be kept around and ordered from left to right. Here an example in terms of the model implementation:

union [[1],[2]], [[3],[]] = [[1,3], [2]]

Proposal

I propose to dispense with these kind of simultaneous event occurrences and change the Event data type so that only a single event may happen at each point in time.

The type of union will be changed to

union :: Monoid a => Event a -> Event a -> Event a

Instead of appending simultaneous events, the union function would merge them into a single event occurrence, using the mappend combinator.

Properties

  1. Since a -> a is a monoid, this works well in conjunction with the accumulation functions accumE, accumB. Existing code is essentially unchanged in this regard, except for ordering.

  2. Merging events in other cases will be a little less pleasant, we probably need a left-biased union

    lunion :: Event a -> Event a -> Event a
    x `lunion` y = (filterJust . getFirst) (First x `mappend` First y)
    

    The drawback is that event occurrences may be lost this way.

  3. The following two behaviors are now equivalent

    initial :: A
    update  :: A -> A
    
    behavior1 :: Event () -> Behavior A
    behavior1 e = accumB initial $ update <$ e
        where event = update <$> (b1 <@ e)
    
    behavior2 :: Event () -> Bevahior A
    behavior2 e = stepper initial event
        where event = accumE initial $ update <$ e
    

    In the presence of simultaneous event occurrences, these two programs are not equivalent because the event e may contain multiple occurrences.

  4. To summarize 2 and 3: the trade-off is that we now have to think about simultaneous events whenever we take the union of two event streams, but we are freed from thinking about them when we take the snapshot of a behavior. Gregory Crosswhite reports that this would probably simplify a program he writes.

  5. The problem about optimizing the Discrete type (issue Optimize Discrete data type by calming simultaneous events #19) disappears.

  6. The old semantics can always be obtained as Event [a] if desired. In fact, that's how the model implementation defines the current semantics. The calm function mentioned in issue Optimize Discrete data type by calming simultaneous events #19 becomes straightforward to implement.

Discuss!

What do you think? How would this change affect your existing code base?

@ehird
Copy link

ehird commented Jan 3, 2012

I don't like this. For starters, a -> a is a Monoid, but probably not the Monoid you want; it's pure mempty and liftA2 mappend, not Endo.

Indeed, I can't think of a single argument to Event other than containers for which the Monoid instance would do what you want.

It also just seems much less modular to me; let's say we have

data Button = A | B
aClicked :: Event ()
bClicked :: Event ()

and we want

buttonClicked :: Event Button

With the current semantics, this is trivial:

buttonClicked = (A <$ aClicked) `union` (B <$ bClicked)

Without simultaneous events, we'd be forced to drop one of the occurrences whenever A and B are clicked simultaneously (not possible for buttons with typical GUI toolkits, perhaps, but you can certainly have completely reasonable Events where this can happen; e.g. two Events filtering/mapping the same base Event), say by defining a horrible Monoid instance or using lunion; but of course this breaks down in that you lose the right event. Yes, you can just use an Event [Button], but pushing the complexity onto the programmer seems like a bad idea.

The only way I could see this working would be if we had an infinitesimal delay:

delay :: Event a -> Event a

but even then I think it'd make things very awkward.

(If you're coming here from email, note that I've updated this comment since posting it.)

@gcross
Copy link

gcross commented Jan 4, 2012

I must completely (but respectfully) disagree with ehird. :-)

In my personal experience, any time I have wanted to merge event streams I have always wanted to do something more intelligent then just merging lists, so although on some level defining my Events in terms of monoids would be less convenient it would let me write far better code.

I would argue in general, in fact, that whenever events are merged, the programmer needs to both think carefully about what this means --- that is, specifically how the information from the two events must be combined --- and also about how to communicate this explicitly to future readers of the code. Taking away freedom for the programmer to do this by forcing him to essentially treat Event a as necessarily being Event [a] in all cases does in a sense save the programmer some thinking by giving him fewer choices (which can make using the library more convenient) but it doesn't make the fundamental problem go away so much as it sweeps it under the rug and makes it harder to get things to work correctly and to communicate intent in contexts where merging lists is not how the information from multiple events should be combined.

ehird proposes buttonClicked as an example of where the new semantics would fail, but if anything I think that this proves the point that the current semantics are sub-optimal because just looking at buttonClicked we have no idea what it is supposed to mean. There are at least two options I can think of: either it specifies when one or the other of the buttons has been clicked, in which case lunion would be the most apprioriate combinator, or it provides a list of which buttons have been clicked, in which case giving it the type Event [Button] is much more clear and explicit.

Finally, if one prefers the old semantics, note that they can be built very easily on top of the proposed semantics by just creating a module where Event a is aliased to Event [a]. Thus, the new semantics are strictly more general than the old semantics.

@ehird
Copy link

ehird commented Jan 4, 2012

Are you saying you never use accumE a (e1 union e2) or similar? For instance:

let numberB = accumB 0 $ ((+1) <$ incrementE) `union` (subtract 1 <$ decrementE)

This proposal would break that; you would instead have to write:

let numberB = accumB 0 $ appEndo <$> (Endo (+1) <$ incrementE) `union`
                                     (Endo (subtract 1) <$ decrementE)

or, at best:

let numberB = accumB 0 $ unionWith (.) ((+1) <$ incrementE) (subtract 1 <$ decrementE)

I do not understand what you mean by "we have no idea what it is supposed to mean"; the semantics are very clear about what it means. They might not be the semantics we want, but that's a different matter :)

I find the notion that allowing two occurrences of an event to be, say, 0.00001 attoseconds apart is just fine, but having them 0 seconds apart is confusing. Why would you expect the meaning of buttonClicked to break down just because there was no imperceptible delay? It fires when either button is clicked. I'm very uneasy about how this proposal seems to break the simple compositionality of FRP.

I agree that the current semantics can be implemented on top of these proposed ones, but I don't really see that as an argument one way or the other; what's important is which semantics is better.

I'm not saying I'd oppose every possible proposal that involves disallowing simultaneous events, but this one doesn't seem to offer nearly enough for it to be compelling to me. I especially don't like the idea of changing the semantics in a fundamental manner just because it makes optimising a type slightly less work.

As an example, here's something changing the semantics of simultaneous events I would find compelling: use a bag (multiset) rather than a list. You can't order two simultaneous occurrences, and union a bunion b a is undesirable. Event a's model would then be [Map a Integer], although defining a separate Bag a type would make this more elegant. (Of course, we wouldn't want the Ord constraint, but that's another matter entirely.)

@gcross
Copy link

gcross commented Jan 4, 2012

Are you saying you never use accumE a (e1 union e2) or similar? For instance: [...]

I agree that some of these constructs would become more awkward, though on the other hard at the moment the presence of simultaneous events often means either that I can't use something like accumE or that I have to just hope that certain events are not fired simultaneously because that would cause internal state to become corrupt.

I do not understand what you mean by "we have no idea what it is supposed to mean"; the semantics are very clear about what it means. They might not be the semantics we want, but that's a different matter :)

I mean that it is not clear at all what the coder had intended for that event to mean, not that it has unclear semantics.

I find the notion that allowing two occurrences of an event to be, say, 0.00001 attoseconds apart is just fine, but having them 0 seconds apart is confusing. Why would you expect the meaning of buttonClicked to break down just because there was no imperceptible delay?

Because the two buttons might indicate contradictory actions. For example, button A might mean "Save all changes" and button B might mean "Revert all changes". Or button A might mean "Move the selection down one item" and button B might mean "Delete the currently selected item". In such cases having the two events occur simultaneously is very different from having one occur slightly after the other.

I'm not saying I'd oppose every possible proposal that involves disallowing simultaneous events, but this one doesn't seem to offer nearly enough for it to be compelling to me. I especially don't like the idea of changing the semantics in a fundamental manner just because it makes optimising a type slightly less work.

I don't see the fact that "it makes optimising a type slightly less work" as being a significant factor at all either way.

The primary advantage of the proposed semantics is twofold. First, although they complicate the code in some respects, they simplify it in other respects because now you no longer ever have to worry about what would happen if an event fires multiple times simultaneously, and with possibly contradictory values. Second, although they they force the programmer to make an explicit choice whenever events are merged about exactly how the information should be merged, this makes the resulting code safer because it forces the coder to resolve the ambiguities caused by simultaneous events rather than letting them propagate.

@gcross
Copy link

gcross commented Jan 4, 2012

One more thing: if you find multiple simultaneous events to be useful, then it is possible to create a module which define all of the functions you use in such a way that they work with a type Event a that is aliased to Event [a], requiring no change in your code at all. By contrast, I can't create a type alias that allows me to be certain that an event I am working with only has a single occurrence. I mention this because even if there were some function that when applied to an event combined all of its multiple simultaneous occurrences into a single occurrence, there is no way for me to leverage the type system to capture this property and preserve its invariance.

So again, the proposed semantics give me tools that let make my code a lot safer, while allowing you to keep the semantics that you like in the form of a library built on top of it.

@ehird
Copy link

ehird commented Jan 4, 2012

at the moment the presence of simultaneous events often means either that I can't use something like accumE or that I have to just hope that certain events are not fired simultaneously because that would cause internal state to become corrupt.

Corrupt? I'd like to see an example, since I'm sincerely having trouble thinking of a situation where I wouldn't consider this a flaw in your design or code.

Because the two buttons might indicate contradictory actions. For example, button A might mean "Save all changes" and button B might mean "Revert all changes". Or button A might mean "Move the selection down one item" and button B might mean "Delete the currently selected item". In such cases having the two events occur simultaneously is very different from having one occur slightly after the other.

Not really, since reactive-banana currently preserves the ordering of events in a well-defined manner. Can you give an example where simultaneity actually causes some kind of time paradox? :)

Anyway, external inputs can't really be external (though of course you can't rule the possibility out), so these examples are fairly unrealistic. That's my fault for bringing up buttons in the first place, though.

I don't see the fact that "it makes optimising a type slightly less work" as being a significant factor at all either way.

I only mentioned it because Apfelmus did :)

As I've said, It's true that the current semantics can be implemented on top of these proposed ones; but that's not an argument for adopting them if the proposed ones are, in fact, worse; I would experience compatibility problems because I'd have to "lift" every event written with the normal style, thus requiring a large amount of glue code. It's not really practical, it's just implementing my own FRP library by outsourcing the hard parts.

I agree that there is currently no type for "event with no simultaneous occurrences" but, as much as I'd like every programmer to be able to use exactly the tools they desire, good design should come above such personal choices.

I hope we can come to an agreement on what should be done that satisfies us all; I'll wait for Apfelmus to have his say before replying any further :)

@gcross
Copy link

gcross commented Jan 4, 2012

I agree that there is currently no type for "event with no simultaneous occurrences" but, as much as I'd like every programmer to be able to use exactly the tools they desire, good design should come above such personal choices.

Of course that sentiment goes both ways...

@ehird
Copy link

ehird commented Jan 4, 2012

Of course :) That's why I hope we can come up with a solution that satisfies everybody.

@gcross
Copy link

gcross commented Jan 4, 2012

Corrupt? I'd like to see an example, since I'm sincerely having trouble thinking of a situation where I wouldn't consider this a flaw in your design or code.

Problems can occur in situations where you have multiple interacting behaviors. For example, suppose we have two behaviors, A and B. One kind of event moves a bit of data from A to B, and another kind of event moves it back from B to A. If both kinds of events fire simultaneously then some bit of data will necessarily be lost because there is no way for it to move back to its original source as its new source doesn't have it yet (by the way that Behaviors are designed). In fairness, though, I suppose that in such cases one can A and B into a single behavior, but this destroys modularity and can result in ugly, tangled code. Nonetheless, perhaps it would have been better for me to say that the multiple simultaneous occurences can greatly complicate matters if you want to avoid corruption, rather than claiming that they cause corruption themselves.

Problems also occur when you want to postpone making a decision about firing an event until all of the information has been accumulated. Unfortunately, there is no construct in reactive-banana that lets you do this; Discrete and accumE will all fire an event for every one of the simultaneously occuring events that provides and update. There have been times when I needed to assemble pieces of information from two Discretes that would always change together and then make a decision based on them, but I couldn't simply create a new discrete from them because only one of them would change at a time and the decision needed to be made on both values simultaneously, which would result in spurious events firing. Admittedly this could be handled without going with the proposed semantics.

Not really, since reactive-banana currently preserves the ordering of events in a well-defined manner. Can you give an example where simultaneity actually causes some kind of time paradox? :)

No, but that question misses the point, which is that currently you can't design your event network assuming that first an event will fire moving the selection box downward, second all the behaviors will be updated, and third an event will fire deleting the currently selected element. You instead have to deal with the possibility that none of the behaviors will be updated between the two events, which greatly complicates the design of the event network if you want to make sure that there is no corruption.

My claim is that it would be easier to deal with this kind of complexity in a model where there was a more explicit way to specify how Events values should be combined, though admittedly this could conceivably be done within the current semantics rather than requiring a switch to the new semantics.

Nonetheless, though, I do think that the subtle issues raised by multiple simultaneous events might be better handled by having the programmer make an explicit decision about how the information from them will be combined.

Anyway, external inputs can't really be external (though of course you can't rule the possibility out), so these examples are fairly unrealistic.

I am not sure what you are getting at with this; ultimately our event networks are maps from events to events, and unless we have full control over where the input events are coming from then we cannot make any assumptions about them if we want our code to be robust. I will admit, though, that my case is atypical in that I am writing event networks that are explicitly not tied to any event loop, but rather intended to be connected to an event loop specified by the user (to make a long story short), and therefore I am hesitant to place any assumptions on the input events. It might be that in the typical case a user can reasonably assume that, for example, two button events will never fire simultaneously.

As I've said, It's true that the current semantics can be implemented on top of these proposed ones; but that's not an argument for adopting them if the proposed ones are, in fact, worse; I would experience compatibility problems because I'd have to "lift" every event written with the normal style, thus requiring a large amount of glue code. It's not really practical, it's just implementing my own FRP library by outsourcing the hard parts.

I seems to me that only parts you would have to lift are the external events, and everything internal to your code could be implemented using the current semantics, though if most of your code is spent interfacing with the external events I can see that this would not be very helpful.

Regardless, I see your point that in practice everyone would be targeting the "official" semantics and so even if you wanted to use another semantics you would be treated as a second class citizen.

Of course :) That's why I hope we can come up with a solution that satisfies everybody.

Yes, it just sounded a bit in your original message like you were implying that I was placing my own personal preferences over good design, though I was probably reacting a bit oversensitively.

@ehird
Copy link

ehird commented Jan 4, 2012

Sorry, I intended to write "external inputs can't really be simultaneous". I'll reply to the rest of your comments later on.

Yes, it just sounded a bit in your original message like you were implying that I was placing my own personal preferences over good design, though I was probably reacting a bit oversensitively.

I didn't mean to imply you were doing that at all, and I apologise if I sounded that way; I only meant to say that when deciding between two possible semantics, the goal should be to find the best one, not one that some people like and others can work around :)

@gcross
Copy link

gcross commented Jan 4, 2012

Sorry, I intended to write "external inputs can't really be simultaneous". I'll reply to the rest of your comments later on.

Okay, I thought that was what you were getting at, in which case my point still stands that this is not true in general because you can make no assumptions about external inputs since by definition you have no control over them.

I didn't mean to imply you were doing that at all, and I apologise if I sounded that way; I only meant to say that when deciding between two possible semantics, the goal should be to find the best one, not one that some people like and others can work around :)

That is certainly fair enough. :-)

@gcross
Copy link

gcross commented Jan 4, 2012

So, perhaps the best way of stating my central thesis is as follows: with the proposed change of semantics, we are granted much more power to encode invariants about our events into the type system, and that is a big win.

@HeinrichApfelmus
Copy link
Owner Author

Accumulation

Concerning the accumulation functions, the intention is, of course, to keep them as they are. I didn't think that a -> a has a weird monoid instance, so that will be

unionWith :: (a -> a -> a) -> Event a -> Event a

instance Monoid (Event (a -> a)) where
    mempty  = never
    mappend = unionWith (.)

then, or anything else to make it pleasant. This is the part I'd like to stay the same.

Simultaneity

Elliott Hird makes the argument that it's weird to have a teensy time difference (0.00001 attoseconds) to make all the difference, but unfortunately, that's true for either semantics.

To take the buttonClicked example, consider two counters that count how often the buttons have been clicked.

data Button = A | B

ebuttonClicked = (A <$ clickedA) `union` (B <$ clickedB)

bcountA = accumB 0 $ (+1) <$> filterE isA ebuttonClicked
bcountB = accumB 0 $ (+1) <$> filterE isB ebuttonClicked

Now, we want to emit a message whenever one button has been clicked more often than the other

emessage
    = (curry (message . update) <$> bcountA <*> bcountB)
    <@> ebuttonClicked

update (a,b) A = (a+1,b)
update (a,b) B = (a,b+1)

message (a,b) = case compare a b of
    LT <- Just "B leads"
    EQ <- Nothing
    GT <- Just "A leads"

This program works fine until you consider the case when both buttons are clicked simultaneously. Under the old semantics, the program will erroneously report that both counters are ahead of each other at once. Under the new semantics, this corner case will simply not appear, but the program would have been rejected by the compiler before that.

(I think that this is an instance of Gregory Crosswhite's general example of two behaviors that are exchanging information with each other.)

Of course, our troubles are in the nature of the program, regardless of the semantics used:

  1. In a sense, we forgot to specify which event should come ahead if both occur simultaneously. The old semantics make it easy to forget this case, while the new semantics would force us to specify this case when merging the event streams. (In the old case, one would have to use accumE, so this is an instance of property 3.)
  2. In a sense, the program was not robust to begin with. Even a teensy time difference can make a substantial difference in the output of the program, depending on which event comes ahead first in case of a tie.

@gcross
Copy link

gcross commented Jan 4, 2012

Heinrich,

First, just as an aside, I agree that if we went with the proposed semantics then it would be optimal to define an instance for Monoid (Event (a -> a)) for the sake of convenience, but there is a slight concern that this will only work as long as nobody else has defined a Monoid instance for a -> a, since in that case there will be overlapping instances for instances of the form Monoid a => Monoid (Event a).

Second, in the example you provided it all we need is something like the calm combinator you have talked about previously to completely solve the problem, since that would prevent emessage from firing until it had taken into account all of the information available at that instant in time. In fact, I suspect that it is generally the case that most of the problems faced in the current version of reactive-banana could by solved by adding combinators that give the user more explicit control over the event stream, though I would consider such an approach to be inferior to adopting the proposed semantics because I would prefer to have invariants such as "only one occurrence of this event will be fired" expressed through the type system so that they can be statically guaranteed rather than merely being promised.

(Also, thank you for putting into concrete code a concrete example of one of the problems that I had been attempting to convey less clearly in words. :-) )

@gcross
Copy link

gcross commented Jan 4, 2012

It is also worth noting, though, that the calm combinator would not solve the problem I mentioned earlier with the behaviors A and B. To explain why, I will be a bit more explicit about the problem that I face.

A contains a list of workers and the workloads that they are respectively working on, and B contains a list of either workers waiting for workloads or workloads waiting for workers. If a worker shows up and a workload is available, then this workload is moved from B to A. If a currently active worker disappears, then that worker's workload is moved from A to B (and possibly immediately handed to another worker). Unfortunately, if a worker simultaneously shows up and disappears then there is the possibility that a workload will be removed from B and sent to A, then deleted in A, so that it disappears entirely; the reason why this happens is because when the events occur A does not contain the workload, and so the event which attempt to move the workload from A back to B will fail.

There are a couple ways of solving this problem. One is to merge A and B, but I really don't like this solution because it breaks modularity. What I would prefer to do instead is to take all of the incoming worker events and run them through a function turns them into a "safe" event. Unfortunately there is presently no way for me to do this because there are no combinators that let me directly access and transform the list of simultaneously occurring events. The calm combinator is insufficient here because it just causes me to lose information. You have actually suggested a couple of combinators that might actually provide a solution for this problem whose names I think were something like reify and reflect.

Nonetheless, as I have stated before I still think that the most elegant solution is just to get rid of simultaneous events entirely and instead explicitly state at each step how information from multiple events is to be combined.

@gcross
Copy link

gcross commented Jan 4, 2012

In fact, now that I think about it, if I could have static guarantees then I could make an incredibly simple change that would immediately solve this problem: I could simply replace the separate worker added and worker removed events with a single event with a value that indicates that either worker is added or a worker is removed. Since this event would be guaranteed to never fire more than once simultaneously, my problem would immediately be solved.

@HeinrichApfelmus
Copy link
Owner Author

Observing simultaneous events

I usually call the combinators you mention

collect :: Event a   -> Event [a]
spill   :: Event [a] -> Event a

In one direction, we have

calm = spill $ last <$> collect

but it turns out that it's also possible to define collect in terms of calm (exercise)!

I think we can all agree that these combinators need to be added to the current semantics.

Alternate semantics as a library

Actually, I now realize that with these combinators, we can implement the proposed new semantics as a library on top of the old ones. (The other direction is property 5).

Namely, the types stay the same

New.Event a    = Old.Event a
New.Behavior a = Old.Behavior a

but we can simply hide the union function and instead provide

unionWith f e1 e2 = spill $ foldr1 f <$> collect (e1 `union` e2)

Assuming that no external event fires multiple simultaneous events, the New.Event type is now guaranteed to contain no multiple simultaneous events.

In other words, Gregory, in case you are dissatisfied with the current semantics, it becomes possible to implement your desired semantics in a library. In fact, that's what you'll be doing as soon as you write the two lines

import Reactive.Banana hiding (union)
unionWith = ...

Tentative Conclusion

Wow, I like that, I didn't think it would be so easy. The idea is simply to forbid yourself to use the union combinator. Now, if we just get rid of the Monoid instance for all events and use

instance Monoid (Event (a -> a)) where ...

instead, I think we will get the best of both worlds, a general union for Elliott and guaranteed invariants for Gregory?

@ehird
Copy link

ehird commented Jan 4, 2012

Hmm. That example is indeed quite bad. I'm beginning to think I might not fully understand the implications of simultaneous events.

So here's a question: Assuming we had a model of infinitesimal delay, what if we switched to a no-simultaneous-events model, and simply specified that union ((t,a):as) ((t,b):bs)(t,a) : (t+ε,b) : union as bs? That would allow us to keep a fully general union which does the right thing when the two event streams fire simultaneously, preserves the "invariants" of our programs that don't expect simultaneous events, and keeps the left-biased semantics of union. And we could still have unionWith.

Admittedly, this is hypothetical since we don't have a notion of infinitesimal delay, but it sounds like the best of both worlds to me. Of course, this isn't quite a complete specification of union; you have to deal with the case where the next element of as or bs is at t+ε, but presumably we can come up with a semantics where these things don't matter; after all, the point of infinitesimal delays is that they don't actually happen :)

Edit: Actually, solving that issue is simple: union ((t,a):as) ((t,b):bs)(t,a) : (t+ε,b) : union (delay as) (delay bs) where

delay :: Event a -> Event a
delay = map (\(t,x) -> (t+ε,x))

@gcross
Copy link

gcross commented Jan 5, 2012

Heinrich,

Merely hiding union from myself is not enough to guarantee that multiple simultaneous events are not fired because it does not give me a static guarantee that the person supplying me with external events didn't use union either. :-) In order for me to have such a static guarantee, New.Event and New.Behavior would have to be made opaque.

Furthermore, if event loops are typically written using Old.Event and Old.Behavior because these are considered the "standard" types, then there is the annoying fact that even though most GUI event loops in practice will never fire more than one event simultaneously, the types do not encode this invariant. Thus, the user is placed in the position of either essentially coercing these types to New.Event and New.Behavior and hoping this is safe due to assumptions about how the event loop works, or making no such assumptions to be sure that they are being safe but as a result being stuck with the inconvenience of the full generality possible with Old.Event and Old.Behavior.

So I wouldn't call this the best of both worlds, though I suppose it could be a decent compromise. :-)

@gcross
Copy link

gcross commented Jan 5, 2012

ehird,

That is an interesting idea because it has the nice property that it lets you essentially lets you update all of the behaviors before processing the next event in the stream, and so in cases where I am dealing with "untrustworthy" sources of events that kind of combinator would help a lot!

However, we would still want combinators that combine simultaneous events without inserting a delay. In Heinrich's example imagine that clickedA and clickedB are not external events but events internal to your application that are both defined to fire if some event C fires. In that case you don't want a delay between them because if C fires then you will want the counters for both A and B to advance simultaneously.

@ehird
Copy link

ehird commented Jan 5, 2012

Yes, unionWith should still be added to allow handling of simultaneous events. I think this is now my preferred model; it matches how I was reasoning about simultaneous events (no "external" delay, but fully-ordered internally), allows a general Monoid instance, and should leave most code unchanged.

The only issue is to find a model of it — something like

data Delayed a = Now a | Delay (Delayed a)
type Event a = [Delayed a]

delay :: Event a -> Event a
delay = map Delay

seems promising.

@gcross
Copy link

gcross commented Jan 5, 2012

In fact, the more that I think about it, the more that it would be nice to have a way to delay an event. There is a situation in my code where the most declarative way for me to express my code unfortunately involved an event cycle which meant I had to uglify it and duplicate some code in order to break the cycle. Having the ability to delay an event in order to explicitly break cycles would be useful --- though it also gives one the ability to create infinite loops, as I had done inadvertently in my original definition :-), though I think I know how to fix that.

@ehird
Copy link

ehird commented Jan 5, 2012

Sorry, that model is totally wrong. I think it has to be something like

type Event a = [Maybe a]

where Nothing is taken as an infinitesimal delay.

@HeinrichApfelmus
Copy link
Owner Author

Infinitesimal delays are, unfortunately, a nasty can of worms that are probably beyond the scope of this issue. I've talked to Conal Elliott about infitesimal delays and he was not exactly thrilled, because they do complicate the semantics quite a lot.

I'm a bit more pragmatic, but my main problem with infinitesimal delays is that I haven't been able to find a semantics model in the spirit of Reactive.Banana.Model so far; nothing I can come up with is lazy enough to support recursion.

Concerning this issue here, would you both agree with the following compromise

  1. We keep the old semantics
  2. But offer a unionWith function and remove the general Monoid instance
  3. And keep infinitesimal delays in mind for future investigation?

@gcross
Copy link

gcross commented Jan 6, 2012

So, just to make sure I understand, unionWith f x y would use f to combine all of the multiple simultaneous events in x and y using f?

@HeinrichApfelmus
Copy link
Owner Author

Yes. The definition of unionWith is going to be

unionWith :: (a -> a -> a) -> Event a -> Event a -> Event a
unionWith f e1 e2 = foldr1 f <$> collect (e1 `union` e2)

For instance

unionWith (+) [[1,2], [3]] [[4], []] = [[7], [3]]

@gcross
Copy link

gcross commented Jan 6, 2012

Then the compromise would be adequate. :-) However, it would still be very useful to me to have a type where I knew that there was only one simultaneous event, so I have a couple more ideas I would like to toss in.

My first idea is based on my suspicion that your underlying implementation would not change much depending on whether it is merging lists, monoids, or some other general datatype given a combining function. Thus, an alternative compromise might be the following:

  1. Write the implementation using something like the proposed semantics under the hood, since this is more general than using lists
  2. Expose the old semantics as the "standard" semantics that library writers will target, since it should be easy to build this on top of the general implementation
  3. Exposed the proposed semantics in a submodule so people like me can still use them

This would make ehird and everyone who prefers the old semantics happy because the semantics that they like would still be the "standard" semantics targeted by library writers, which is their strongest objection to the proposed semantics, while still giving people like me who are happy to make the tradeoff involved access to them. :-)

My second idea builds on your proposed compromise but adds a new type --- called, say, CalmEvent --- that can easily be converted to Event but that one can only get from Event by using something like unionWith or collect; it could even prove useful to create a typeclass so that collect and unionWith could produce either an Event or a CalmEvent from an Event. Though now that I think of it, this is essentially a variation of and possibly isomorphic to your idea of building the proposed semantics as a library on top of the current semantics. :-) This idea would only work if CalmEvent was opaque, of course, and it might only work if Event and Behavior were opaque too, which might be a fatal flaw if you were intending for them to be open.

@ehird
Copy link

ehird commented Jan 6, 2012

@HeinrichApfelmus:

Well, I don't really mind losing the Monoid instance, and unionWith seems benign to me, but I can't say I feel satisfied with the current semantics any more :)

I think infinitesimal delays are actually an elegant solution to this problem, but I agree that they're probably outside the scope of this issue. What do you think about opening a new design-discussion issue for infinitesimal events? I have a few ideas for a model that I think could work, although I'm sure you've thought about it more than me.

@gcross:

I'm not sure how I feel about half-way solutions that would require such a large code change. They might make things easier in the very short-term, but I'm not convinced we couldn't find a decent model for infinitesimal delays before a major release or two...

Of course, if there really is no elegant way to accomplish that, then such things are worthwhile :)

@gcross
Copy link

gcross commented Jan 6, 2012

ehird,

I am not sure what you mean by "such a large code change". Neither solution would require you to change any of your code. Heinrich was planning on rewriting the internals anyway, so no matter what he is going to be making a large code change. So the question is how much either of my proposals increase the amount of work he would have to do relative to that.

In the first case, since merging lists is not so different from accumulating a Monoid, I don't think that the latter would involve more work than the former, so he may as well use the latter (or more accurately something like unionWith) for the underlying implementation because it increases the generality which gives him freedom to play with different semantics in the future; the only additional work in that case would be having to then write a relatively thin layer on top of this to implement the current semantics, which is relatively easy to do and so should be a relatively small amount of work relative to what he had already done.

In the second case, it's just a matter of defining an opaque newtype wrapper and some functions in terms of it, which again is not a large code change compared to the rewrite of the implementation that Heinrich has to do anyway.

So in short, in neither case is there a large code change. The second case might be a "half-way" solution, but the first case is definitely not because it increases the generality of the library in a way that opens up potential options in the future.

@ehird
Copy link

ehird commented Jan 6, 2012

@gcross:

Ah, I meant external code — i.e. the use of CalmEvent changes a lot of code, and if infinitesimal delays are added later, then those changes would have to be undone. The multiple semantics compromise seems fine.

@gcross
Copy link

gcross commented Jan 6, 2012

ehird,

Oh, I see what you mean by "half-way" now; you mean "half-way" in the sense of not having delays. :-) The thing is, infinitesimal delays are orthogonal both with respect to our choice of semantics and to the kinds of problems that they solve.

In the case of the buttons, imagine that they really had been clicked simultaneously --- or at least, imagine some example isomorphic to that one with two events occuring simultaneously. :-) In that case, the last thing that you want is a delay because the correct value of the behavior is the one in which both counts have been updated simultaneously. For this, only something like unionWith or the proposed new semantics will help.

In the case I described with data moving between new behaviors, the proposed semantics do not do anything to help out that problem because they don't let you break an event cycle; only the ability to insert a delay lets you do that.

Thus, it is not "half-way" to figure out what we want to do with the semantics independently of what we want to do with infinitesimal delays.

@gcross
Copy link

gcross commented Jan 6, 2012

Ah, I meant external code — i.e. the use of CalmEvent changes a lot of code, and if infinitesimal delays are added later, then those changes would have to be undone. The multiple semantics compromise seems fine.

No external code would have to change --- that was the whole point of introducing a new type. :-)

And as I wrote before, the choice of semantics is orthogonal to the existence of infinitesimal delays, so if infinitesimal delays were added then that would have no effect on the relevence of CalmEvent.

@gcross
Copy link

gcross commented Jan 6, 2012

Actually, having thought about it, I do agree with you on one thing: there are some problems that would be more easily solved if one could take a stream of events and insert delays rather than having to combine them into a single CalmEvent, so for such problems the ability to insert delays could make life easier than merely having CalmEvent around. Nonetheless, neither solution alone solves all the problems faced presently, and in general I think the two changes should be treated orthogonally.

@gcross
Copy link

gcross commented Jan 6, 2012

The multiple semantics compromise seems fine.

Hehe, I am glad we can agree on one thing. :-)

@ehird
Copy link

ehird commented Jan 6, 2012

Right, I agree that unionWith is a good thing to have no matter what semantics we pick, and that the Monoid instance probably does more harm than good with the current semantics.

By "external code", I meant that anyone that wants to use CalmEvent will have to adapt their code to do so. I'm not sure I understand what you mean by CalmEvent being orthogonal to the semantics; if we remove simultaneous occurrences and add infinitesimal delays in their place, then CalmEvent is the same thing as Event, and so would only exist for backwards-compatibility.

Edit: I suppose the miscommunication is that I'm seeing "replace simultaneous occurrences with infinitesimal delays" as an atomic change to the semantics, and you see the two as orthogonal; probably because I consider "remove simultaneous occurrences" by itself unworkable :)

@gcross
Copy link

gcross commented Jan 6, 2012

ehird,

First, infinitesimal delays are not incompatible at all with multiple simultaneous occurrences, so the presence or absence of delays is completely orthogonal to the presence or absence of multiple simultaneous occurrences.

Second, I am not sure what exactly you are envisioning by "remove simultaneous occurrences and add infinitesimal delays in their place", but if you mean that (union a b) now adds a delay to b instead of concatenating the lists of occurrences in a and b then that is such a hugely backward-compatibility breaking change that we may as well just get rid of CalmEvent at the same time. :-) To see what I mean by this, suppose that people had been relying on the current semantics to build up a large set of multiple simultaneous occurrences that they then merged using unionWith. By changing the semantics in this way we have completely negated unionWith because now instead of taking all of the events, merging them together, and then possibly firing and event with all of their combined information, the events will instead be processed one at a time, possibly resulting in spurious events just like in the button example.

@ehird
Copy link

ehird commented Jan 6, 2012

First, infinitesimal delays are not incompatible at all with multiple simultaneous occurrences, so the presence or absence of delays is completely orthogonal to the presence or absence of multiple simultaneous occurrences.

Completely agreed — the reason I was treating them as a single change is because I see infinitesimal delays as the best way to deal with the problem of simultaneous occurrences.

I am not sure what exactly you are envisioning by "remove simultaneous occurrences and add infinitesimal delays in their place", but if you mean that (union a b) now adds a delay to b instead of concatenating the lists of occurrences in a and b then that is such a hugely backward-compatibility breaking change that we may as well just get rid of CalmEvent at the same time. :-)

Fair enough; I was just reasoning that one compatibility-breaking change is better than two, but it's unreasonable to do so without knowing if delays are workable or not.

By changing the semantics in this way we have completely negated unionWith because now instead of taking all of the events, merging them together, and then possibly firing and event with all of their combined information, the events will instead be processed one at a time, possibly resulting in spurious events just like in the button example.

Hmm. With delays, unionWith f a b would still merge simultaneous occurrences in a and b with f; for instance, consider bf <$> filterApply c a. I think you're saying that many uses of unionWithwill mostly be used in a manner that will not result in simultaneous occurrences in its arguments if make union insert delays to replace simultaneous occurrences. Am I right?

It's of course ridiculous to say that at most one Event fires at a single moment. What infinitesimal delays would allow is things like a general union without allowing simultaneous occurrences of a single Event.

@gcross
Copy link

gcross commented Jan 6, 2012

I think you're saying that many uses of unionWith will mostly be used in a manner that will not result in simultaneous occurrences in its arguments if [we] make union insert delays to replace simultaneous occurrences. Am I right?

I am having trouble parsing that sentence to figure what you mean, so I am not sure.

My point is as follows. Imagine that a and b are two complex events that have been built up by using union to merge lots of simpler events. If we change union to insert delays rather than to merge simultaneous occurrences then, while it is true that unionWith f a b will continue to merge simultaneous occurrences in a and b, the problem is that now the meaning of a and b themselves will have changed because they no longer contain all of the simultaneous occurrences we had designed them to contain! In this case, the only solution for users to get their code working again is for them to change a and b and all of their component events to have type Event [t] instead of Event t and to replace all of uses of union with unionWith (++). Mind you, on some level I am okay with this since it is basically the proposed semantics with all of the traits that I like about them but with a different definition of union, but if this is really the end game we are going for then why not start with the proposed semantics? :-)

It's of course ridiculous to say that at most one Event fires at a single moment. What infinitesimal delays would allow is things like a general union without allowing simultaneous occurrences of a single Event.

That is true, but it comes at the price of postponing information you might have needed at this instant to make the right decision about something into the future where you cannot access it or even know that it exists. Thus, while infinitesimal delays are useful for solving some problems, they do not provide a general solution for the problem of multiple simultaneous occurrences; you also need something like unionWith.

@ehird
Copy link

ehird commented Jan 6, 2012

Imagine that a and b are two complex events that have been built up by using union to merge lots of simpler events. If we change union to insert delays rather than to merge simultaneous occurrences then, while it is true that unionWith f a b will continue to merge simultaneous occurrences in a and b, the problem is that now the meaning of a and b themselves will have changed because they no longer contain all of the simultaneous occurrences we had designed them to contain!

I assumed that the primary intention of unionWith was to merge the values that have coinciding times in a and b. You're saying that the most common use will be to merge events that themselves have simultaneous occurrences, right?

I'm not so sure about that. For instance, in the (admittedly bad) button example, you might use unionWith as follows:

buttonClicked :: Event Button
buttonClicked = unionWith const aClicked bClicked

or, alternatively

buttonClicked :: Event [Button]
buttonClicked = unionWith (++) (pure <$> aClicked) (pure <$> bClicked)

None of these are handling simultaneous occurrences within one Event; they're just combining simultaneous occurrences of two Events, which seems like the compelling use-case to me.

I agree that using Event [a] is perfectly reasonable here; my desired semantics for buttonClicked were

buttonClicked :: Event Button
buttonClicked = union aClicked bClicked

under the infinitesimal delay definition of union — i.e. when A and B are clicked simultaneously, buttonClicked would be (t, A) : (t+ε, B) : ..., the idea being that many things might want to react and update state to buttonClicked, and Event [Button] would shift the "implementation detail" of what the sources of buttonClicked are to the consumer of the event.

Basically, I agree that Event [a] and unionWith (++) are useful and make a lot of sense to use, I just think that you lose a bit too much "compositionality" with it without also having delays. (Not that the current semantics is perfect here, either.)

That is true, but it comes at the price of postponing information you might have needed at this instant to make the right decision about something into the future where you cannot access it or even know that it exists.

Agreed; you sometimes have to wait for more information to be able to make decisions. This applies generally in FRP, I think.

Thus, while infinitesimal delays are useful for solving some problems, they do not provide a general solution for the problem of multiple simultaneous occurrences; you also need something like unionWith.

I think we're in violent agreement :)

I'm all for @HeinrichApfelmus' proposed change, and I now agree that simultaneous occurrences are bad news in general. Our only difference of opinion is that I think your proposed semantics are a little too drastic without also having infinitesimal delays; i.e. that adjusting the semantics would lose a bit too much for me to support it, but adjusting the semantics and adding infinitesimal delays would be better than the sum of its parts, and better than the current situation altogether.

@gcross
Copy link

gcross commented Jan 6, 2012

I assumed that the primary intention of unionWith was to merge the values that have coinciding times in a and b. You're saying that the most common use will be to merge events that themselves have simultaneous occurrences, right?

My point is only that it would be a really bad idea for us to keep union with its current semantics and then one day change it to have the semantics that you want because this would break a lot of code in an ugly way. In particular, in many cases this change would take code that had originally merged a bunch of simultaneous events together in an instant in order to make a decision based on all of them into code that picked out one event from the lot and made a decision based on only that one.

That doesn't mean that we should never introduce your combinator, only that we should either call it something else (like unionDelay) or not have any function called union at all until it has the semantics you want.

I'm all for @HeinrichApfelmus' proposed change, and I now agree that simultaneous occurrences are bad news in general. Our only difference of opinion is that I think your proposed semantics are a little too drastic without also having infinitesimal delays; i.e. that adjusting the semantics would lose a bit too much for me to support it, but adjusting the semantics and adding infinitesimal delays would be better than the sum of its parts, and better than the current situation altogether.

Okay, I see your point. I completely agree with you that both would be ideal, but sadly the semantics for infinitesimal delays are probably too much of a hornet's nest to handle for now, so most likely we'll have to come up with some other muddled compromise. :-)

@ehird
Copy link

ehird commented Jan 6, 2012

That doesn't mean that we should never introduce your combinator, only that we should either call it something else (like unionDelay) or not have any function called union at all until it has the semantics you want.

Fair enough. I don't entirely agree, since breaking compatibility is what major version changes are for, but it's a reasonable point.

I would prefer to rename union now rather than giving it a name like unionDelay later, since it would make sense to remove simultaneous events simultaneously1 with adding infinitesimal delays, and so there would be no union to be analogous to, but that would be a much more breaking change than the restriction of the Monoid instance, so it's probably not practical.

Okay, I see your point. I completely agree with you that both would be ideal, but sadly the semantics for infinitesimal delays are probably too much of a hornet's nest to handle for now, so most likely we'll have to come up with some other muddled compromise. :-)

I've opened issue #27 so it can be discussed separately from this one :)

1 ;)

@HeinrichApfelmus
Copy link
Owner Author

Thanks a lot for the discussion, Elliott and Gregory!

I've detailed the compromise we achieved on the wiki.

@gcross
Copy link

gcross commented Jan 7, 2012

That compromise seems to address all of my major concerns. :-)

@HeinrichApfelmus
Copy link
Owner Author

Quick update: points 1 and 2 of the compromise have been implemented in the current development version of reactive-banana 0.5. I'm currently updating the example code, everything seems to work like a charm.

@gcross
Copy link

gcross commented Mar 20, 2012

Great! Thanks for the heads up. :-)

  • Greg

On 03/21/2012 01:04 AM, Heinrich Apfelmus wrote:

Quick update: points 1 and 2 of the compromise have been implemented in the current development version of reactive-banana 0.5. I'm currently updating the example code, everything seems to work like a charm.


Reply to this email directly or view it on GitHub:
#25 (comment)

@ehird
Copy link

ehird commented Mar 21, 2012

For what it's worth, I'm not particularly attached to the Monoid instance for Event (a -> a) any more; I think I'd prefer the uniformity of simply not having any Monoid instance over only having it for certain types.

However, I'm not sure I still understand the benefit CalmEvent gives you. Wouldn't simply using calm on any external events you get in have the same effect? If keeping track of these is a problem, then wouldn't

newtype CalmEvent a = CalmEvent { calmedEvent :: Event a }

calmEvent :: Event a -> CalmEvent a
calmEvent = CalmEvent . calm

suffice? Admittedly, it's a very small thing to include, but IMO, it seems odd to include one of the many possible "taintings" of Event you could want.

It's great to see work on reactive-banana progressing :)

@HeinrichApfelmus
Copy link
Owner Author

@ehird: The Monoid instance is mainly for backwards compatibility. And yes, it's basically just CalmEvent as you mention. We'll see how it works out.

That's all, folks, the compromise has been implemented! But feel free to discuss further (or reopen).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants