As written, a "scope" is a function, s: G² → None + T, from the current global state and the previous global state to an optional value of some type T. Meanwhile, a "propagator" is a function, p: G² × A × B → B, from the current global state, the previous global state, a source node, and a target node to a new target node, such that p(g, g', a, b) = if s(g, g') is None then b else f(s(g, g'), a, b) for some f: T × A × B → B.<p>For example tick(g, g') = time(g') - time(g), and change(g, g') = if node_s(g') ≠ node_s(g) then () else None where node_s(g) gets the source node of the change scope from global state g.<p>In practice the outputs of a scope can be computed once per state change and called an event, rather than computed on demand each time it's used, and scopes which return None don't need to be propagated. Also, I suspect it would be useful to restrict scopes to (A × B)² → None + T or even just A² → None + T, so that events are limited to propagating along chains of edges.