-
Notifications
You must be signed in to change notification settings - Fork 71
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
Comments
I don't like this. For starters, 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 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.) |
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 ehird proposes 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 |
Are you saying you never use 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 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 |
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 mean that it is not clear at all what the coder had intended for that event to mean, not that it has unclear semantics.
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 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. |
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 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. |
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.
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 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 :) |
Of course that sentiment goes both ways... |
Of course :) That's why I hope we can come up with a solution that satisfies everybody. |
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.
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.
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.
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.
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. |
Sorry, I intended to write "external inputs can't really be simultaneous". I'll reply to the rest of your comments later on.
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 :) |
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.
That is certainly fair enough. :-) |
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. |
AccumulationConcerning the accumulation functions, the intention is, of course, to keep them as they are. I didn't think that
then, or anything else to make it pleasant. This is the part I'd like to stay the same. SimultaneityElliott 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
Now, we want to emit a message whenever one button has been clicked more often than the other
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:
|
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 Second, in the example you provided it all we need is something like the (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. :-) ) |
It is also worth noting, though, that the 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 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. |
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. |
Observing simultaneous eventsI usually call the combinators you mention
In one direction, we have
but it turns out that it's also possible to define I think we can all agree that these combinators need to be added to the current semantics. Alternate semantics as a libraryActually, 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
but we can simply hide the
Assuming that no external event fires multiple simultaneous events, the 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
Tentative ConclusionWow, I like that, I didn't think it would be so easy. The idea is simply to forbid yourself to use the
instead, I think we will get the best of both worlds, a general |
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 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 Edit: Actually, solving that issue is simple: delay :: Event a -> Event a
delay = map (\(t,x) -> (t+ε,x)) |
Heinrich, Merely hiding Furthermore, if event loops are typically written using So I wouldn't call this the best of both worlds, though I suppose it could be a decent compromise. :-) |
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. |
Yes, 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. |
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. |
Sorry, that model is totally wrong. I think it has to be something like type Event a = [Maybe a] where |
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 Concerning this issue here, would you both agree with the following compromise
|
So, just to make sure I understand, |
Yes. The definition of
For instance
|
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:
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, |
Well, I don't really mind losing the 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. 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 :) |
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 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. |
Ah, I meant external code — i.e. the use of |
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 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. |
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 |
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 |
Hehe, I am glad we can agree on one thing. :-) |
Right, I agree that By "external code", I meant that anyone that wants to use 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 :) |
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 |
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.
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.
Hmm. With delays, It's of course ridiculous to say that at most one |
I am having trouble parsing that sentence to figure what you mean, so I am not sure. My point is as follows. Imagine that
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 |
I assumed that the primary intention of I'm not so sure about that. For instance, in the (admittedly bad) button example, you might use 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 I agree that using buttonClicked :: Event Button
buttonClicked = union aClicked bClicked under the infinitesimal delay definition of Basically, I agree that
Agreed; you sometimes have to wait for more information to be able to make decisions. This applies generally in FRP, I think.
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. |
My point is only that it would be a really bad idea for us to keep That doesn't mean that we should never introduce your combinator, only that we should either call it something else (like
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. :-) |
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
I've opened issue #27 so it can be discussed separately from this one :) 1 ;) |
Thanks a lot for the discussion, Elliott and Gregory! I've detailed the compromise we achieved on the wiki. |
That compromise seems to address all of my major concerns. :-) |
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. |
Great! Thanks for the heads up. :-)
On 03/21/2012 01:04 AM, Heinrich Apfelmus wrote:
|
For what it's worth, I'm not particularly attached to the However, I'm not sure I still understand the benefit 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 It's great to see work on reactive-banana progressing :) |
@ehird: The That's all, folks, the compromise has been implemented! But feel free to discuss further (or reopen). |
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:
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 toInstead of appending simultaneous events, the
union
function would merge them into a single event occurrence, using themappend
combinator.Properties
Since
a -> a
is a monoid, this works well in conjunction with the accumulation functionsaccumE
,accumB
. Existing code is essentially unchanged in this regard, except for ordering.Merging events in other cases will be a little less pleasant, we probably need a left-biased union
The drawback is that event occurrences may be lost this way.
The following two behaviors are now equivalent
In the presence of simultaneous event occurrences, these two programs are not equivalent because the event
e
may contain multiple occurrences.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.
The problem about optimizing the
Discrete
type (issue Optimize Discrete data type by calming simultaneous events #19) disappears.The old semantics can always be obtained as
Event [a]
if desired. In fact, that's how the model implementation defines the current semantics. Thecalm
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?
The text was updated successfully, but these errors were encountered: