Pragmatic declarative event abstraction

My recent exploration of Rx has gotten me thinking again about events. Ever since I read Conal's original Fran paper, I've been fascinated and confused by the relationship between discrete streams of events vs. continuous behaviors. Although they can be wrapped up in one abstraction (signals), this unification leads to pragmatic if not semantic mismatches resulting from the difference between discrete and continuous.

Bling, my current language effort, provides very concise syntax for creating continuous relationships between properties; e.g., Matrix.Bind = Matrix4Bl.MakeScale(.8d) * Rotate would bind a matrix property to a rotation matrix scaled down 80%. The programmer doesn't need to create a value converter, populate a binding object, or even express a closure. On the other hand, the programmer has to manipulate values indirectly because they are "lifted." This is problematic when we need to change a property's value discretely. Consider updating a 3D quaternion rotation via a virtual trackball. The idea is to take a 2D mouse position and convert it into a 3D sphere position:

var Mouse2D = sphere.Mouse.Position / sphere.Size;
Mouse2D = (Mouse2D * 2d - 1d);
var Mouse3D = MouseAt.AddZ((1d - Mouse2D.LengthSquared).Max(0d).Sqrt).Normalize;

Now, we want to update the Rotate property, and for efficiency reasons, we want to generate code that does this update:

var Rotation0 = this.Temporary(QuaternionBl.Identity);
var Mouse3D0 = this.Temporary(Double3Bl.Zero);
Action Begin0 = Rotation0.MkAssign(Rotation);
Action Begin1 = Mouse3D0.MkAssign(Mouse3D);
Action Update = Rotation.MkAssign(Mouse3D.AngleBetween(Mouse3D0) * Rotation0);

Rotation0 and Mouse3D0 are explicit temporary properties that remember the 3D mouse position and initial rotation when a rotation begins. The update then takes the current 3D mouse position, finds its angle with the remembered 3D mouse position, and prepends that to the remembered quaternion rotation. Finally, these actions are used in a drag state machine subscription:

DragBegin.Subscribe(() => { Begin0(); Begin1(); });
DragUpdate.Subscribe(() => Update());

This was the typical way to handle events in Bling, where a lot behavior occurs by updating and manipulating UI properties. This solution was sub-optimal for the following reasons:

  • Update actions and subscriptions are separate steps.
  • Explicit temporaries are ugly, and we still have to create update actions and subscriptions for these temporaries.

Then I realized we could treat events themselves as indices of "time" into a lifted values, much like an integer is an index of space into an array. So a[e] represents the value of expression a when event e fires, in essence e is like a continuation. This is how things work in Rx; e.g.,

from mouseLeftButtonDown in this.GetMouseLeftButtonDown()
let rotation0 = this.Rotate.CurrentValue
let position0 = Mouse.GetPosition(this)
...

where "let" binds in the context of mouseLeftButtonDown event/continuation. So I thought, that's essentially what I'm writing so much code to do anyways, so let's do that in Bling. Also, we can flip the syntax to also control assignment: a[e] = ... would cause a value to be pushed into the a expression when e fires. Redoing my virtual trackball example in the new syntax:

Rotate[outer.Mouse.Drag.Update] =
  Mouse3D.AngleBetween(Mouse3D[outer.Mouse.Drag.Enter]) * Rotate[outer.Mouse.Drag.Enter];

No temporaries, updates, or subscriptions, just an assignment: so much better! An artifact of my approach is that events can't directly carry data payloads; they are just points in time. However, I've solved this problem (for now) by making event data available via dynamic context.

Done before? Crazy language idea?

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.

Two perspectives. First,

Two perspectives. First, beyond syntax, I'm not sure how this is different from cells and snapshots. Maybe a threading of when to snapshot? Understanding the distinction might help tease out what's going on.

Second, at a more fundamental level, I've felt this way for awhile: whenever there is a variable ("int foo = ..." in C, "var x = ..." in JavaScript, etc.) or field, I think it should be treatable as a cell. Heck, we translate these to first-class ref types in type systems already! Being able to inject streams of values into it and react to its stream of changes would be a way to simplify transitioning away from overly imperative code.

Edit: perhaps if something like the clock calculus was used, this could lead to an interesting approach. E.g., even with multiple assignments, you can ensure no double assignments in the same timestep.

Its probably very related to

Its probably very related to cells and snapshots, but I believe the syntax is new. As for clocks, there are definitely issues that have unclear interpretation, consider:

b[e] = ... a ...;
a[e] = ... b ...;

There is an implicit assumption that the a[e] assignment occurs after a is used in the b[e] assignment, even though there is no strong reasoning behind this assumption. I've written some code in this style that is very questionable:

BoolBl StartIsX = CutStart.X gt 0 & CutStart.X lt 1;
DoubleBl StartRatio = CutStart.Access[StartIsX.Condition(0, 1)];

Double2Bl OldCutStart = CutStart[CutRect.Mouse.Drag.Exit];
Double2Bl NewCutStart = (StartIsX[CutRect.Mouse.Drag.Exit]).Condition(
  new Double2Bl(CutSlide, OldCutStart.Y),
  new Double2Bl(OldCutStart.X, CutSlide));

CutSlide[CutRect.Mouse.Drag.Exit] = StartRatio;
CutStart[CutRect.Mouse.Drag.Exit].Bind = NewCutStart;

The point of this code is to data bind CutStart at the end of a drag to make it related to CutSlide (which is the value of a slider), but CutSlide's initial value after the drag depends on CutStart's! The code is careful not to modify CutStart's current value, but just make it so we can modify its value with the slider in the future.

The ordering of the last two statements is then crucial to the correctness of this code, which probably breaks some kind of semantic purity property. My sole consolation is that the original code that this replaced was even crazier.

Overly Imperative

Being able to inject streams of values into it and react to its stream of changes would be a way to simplify transitioning away from overly imperative code.

This approach feels even more imperative than imperative programming. Any attempt to change any variable can lead to a cascading and possibly cyclic set of side-effects, which one can't determine locally, since they may be defined dynamically anywhere else in the program.

Because the properties

Because the properties involved are data-bound WPF dependency properties, any change made via an event would automatically propagate to all clients regardless. The presence of declarative databinding in an imperative language already makes things a bit strange. What is added here is a concise syntax to influence declarative relationships when certain events occur; e.g., you rotate your virtual sphere using the mouse in a 3D simulation, the rotate property has to be updated somehow.

Clarification Request

Using events as indices in time is a very interesting idea. I certainly favor treating time as a series of events. I'll probably spend a week pondering this before I have a real response.

Just a simple question: Does outer.Mouse.Drag.Enter in the above code refer somehow to the last time the the Enter event fired?

Yes, my current

Yes, my current implementation just captures the evaluation when the event fires. If Enter fires multiple times before an update occurs (which won't happen in this example), then you'd miss some values. One could imagine more aggressive checking to make sure that the events were properly nested somehow (i.e., in a state machine).

Random comments

It sounds like 'outer.Mouse.Drag.Update' isn't really an event, but is more of an event class (it can fire multiple times). If you had unique events and some way to traverse between related events (like from Update[i] to Enter[i]) it seems like this would be on better semantic footing.

Offhand, it looks like a nightmare to figure out when you can collect past data. Is that not a problem?

On a different track, can you not do this by specifying/activating a drag behavior on Enter that closes over the starting angle?

My personal viewpoint is that time should be modeled as discrete passing events, but shouldn't be indexable. The past is too big to randomly access and the future is even more problematic.

It isn't necessarily

It isn't necessarily randomly accessed -- see my comment about clocks. Most of the reactive programming discussed on LtU doesn't allow fine-grained reasoning about when events occur, but that does not make it inherent.

Outer.Mouse.Drag.Update is

It sounds like 'outer.Mouse.Drag.Update' isn't really an event, but is more of an event class

Outer.Mouse.Drag.Update is an event stream, which is an important distinction, meaning it will potentially fire forever. Actually, its even richer than that, outer.Mouse.Drag is a state in a drag/hold/click state machine: the Enter event stream fires when you enter the state while the Update event stream fires when there is a self transition (ya, there is an Exit event stream also...). But I can't figure out how to expose the state abstraction by itself, so its just decomposed into multiple event streams.

Offhand, it looks like a nightmare to figure out when you can collect past data. Is that not a problem?

Past data is collected whenever the event stream fires, so for a[e] you'll get the value of a when e last fired. This means you can't reason about an evaluation from before the last event fired, which would rather require a state machine.

On a different track, can you not do this by specifying/activating a drag behavior on Enter that closes over the starting angle?

Sort of. The code for that is:

Rotate[outer.Mouse.Drag.Enter].Bind =
  Mouse3D.AngleBetween(Mouse3D[outer.Mouse.Drag.Enter]) * Rotate[outer.Mouse.Drag.Enter];  

Notice the bind, so it activates a data binding relationship when the drag state is entered. Because the same event is being referred to, we could just use frozen instead on the expressions we don't want to change during the bind:

Rotate[outer.Mouse.Drag.Enter].Bind =
  Mouse3D.AngleBetween(Mouse3D.Frozen) * Rotate.Frozen;  

This is not that elegant because we need another line of code to stop the data binding relationship when we stop dragging:

Rotate[outer.Mouse.Drag.Exit].Freeze();  

I guess we could allow states to index a bind assignment, meaning the binding is only active while we are in the state, and the value of the expression is frozen after we leave the state:

Rotate[outer.Mouse.Drag].Bind =
  Mouse3D.AngleBetween(Mouse3D.Frozen) * Rotate.Frozen;  

This syntax might be more elegant for some cases.

My personal viewpoint is that time should be modeled as discrete passing events, but shouldn't be indexable. The past is too big to randomly access and the future is even more problematic.

Possibly. Right now I'm replacing verbatim event handling code that I had before and was more ugly, so say I can replace 5 adjacent lines of code with 1 line of code. I haven't yet thought about how this syntax could be abused if its used in less local ways.

An artifact of my approach

An artifact of my approach is that events can't directly carry data payloads; they are just points in time. However, I've solved this problem (for now) by making event data available via dynamic context.

Can you elaborate on the "dynamic context"?

Here is code that adjusts a

Here is code that adjusts a zoom level using the mouse wheel:

Zoom[OverlayContent.Mouse.Wheel] =
 (Zoom + ((DoubleBl) OverlayContent.Mouse.Wheel.Delta) / 1200d).Clamp(.1, 10);

The Delta member of a wheel event stream only has a meaningful evaluation when a mouse wheel event is being handled. So we say that "Delta" is scoped within the dynamic context of wheel event, and we can't just do:

Console.WriteLine(OverlayContent.Mouse.Wheel.Delta.CurrentValue);

which will throw an exception (static checking could be possible but not in the context of C#). Implementation-wise, a thread local static is used to capture the Delta value. This is fairly simple, dynamic context becomes more complicated when we start considering tracking states rather than events; e.g., for a key down state, we have to track what key is being pressed throughout the entire state.