When Spec
Callbacks and eventing are an inherent part of asynchronous or multi-threaded programming, which are both themselves inherent to most programs today.
Herein follows a proposal for a mediator model that is baked in to the language:
- Any property may be observed, as long as it is available in the same scope as the
whenconstruct that refers to it - Invoking the callback should be done implicitly by the run time, NOT explicitly by user code
- Eventing must support static optimization to minimize performance overhead
Basic Idea
The when keyword syntax is structured the same as a common if expression:
when(predicate)
{
...statements
}
When the predicate expression (aka. the 'event') resolves to true, the statements within the attached block scope are executed.
The block scope creates a closure within which local variables from the outer scope can be referred to beyond their natural scope.
Example
public class Player {
final int MIN_HEALTH_VALUE = 0;
final int MAX_HEALTH_VALUE = 100;
private int health;
public Player(GameWorld gameWorld)
{
when(this.health <= MIN_HEALTH_VALUE && gameWorld.getEnemyCount() > 0)
{
gameWorld.gameOver();
}
}
public void setHealth(int value)
{
this.health = value;
}
public void applyPowerUp(PowerUp powerUp)
{
...
when(powerUp.getEnergyLevel() < 10)
{
System.out.println(powerUp.getName() + " running low");
this.applyBoost(powerUp);
}
...
}
private void applyBoost(PowerUp powerUp)
{
...
}
...
}
- We can subscribe to events in the constructor or at an abritrary point in the code
- The closure allows us to refer to
gameWorldlong after the constructor routine finishes
Validation
The compiler statically asserts that the when construct is valid by:
- Checking all symbols within are available in scope
- The predicate evaluates to a boolean (type coercion optional dependant on language semantics)
Nesting
One could even nest when constructs:
when(predicateA)
{
when(predicateB)
{
...statements
}
}
Which would mean, I only care about the predicate B being true when predicate A is presently true also.
Moreover, if there were no other statements inside the outermost when, you could combine both predicate A and predicate B into a single when statement:
when(predicateA && predicateB)
{
...statements
}
Notice how this again feels consistent with if semantics.
Unbinding
You might want to unbind or cancel the subscription at some point, ie. not have it fire multiple times over the program's lifetime.
This could be as simple as using the break keyword, just like we do in loops or switch cases:
when(...)
{
...
break;
}
At this point the reference counts for all encapsulated variables would be decremented, allowing their memory to be reclaimed at some time thereafter without leaks.
Whether break is the correct keyword semantically is a triviality, the idea is the important part!
Composition
One could even compose an example of nested when blocks with a conditional unbinding.
That is, the outer when is unbound only when it AND the inner when are true!
The unbinding of the outer when implicitly unbinds the inner when as a result.
This would follow the semantics of breaking out of nested loops (if the language supported it):
outerwhen:
when(...)
{
when(...)
{
break outerwhen;
}
...
}
Suppression
In some scenarios you may wish to change variable state without triggering any callbacks:
suppress {
this.health = MAX_HEALTH_VALUE; // no events fired
}
Statements executed inside a suppress block are guaranteed not to trigger any events.
Optimizations
The most naïve implementation would be to evaluate all registered when subscriptions whenever any live variable in the system changed.
This of course would result in massive overhead, and we have enough information statically to do better:
Dependencies
One key part of optimization comes from knowing the when predicate's dependencies.
The type and origin of these dependencies can be known ahead-of-time. For example:
public Player(GameWorld gameWorld)
{
when(this.health <= MIN_HEALTH_VALUE && gameWorld.getEnemyCount() > 0)
{
gameWorld.gameOver();
}
}
The compiler knows this when construct depends on:
- A
Playerinstance variable of typeint - An input parameter of type
GameWorld
From this information it could:
- Reason about the requisite memory needed to store this
whenconstruct compactly - Allocate said memory within the locality of the dependent variables
- Only bind observers to these two variables, and only evaluate the predicate when the state of either changes
Lazy Evaluation
Nested when constructs should only be evaluated when the enclosing (parent) when block is true
This is an extension of short-circuit evaluation, that would be performed on any predicate implicitly.
Moreover, if multiple observed variables are mutated sequentially, the compiler should perform the when checking after the last mutate statement has been executed.
Invocations in Predicates
Predicates may contain one or many invocations:
when(player.calculateBoundingBox().isOutside(LEVEL_BOUNDS))
{
...
}
The expression will be evaluated N times during the program's lifetime, whereby N is the sum of the number of times the state of each dependency changes.
In a non-pure language (ie. a system with state) it would be the programmer's responsibility to ensure the continual reevaluation of the predicate does not cause unwanted side effects.
Moreover, the programmer should ensure the predicate is not overly expensive to compute, or it should be wrapped in a guarding when that throttles the number of times the expensive expression needs to be evaluated.
Closing Remarks
This is just a quick exploration in to a feature I would love to see in languages. If you know of similar mechanisms let me know!
This is NOT the same as the .NET eventing model, as the programmer still needs to manage those events and explicitly trigger them.
Moreover, that implementation is just syntactic sugar and not something that is optimized in the ways described above.
Oh and, RFC!
D. Ho (@ComethTheNerd)