archives

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?