state v0.2.0
State.js
State is a framework for implementing state-driven behavior directly into JavaScript objects.
function Person () {}
state( Person.prototype, {
Formal: state( 'initial', {
greet: function () { return "How do you do?"; }
}),
Casual: {
greet: function () { return "Hi!"; }
}
});
var person = new Person;
person.greet(); // >>> "How do you do?"
person.state('-> Casual');
person.greet(); // >>> "Hi!"
The lone dependency of State is Omicron.
State can be installed via npm:
$ npm install state
var state = require('state');
or included in the browser:
<script src="omicron.js"></script>
<script src="state.js"></script>
which will expose the module at window.state
(this can be reclaimed with a call to state.noConflict()
).
Step 0 — The state
function
The State module is exported as a function called state
, which can be used in either of two ways:
state( [attributes], expression )
- Given a single
expression
object,state
will create and return a state expression, based on the contents ofexpression
(and any keywords included in the optionalattributes
string).
state( owner, [attributes], expression )
- Given two object-typed arguments,
state
will augment theowner
object with its own working state implementation based on the providedexpression
(andattributes
), and then return the newly stateful object’s initial state.
Step 1 — Building a state expression
The state
function’s expression
argument, usually an object literal, describes the constituent states, methods, and other features that will form the state implementation of owner
:
var owner = {
aMethod: function () { return "default"; }
};
state( owner, {
aState: {
aMethod: function () { return "stateful!"; }
}
});
Step 2 — Accessing an object’s state
After calling state
to implement state into an owner
object, this new state implementation will be exposed through an accessor method, also named state
, that will be added to the object.
Calling this accessor with no arguments queries the object for its current state:
owner.state(); // >>> State '' (the top-level *root state*)
Step 3 — Transitioning between states
The object’s current state may be reassigned to a different state by calling its change
method and providing it the name of a state to be targeted. Transitioning between states allows an object to exhibit different behaviors:
owner.state(); // >>> State ''
owner.aMethod(); // >>> "default"
owner.state().change('aState');
owner.state(); // >>> State 'aState'
owner.aMethod(); // >>> "stateful!"
In addition, a sugary alternative to calling change()
is to prepend a transition arrow to the targeted state, and pass this expression into the accessor method:
owner.state('-> aState');
All together now …
With these tools we can model a simple yet thoroughly polite person
, like that shown in the introductory example, who will behave appropriately according to the state we give it:
Note: from this point forward, example code will first be presented in hand-rolled JavaScript, and then followed by a logically equivalent bit of CoffeeScript. Please freely follow or ignore either according to taste.
var person = {
greet: function () { return "Hello."; }
};
state( person, {
Formal: {
greet: function () { return "How do you do?"; }
},
Casual: {
greet: function () { return "Hi!"; }
}
});
person.greet();
// >>> "Hello."
person.state('-> Formal');
person.greet();
// >>> "How do you do?"
person.state().go('Casual'); // [1]
person.greet();
// >>> "Hi!"
person.state('->'); // [2]
person.greet();
// >>> "Hello."
person = greet: -> "Hello."
state person,
Formal:
greet: -> "How do you do?"
Casual:
greet: -> "Hi!"
person.greet()
# >>> "Hello."
person.state.be 'Formal' # [1]
person.greet()
# >>> "How do you do?"
person.state -> 'Casual' # [3]
person.greet()
# >>> "Hi!"
person.state -> '' # [3]
person.greet()
# >>> "Hello."
The
change
method is also aliased togo
andbe
.A naked transition arrow is simply a
change
to the object’s default, or root state.Another option is to use a function literal, which mimics the transition arrow; the function is immediately invoked and its return value is passed to the current state’s
change
method.
Return to: Getting started < top
Before diving in further it may be helpful to gain a broad, high-level view of the concepts involved in State. To that end, the points below offer previews of the more in-depth discussions upcoming in the following section.
States — Formally, a state is an instance of
State
that encapsulates all or part of an owner object’s condition at a given moment. The owner may adopt different behaviors at various times by transitioning from one of its states to another.Expressions — A state expression describes the contents of a
State
. States may be expressed concisely with an object literal, which, along with an optional set of attribute keywords, can be passed into thestate()
function. There the provided input is interpreted into a formalStateExpression
, which can then be used to create aState
instance.Inheritance — States are arranged hierarchically in a rooted tree structure: the owner object is given exactly one root state, within which may be nested zero or more substates, which may themselves contain further substates, and so on, thereby expressing specificity of the owner’s behavior. A state inherits from its superstate, with which it shares the same owner, and also inherits from any protostate, defined as the equivalently positioned state within a prototype of the owner object. Protostates have a higher inheriting precedence than superstates.
Selectors — A stateful owner
object
’s accessor method atobject.state()
can be called without arguments to retrieve the object’s current state, or, if provided a selector string, to query for a specificState
of the object, or a specific set of states.Attributes — A state expression may include a set of attribute keywords (e.g.:
mutable
,initial
,conclusive
,abstract
, etc.), which will enable certain features or impose certain constraints for theState
that the expression is to represent.Data — Arbitrary data can be attached to each state, and inherited accordingly through protostates and superstates.
Methods — Behavior is modeled by defining state methods that opaquely override the object’s methods. Consumers of the object simply call its methods as usual, and need not be aware of the object’s current state, or even that a concept of state exists at all. State methods are invoked in the context of the state in which the method is defined, allowing for polymorphic features like invoking the overridden methods of a superstate.
Transitions — When an object is directed to change from one state to another, it does so by temporarily entering into a transition state. A state expression may include transition expressions that describe, given a specific pairing of origin and target states, a synchronous or asynchronous action to be performed over the duration of the transition.
Events — Listeners for specific event types can be bound to a state, which will be called in the context of the bound state as it is affected by a progressing transition (
depart
,exit
,enter
,arrive
), as the state itself changes (mutate
), or upon the state’s construction or destruction (construct
,destroy
). State also allows for custom typed events, which can be emitted from a particular state and propagated to listeners bound to the state itself as well as its protostates and superstates.Guards may be applied to a state to govern its viability as a transition target, dependent on the outgoing state and any other conditions that may be defined. Likewise guards may also be included in transition expressions, where they are used to select a particular transition to execute. Guards are evaluated as predicates if supplied as functions, or as boolean values otherwise.
A state expression defines the contents and structure of a State
instance. A StateExpression
object can be created using the exported state()
function, and providing it a plain object map, optionally preceded by a string of whitespace-delimited attributes to be applied to the expressed state.
The contents of a state expression decompose into six categories: data
, methods
, events
, guards
, states
, and transitions
. The object map supplied to the state()
call can be categorized accordingly, or alternatively it may be pared down to a more convenient shorthand, either of which will be interpreted into a formal StateExpression
.
Building upon the introductory example above, we could write a state expression that consists of states, methods, and events, looking something like this:
var longformExpression = state({
methods: {
greet: function () { return "Hello."; }
},
states: {
Formal: {
methods: {
greet: function () { return "How do you do?"; }
},
events: {
enter: function () { this.owner().wearTux(); }
}
},
Informal: {
methods: {
greet: function () { return "Hi!"; }
},
events: {
enter: function () { this.owner().wearJeans(); }
}
}
}
});
longformExpression = state
methods:
greet: -> "Hello."
states:
Formal:
methods:
greet: -> "How do you do?"
events:
enter: -> do @owner().wearTux
Informal:
methods:
greet: -> "Hi!"
events:
enter: -> do @owner().wearJeans
Explicitly categorizing each element is unambiguous, but also unnecessarily verbose. To that point, state()
also accepts a more concise expression format, which, using a fixed set of rules, is interpreted into a StateExpression
identical to that of the example above:
var shorthandExpression = state({
greet: function () { return "Hello."; },
Formal: {
enter: function () { this.owner().wearTux(); },
greet: function () { return "How do you do?"; }
},
Informal: {
enter: function () { this.owner().wearJeans(); },
greet: function () { return "Hi!"; }
}
});
shorthandExpression = state
greet: -> "Hello."
Formal:
enter: -> do @owner().wearTux
greet: -> "How do you do?"
Informal:
enter: -> do @owner().wearJeans
greet: -> "Hi!"
Expression input provided to state()
is interpreted according to the following rules:
If an entry’s value is a typed
StateExpression
orTransitionExpression
, interpret it as a substate or transition expression, respectively.Otherwise, if an entry’s key is a category name, its value must be either
null
or an object to be interpreted as longform.Otherwise, if an entry’s key matches a built-in event type or if its value is a string, then interpret the value as either an event listener function, an array of event listeners, or a named transition target to be bound to that event type.
Otherwise, if an entry’s key matches a guard action (i.e.,
admit
,release
), interpret the value as a guard condition (or array of guard conditions).Otherwise, if an entry’s value is an object, interpret it as a substate whose name is the entry’s key, or if the entry’s value is a function, interpret it as a method whose name is the entry’s key.
Return to: Expressions < Concepts < Overview < top
The state model is a classic tree structure: any state may serve as a superstate of one or more substates, which express further specificity of their owner’s behavior and condition.
For every stateful object, a single root state is automatically generated, which is the top-level superstate of all other states. The root state’s name is always and uniquely the empty string ''
. Either an empty-string selector or naked transition arrow may be used to change an object’s current state to the root state, causing the object to exhibit the its default behavior.
obj.state().root() === obj.state(''); // >>> true
obj.state('->'); // >>> State ''
obj.state().root() is obj.state '' # >>> true
obj.state '->' # >>> State ''
The root state also acts as the default method store for the object’s state implementation, containing any methods originally defined on the object itself, for which now exist one or more stateful reimplementations elsewhere within the state tree. This capacity allows the method delegation pattern to work simply by forwarding a method call made on the object to the object’s current state, with the assurance that the call will be resolved somewhere in the state tree: if a method override is not present on the current state, then the call is forwarded on to its superstate, and so on as necessary, until as a last resort State will resolve the call using the original implementation held within the root state.
See also: Delegator methods
Substates help to express ever greater specificity of their owner’s behavior and condition.
function Person () {
this.give = function ( to, what ) {
to.receive( this, what );
return this;
};
this.receive = function ( from, what ) { return this; };
this.greet = function () { return "Hello."; };
state( this, {
Formal: {
greet: function ( other ) { return "How do you do?"; }
},
Informal: {
greet: function ( acquaintance ) { return "Hi!"; },
Familiar: {
hug: function ( friend ) {
this.owner().give( friend, 'O' );
return this;
},
greet: function ( friend ) {
this.owner().hug( friend );
},
Intimate: {
kiss: function ( spouse ) {
this.owner().give( spouse, 'X' );
return this;
},
greet: function ( spouse ) {
this.superstate().call( 'greet', spouse );
this.owner().kiss( spouse );
}
}
}
}
});
}
class Person
constructor: ->
@give = ( to, what ) -> to.receive this, what; this
@receive = ( from, what ) -> this
@greet = -> "Hello."
state this,
Formal:
greet: ( other ) -> "How do you do?"
Informal:
greet: ( acquaintance ) -> "Hi!"
Familiar:
hug: ( friend ) -> @owner().give friend, 'O' ; this
greet: ( friend ) -> @owner().hug friend
Intimate:
kiss: ( spouse ) -> @owner().give spouse, 'X' ; this
greet: ( spouse ) ->
@superstate().call 'greet', spouse
@owner().kiss spouse
Since the opening introductory code sample, the examples we’ve looked at have created stateful objects by applying the state()
function directly to the object. Let’s consider now the case of an object that inherits from a stateful prototype.
function Person () {}
Person.prototype.greet = function () { return "Hello."; };
state( Person.prototype, {
Formal: {
greet: function () { return "How do you do?"; }
},
Casual: {
greet: function () { return "Hi!"; }
}
});
var person = new Person;
class Person
greet: -> "Hello."
state @::,
Formal:
greet: -> "How do you do?"
Casual:
greet: -> "Hi!"
person = new Person
Since the person
object in the code above inherits from Person.prototype
, given what’s been covered to this point, it may be expected that a transition instigated using person.state().change('Formal')
would actually take effect on Person.prototype
, in turn affecting all other instances of Person
as well. Sharing stateful behavior through prototypes is desirable, however, it is also essential that each instance be able to maintain state and undergo changes to its state independently.
To that end, State automatically outfits each inheriting object with its own state implementation whenever one is necessary but does not exist already. This new implementation will itself be empty, but will inherit from the state implementation of the prototype, thus allowing the object to experience its own states and transitions without also indirectly affecting all of its fellow inheritors.
Person.prototype.state(); // >>> State ''
'state' in person; // >>> true
person.hasOwnProperty('state'); // >>> false
person.state(); // >>> State ''
person.hasOwnProperty('state'); // >>> true
person.state().isVirtual(); // >>> false
person.greet(); // >>> "Hello."
person.state('-> Casual'); // >>> State 'Casual'
person.state().isVirtual(); // >>> true
person.greet(); // >>> "Hi!"
Person.prototype.state(); // >>> State ''
Person::state() # >>> State ''
'state' of person # >>> true
person.hasOwnProperty 'state' # >>> false
person.state() # >>> State ''
person.hasOwnProperty 'state' # >>> true
person.state().isVirtual() # >>> false
person.greet() # >>> "Hello."
person.state '-> Casual' # >>> State 'Casual'
person.state().isVirtual() # >>> true
person.greet() # >>> "Hi!"
Person::state() # >>> State ''
When an accessor method (person.state
) is called, it first checks the context object (person
) to ensure that it has its own accessor method. If it does not, and is instead attempting to inherit the accessor (state
) of a prototype, then an empty state implementation is automatically created for the inheritor, which in turn generates a corresponding new accessor method (person.state
), to which the original call is then forwarded.
Even though the inheritor’s new state implementation is empty, it inherits all the methods, data, events, etc. of the prototype’s states, which it identifies as its protostates. The inheritor may adopt a protostate as its current state just as it would with a state of its own. In this case a temporary virtual state is created within the state implementation of the inheritor, as a stand-in for the protostate. Virtual states exist only so long as they are active; once the object transitions elsewhere, any virtual states consequently rendered inactive are automatically destroyed.
This system of protostates and virtual states allows an object’s state implementation to benefit from the prototypal reuse patterns of JavaScript without the states themselves having to maintain any direct prototypal relationship with each other.
View source: State
constructor, State.prototype.protostate
Return to: Inheritance < Concepts < Overview < top
The accessor method of a stateful object (object.state()
) returns its current state if called with no arguments. If a selector string argument is provided, the accessor will query the object’s state tree for a matching state.
State uses a simple selector format:
State names are delimited from their member substates with the dot (
.
) character.A selector that begins with
.
will be evaluated relative to the local context, while a selector that begins with a name will be evaluated as absolute, i.e., relative to the root state.An absolute fully-qualified name is not necessary except for disambiguation:
'A.B.C'
and'C'
will both resolve to the deep substate namedC
provided that there is no other state namedC
located higher in the state tree.Special cases: empty-string
''
references the root state; single-dot.
references the local context state; double-dot..
references its immediate superstate, etc.Querying a selector ending in
*
returns an array of the immediate substates of that level, while**
returns a flattened array of all descendant substates of that level.
var o = {};
state( o, {
A: {
AA: state( 'initial', {
AAA: state
}),
AB: state
},
B: state
});
o.state(); // >>> State 'AA'
o.state(''); // >>> State ''
o.state('A.AA.AAA'); // >>> State 'AAA'
o.state('.'); // >>> State 'AA'
o.state('..'); // >>> State 'A'
o.state('...'); // >>> State ''
o.state('.AAA'); // >>> State 'AAA'
o.state('..AB'); // >>> State 'AB'
o.state('...B'); // >>> State 'B'
o.state('AAA'); // >>> State 'AAA'
o.state('.*'); // >>> [ State 'AAA' ]
o.state('AAA.*'); // >>> []
o.state('*'); // >>> [ State 'A', State 'B' ]
o.state('**'); // >>> [ State 'A', State 'AA', State 'AAA', State 'AB', State 'B' ]
o = {}
state o,
A:
AA: state 'initial'
AAA: state
AB: state
B: state
o.state() # >>> State 'AA'
o.state '' # >>> State ''
o.state 'A.AA.AAA' # >>> State 'AAA'
o.state '.' # >>> State 'AA'
o.state '..' # >>> State 'A'
o.state '...' # >>> State ''
o.state '.AAA' # >>> State 'AAA'
o.state '..AB' # >>> State 'AB'
o.state '...B' # >>> State 'B'
o.state 'AAA' # >>> State 'AAA'
o.state '.*' # >>> [ State 'AAA' ]
o.state 'AAA.*' # >>> []
o.state '*' # >>> [ State 'A', State 'B' ]
o.state '**' # >>> [ State 'A', State 'AA', State 'AAA', State 'AB', State 'B' ]
Selectors are similarly put to use elsewhere as well: for example, a transition’s origin
and target
properties are evaluated as selectors, and several State
methods, including change
, is
, isIn
, has
, isSuperstateOf
, and isProtostateOf
, accept a selector as their main argument.
View source: State.prototype.query
Return to: Selectors < Concepts < Overview < top
State expressions may include a space-delimited set of attributes, provided as a single string argument that precedes the object map within a state()
call.
state( obj, 'abstract', {
Alive: state( 'default initial mutable', {
update: function () { /*...*/ }
}),
Dead: state( 'final', {
update: function () { /*...*/ }
})
});
state obj, 'abstract'
Alive: state 'default initial mutable'
update: -> # ...
Dead: state 'final'
update: -> # ...
By default, states are weakly immutable — their data, methods, guards, substates, and transitions cannot be altered once the state has been constructed — a condition that can be affected at construct-time by these mutability attributes. Each attribute is implicitly inherited from any of the state’s ancestors, be they superstates or protostates. They are listed here in order of increasing precedence.
mutable — Including the
mutable
attribute in the state’s expression lifts the default restriction of weak immutability, exposingState
instance methods such asmutate
,addMethod
,addSubstate
, and so on.finite — Declaring a state
finite
guarantees its hierarchical structure; descendant states may neither be added nor removed.immutable — Adding
immutable
makes a state strongly immutable, whereupon immutability is enforced permanently and absolutely;immutable
overrules and contradictsmutable
(and impliesfinite
), irrespective of whether the attributes are literal or inherited.
State does not confine currency to “leaf” states; rather, all states — including substate-bearing interior states — are by default regarded as concrete, and thus may be targeted by a transition. Nevertheless, sometimes it may still be appropriate to author abstract states whose purpose is limited to serving as a common ancestor of descendant concrete states.
abstract — A state that is
abstract
cannot itself be current. Consequently a transition target that points to an abstract state will be forcibly redirected to one of its substates.concrete — Including the
concrete
attribute will override the abstraction that would otherwise have been inherited from anabstract
protostate.default — Marking a state
default
designates it as the intended redirection target for any transition that has targeted its abstract superstate.
Currency must often be initialized or confined to particular states, as directed by the destination attributes:
initial — Marking a state
initial
specifies which state is to be assumed immediately following thestate()
application. No transition or anyenter
orarrive
events result from this initialization.conclusive — Once a
conclusive
state is entered, it cannot be exited, although transitions may still freely traverse within its substates.final — Once a state marked
final
is entered, no further transitions are allowed.
“finite mutable” — A state that is, literally or by inheritance, both
finite
andmutable
guarantees its hierarchical structure without imposing absolute immutability.“abstract concrete” is an invalid contradiction. If both attributes are literally applied to a state,
concrete
takes precedence and negatesabstract
.
Return to: Attributes < Concepts < Overview < top
Arbitrary data can be attached to each state, and inherited accordingly through protostates and superstates. Data may be declared within an expression, and both read and written using the data
method:
function Chief () {
state( this, 'mutable', {
Enraged: {
Thermonuclear: {
data: {
task: 'destroy'
budget: Infinity
}
}
}
});
}
state( Chief.prototype, {
data: {
task: 'innovate',
budget: 1e10
},
Enraged: {
data: {
action: 'compete'
}
}
}
var mobs = new Chief;
mobs.state().data();
// >>> { task: 'innovate', budget: 10000000000 }
mobs.state('-> Enraged');
mobs.state().data({ target: 'Moogle' });
mobs.state().data();
// >>> { target: 'Moogle', task: 'compete', budget: 10000000000 }
mobs.state().go('Thermonuclear');
mobs.state().data();
// >>> { target: 'Moogle', task: 'destroy', budget: Infinity }
class Chief
state @::,
data:
task: 'innovate'
budget: 1e10
Enraged:
data:
task: 'compete'
constructor: ->
state this, 'mutable'
Enraged:
Thermonuclear:
data:
task: 'destroy'
budget: Infinity
mobs = new Chief
mobs.state().data()
# >>> { task: 'innovate', budget: 10000000000 }
mobs.state '-> Enraged'
mobs.state().data target: 'Moogle'
mobs.state().data()
# >>> { target: 'Moogle', task: 'compete', budget: 10000000000 }
mobs.state().go 'Thermonuclear'
mobs.state().data()
# >>> { target: 'Moogle', task: 'destroy', budget: Infinity }
View source: State.privileged.data
Return to: Data < Concepts < Overview < top
A defining feature of State is the ability for an object to exhibit a variety of behaviors. A state expresses behavior by defining overrides for any of its object’s methods.
When state is applied to an object, State identifies any methods already present on the object for which there exists at least one override somewhere within the state expression. These methods will be relocated to the root state, and replaced on the object with a special delegator method. The delegator’s job is to redirect any subsequent calls it receives to the object’s current state, from which State will then locate and invoke the proper stateful implementation of the method. Should no active states contain an override for the called method, the delegation will default to the object’s original implementation of the method if one exists, or result in a noSuchMethod
event otherwise.
var shoot = function () { return "pew!"; },
raygun = { shoot: shoot };
raygun.shoot === shoot; // >>> true
state( raygun, {
RapidFire: {
shoot: function () { return "pew pew pew!"; }
}
});
raygun.shoot === shoot; // >>> false
raygun.shoot.isDelegator; // >>> true
raygun.state('').method('shoot') === shoot // >>> true
raygun.shoot(); // >>> "pew!"
raygun.state('-> RapidFire'); // >>> State 'RapidFire'
raygun.shoot(); // >>> "pew pew pew!"
shoot = -> "pew!"
raygun = shoot: shoot
raygun.shoot is shoot # >>> true
state raygun,
RapidFire:
shoot: -> "pew pew pew!"
raygun.shoot is shoot # >>> false
raygun.shoot.isDelegator # >>> true
raygun.state('').method('shoot') is shoot # >>> true
raygun.shoot() # >>> "pew!"
raygun.state '-> RapidFire' # >>> State 'RapidFire'
raygun.shoot() # >>> "pew pew pew!"
View source: State createDelegator
, State.privileged.addMethod
When an owner object’s delegated state method is called, it is invoked not in the context of its owner, but rather of the state in which it is declared, or, if the method is inherited from a protostate, in the context of the local state that inherits from that protostate. This subtle difference in policy does mean that, within a state method, the owner cannot be directly referenced by this
as it normally would; however, it is still always accessible by calling this.owner()
.
Of greater importance is the lexical information afforded by binding state methods to their associated state. This allows state method code to take advantage of polymorphic idioms, such as calling up to a superstate’s implementation of a method, as facilitated by the apply
and call
methods of State
.
state( owner, {
A: {
bang: function ( arg1, arg2 ) { /* ... */ }
B: {
bang: function () { return this.superstate().apply( 'bang', arguments ); }
}
}
});
state owner,
A:
bang: ( arg1, arg2 ) -> # ...
B:
bang: -> @superstate().apply 'bang', arguments
Note: it may be important here to call attention to a significant difference distinguishing these methods from their familiar eponymous counterparts at
Function.prototype
— here, the first argument accepted byapply
andcall
is a string that names a state method, rather than a context object (since, again, the resulting invocation’s context is automatically bound to that method’s associatedState
).
View source: State.prototype.apply
, State.privileged.method
In the case of an attempt to call
or apply
a state method that does not exist within that state and cannot be inherited from any protostate or superstate, the invocation will fail and return undefined
. In addition, State allows such a contingency to be “trapped” by emitting a generic noSuchMethod
event, whose listeners take as arguments the sought methodName
and an Array
of the arguments provided to the failed invocation. Additionally, a more specific noSuchMethod:<methodName>
event type is emitted as well, whose listeners take just the arguments as provided to the failed invocation.
var log = console.log,
owner = {},
root;
state( owner, 'abstract', {
foo: function () { log("I exist!"); },
A: state( 'default', {
bar: function () { log("So do I!"); }
}),
B: state
});
// >>> State 'A'
root = owner.state('');
root.on( 'noSuchMethod', function ( methodName, args ) {
log("`owner` has no method " + methodName + " in this state!");
});
root.on( 'noSuchMethod:bar': function () {
log("You also could have trapped a bad call to 'bar' like this.");
});
owner.foo(); // log <<< "I exist!"
owner.bar(); // log <<< "So do I!"
owner.state('-> B'); // State 'B'
owner.foo(); // log <<< "I exist!"
owner.bar(); // undefined
// log <<< "`owner` has no method 'bar' in this state!"
// log <<< "You also could have trapped a bad call to 'bar' like this."
log = console.log
owner = {}
state owner, 'abstract'
foo: -> log "I exist!"
A: state 'default'
bar: -> log "So do I!"
B: state
# >>> State 'A'
root = owner.state ''
root.on 'noSuchMethod', ( methodName, args ) ->
log "`owner` has no method '#{methodName}' in this state!"
root.on 'noSuchMethod:bar', ( args... ) ->
log "You also could have trapped a bad call to 'bar' like this."
owner.foo() # log <<< "I exist!"
owner.bar() # log <<< "So do I!"
owner.state '-> B' # State 'B'
owner.foo() # log <<< "I exist!"
owner.bar() # undefined
# log <<< "`owner` has no method 'bar' in this state!"
# log <<< "You also could have trapped a bad call to 'bar' like this."
View source: State.prototype.apply
This example of a simple Document
class demonstrates state method inheritance and polymorphism. Note the points of interest that are numbered in trailing comments and explained below:
var fs = require('fs'),
state = require('state');
function Document ( location, text ) {
this.location = function () {
return location;
};
this.read = function () {
return text;
};
this.edit = function ( newText ) { // [1]
text = newText;
return this;
};
}
state( Document.prototype, 'abstract', {
freeze: function () { // [3]
var result = this.call( 'save' ); // [4]
this.change('Frozen');
return result;
},
Dirty: {
save: function () {
this.change( 'Saved', [
this.owner().location(), this.owner().read()
]); // [5]
return this.owner();
}
},
Saved: state( 'initial', {
edit: function () {
var result = this.superstate().apply( 'edit', arguments ); // [2]
this.change('Dirty');
return result;
},
Frozen: state( 'final', {
edit: function () {},
freeze: function () {},
})
}),
transitions: {
Writing: {
origin: 'Dirty',
target: 'Saved',
action: function ( location, text ) {
var transition = this;
return fs.writeFile( location, text, function ( err ) {
if ( err ) return transition.abort( err ).change('Dirty');
transition.end();
});
}
}
}
});
fs = require 'fs'
state = require 'state'
class Document
constructor: ( location, text ) ->
@location = -> location
@read = -> text
@edit = ( newText ) -> # [1]
text = newText
this
state @::, 'abstract'
freeze: -> # [3]
result = @call 'save' # [4]
@change 'Frozen'
result
Dirty:
save: ->
@change 'Saved', [ @owner.location(), @owner().read() ] # [5]
@owner()
Saved: state 'initial'
edit: ->
result = @superstate().apply 'edit', arguments # [2]
@change 'Dirty'
result
Frozen: state 'final'
edit: ->
freeze: ->
transitions:
Writing: origin: 'Dirty', target: 'Saved', action: ( location, text ) ->
fs.writeFile location, text, ( err ) =>
return @abort( err ).change 'Dirty' if err
do @end
A “privileged” method
edit
is defined inside the constructor, closing over a private variabletext
to which it requires access. Later, when state is applied to the object, this method will be moved to the root state, and a delegator will be added to the object in its place.An overridden implementation of
edit
, while not closed over the constructor’s private variabletext
, is able to call up to the original implementation usingthis.superstate().apply('edit')
.The
freeze
method is declared on the abstract root state, callable from statesDirty
andSaved
(but notFrozen
, where it is overridden with a no-op).The
save
method, which only appears in theDirty
state, is still callable from other states, as its presence inDirty
causes a no-op version of the method to be automatically added to the root state. This allowsfreeze
to safely callsave
despite the possiblity of being in a state (Saved
) with no such method.Changing to
Saved
fromDirty
results in theWriting
transition, whose asynchronousaction
is invoked with the arguments array provided by thechange
call.
Return to: Methods < Concepts < Overview < top
Whenever an object’s current state changes, a transition state is created, which temporarily assumes the role of the current state while the object is travelling from its origin or source state to its target state.
A state expression may include any number of transition expressions, which define some action to be performed, either synchronously or asynchronously, along with selectors for the origin
/source
and target
states to which the transition should apply, and guards to determine the appropriate transition to employ.
Before an object undergoes a state change, it examines the transition expressions available for the given origin and target, and selects one to be enacted. To test each expression, its origin
state is validated against its admit
transition guards, and its target
state is validated against its release
transition guards. The object then instantiates a Transition
based on the first valid transition expression it encounters, or, if no transition expression is available, a generic actionless Transition
.
Where transition expressions should be situated in the state hierarchy is largely a matter of discretion. In determining the appropriate transition expression for a given origin–target pairing, the search proceeds, in order:
- at the expression’s
target
state (compare to the manner in which CSS3 transitions are declared with respect to classes) - at the expression’s
origin
state - progressively up the superstate chain of
target
- progressively up the superstate chain of
origin
Transitions can therefore be organized in a variety of ways, but ambiguity resolution is regular and predictable, as demonstrated with the Zig
transition in the example below:
// An asynchronous logger
function log ( message, callback ) { /* ... */ }
function Foo () {}
state( Foo.prototype, 'abstract', {
Bar: state( 'default initial' ),
Baz: state({
transitions: {
Zig: { action: function () {
var transition = this;
log( "BLEEP", function () { transition.end(); } );
}}
}
}),
transitions: {
Zig: { origin: 'Bar', target: 'Baz', action: function () {
var transition = this;
log( "bleep", function () { transition.end(); } );
}},
Zag: { origin: 'Baz', target: 'Bar', action: function () {
var transition = this;
log( "blorp", function () { transition.end(); } );
}}
}
});
var foo = new Foo;
function zig () {
var transition;
foo.state(); // State 'Bar'
foo.state('-> Baz'); // (enacts the `Zig` transition of `Baz`)
transition = foo.state(); // Transition 'Zig'
transition.on( 'end', zag );
}
function zag () {
var transition;
foo.state(); // State 'Baz'
foo.state('-> Bar'); // (enacts the `Zag` transition of the root state)
transition = foo.state(); // Transition `Zag`
transition.on( 'end', stop );
}
function stop () {
return "take a bow";
}
zig();
// ...
// log <<< "BLEEP"
// ...
// log <<< "blorp"
# An asynchronous logger
log = ( message, callback ) -> # ...
class Foo
state @::, 'abstract'
Bar: state 'default initial'
Baz: state
transitions:
Zig: action: ->
log "BLEEP", => @end()
transitions:
Zig: origin: 'Bar', target: 'Baz', action: ->
log "bleep", => @end()
Zag: origin: 'Baz', target: 'Bar', action: ->
log "blorp", => @end()
foo = new Foo
zig = ->
foo.state() # State 'Bar'
foo.state '-> Baz' # (enacts the `Zig` transition of `Baz`)
transition = foo.state() # Transition 'Zig'
transition.on 'end', zag
zag = ->
foo.state() # State 'Baz'
foo.state '-> Bar' # (enacts the `Zag` transition of the root state)
transition = foo.state() # Transition 'Zag'
transition.on 'end', stop
stop = -> "take a bow"
do zig
# ...
# log <<< "BLEEP"
# ...
# log <<< "blorp"
A transition performs a stepwise traversal over its domain, which is defined as the subtree rooted at the least common ancestor state between the transition’s source
and target
. At each step in the traversal, the transition instance acts as a temporary substate of the visited state, such that event listeners may expect to inherit from the states in which they are declared.
The traversal sequence is decomposable into an ascending phase, an action phase, and a descending phase.
During the ascending phase, the object emits a
depart
event on thesource
, and anexit
event on any state that will be rendered inactive as a consequence of the transition.The transition then reaches the domain root and moves into the action phase, whereupon it executes any
action
defined in its associated transition expression.Once the action has ended, the transition then proceeds with the descending phase, emitting
enter
events on any state that is rendered newly active, and concluding with anarrival
event on itstarget
state.
function Mover () {}
state( Mover.prototype, {
Stationary: {
Idle: state( 'initial' ),
Alert: state
},
Moving: {
Walking: state,
Running: {
Sprinting: state
}
},
transitions: {
Announcing: { source: '*', target: '*', action: function () {
var name = this.superstate().name() || "the root state";
console.log "action of transition is at " + name;
this.end();
}}
},
// Log the transitional events of all states
construct: function () {
var events, substates, i, j;
events = 'depart exit enter arrive'.split(' ');
substates = [this].concat( this.substates( true ) );
for ( i in substates ) for ( j in events ) {
( function ( s, e ) {
s.on( e, function () {
console.log this.name() + " " + e;
});
}( substates[i], events[j] ) );
}
}
});
var m = new Mover;
m.state('-> Alert');
// log <<< "depart Idle"
// log <<< "exit Idle"
// log <<< "action of transition is at Stationary"
// log <<< "enter Alert"
// log <<< "arrive Alert"
m.state('-> Sprinting');
// log <<< "depart Alert"
// log <<< "exit Alert"
// log <<< "exit Stationary"
// log <<< "action of transition is at the root state"
// log <<< "enter Moving"
// log <<< "enter Running"
// log <<< "enter Sprinting"
// log <<< "arrive Sprinting"
class Mover
state @::,
Stationary:
Idle: state 'initial'
Alert: state
Moving:
Walking: state
Running:
Sprinting: state
transitions:
Announcing: source: '*', target: '*', action: ->
name = @superstate().name() or "the root state"
console.log "action of transition is at {name}"
@end()
# Log the transitional events of all states
construct: ->
events = 'depart exit enter arrive'.split ' '
for s in [this].concat @substates true
for e in events
do ( s, e ) -> s.on e, -> console.log "#{e} #{@name()}"
m = new Mover
m.state '-> Alert'
# log <<< "depart Idle"
# log <<< "exit Idle"
# log <<< "action of transition is at Stationary"
# log <<< "enter Alert"
# log <<< "arrive Alert"
m.state '-> Sprinting'
# log <<< "depart Alert"
# log <<< "exit Alert"
# log <<< "exit Stationary"
# log <<< "action of transition is at the root state"
# log <<< "enter Moving"
# log <<< "enter Running"
# log <<< "enter Sprinting"
# log <<< "arrive Sprinting"
(See Transitional events.)
Should a new transition be started while a transition is already in progress, an abort
event is emitted on the previous transition. The new transition will reference the aborted transition as its source
, retaining by reference the same origin
state as that of the aborted transition, and the traversal will resume, starting with a depart
and exit
event emitted on the aborted transition. Further redirections of the pending traversal will continue to grow this source
chain until a transition finally arrives at its target
state.
View source: Transition
, TransitionExpression
, StateController.privileged.change
Return to: Transitions < Concepts < Overview < top
Events in State follow the familiar emitter pattern: State
exposes methods emit
(aliased to trigger
) for emitting typed events, and addEvent
/removeEvent
(aliased to on
/off
and bind
/unbind
) for assigning listeners to a particular event type.
- Existential events
- Transitional events
- Mutation events
- Custom event types
- Using events to express determinism
construct — Once a state has been instantiated, it emits a
construct
event. Since a state is not completely constructed until its substates have themselves been constructed, the fullconstruct
event sequence proceeds in a bottom-up manner.destroy — A state is properly deallocated with a call to
destroy()
, either on itself or on a superstate. This causes adestroy
event to be emitted immediately prior to the state and its contents being cleared.
As alluded to above, during a transition’s progression from its origin state to its target state, all affected states along the way emit any of four types of events that describe their relation to the transition.
depart — Exactly one
depart
event is always emitted from the origin state, and marks the beginning of the transition.exit — It is followed by zero or more
exit
events, one each from amongst the origin state and any of its superstates that will no longer be active as a result of the transition.enter — Likewise, zero or more
enter
events are emitted, one for each state that will become newly active.arrive — Finally, an
arrive
event will occur exactly once, specifically at the target state, marking the end of the transition.
Given this scheme, a few noteworthy cases stand out. A “non-exiting” transition is one that only descends in the state tree, i.e. it progresses from a superstate to a substate of that superstate, emitting one depart
, zero exit
events, one or more enter
events, and one arrive
. Conversely, a “non-entering” transition is one that only ascends in the state tree, progressing from a substate to a superstate thereof, emitting one depart
, one or more exit
events, zero enter
events, and one arrive
. For a reflexive transition, which is one whose target is its origin, the event sequence consists only of one depart
and one arrive
, both emitted from the same state.
- mutate — When a state’s data or other contents change, it emits a
mutate
event containing the changes made relative to its immediately prior condition.
var flavors = [ 'vanilla', 'chocolate', 'strawberry', 'Stephen Colbert’s Americone Dream' ];
function Kid () {}
state( Kid.prototype, {
data: {
favorite: 'chocolate'
},
whim: function () {
this.data({ favorite: flavors[ Math.random() * flavors.length << 0 ] });
},
whine: function ( complaint ) {
typeof console !== 'undefined' && console.log( complaint );
},
mutate: function ( mutation, delta, before, after ) {
this.owner().whine( "I hate " + delta.favorite + ", I want " + edit.favorite + "!" );
}
});
var junior = new Kid;
// We could have added listeners this way also
junior.state().on( 'mutate', function ( expr, delta, before, after ) { /* ... */ });
junior.whim(); // log <<< "I hate chocolate, I want strawberry!"
junior.whim(); // log <<< "I hate strawberry, I want chocolate!"
junior.whim(); // No whining! On a whim, junior stood pat this time.
junior.whim(); // log <<< "I hate chocolate, I want Stephen Colbert’s Americone Dream!"
flavors = [ 'vanilla', 'chocolate', 'strawberry', 'Stephen Colbert’s Americone Dream' ]
class Kid
state @::,
data:
favorite: 'chocolate'
whim: ->
@data favorite: flavors[ Math.random() * flavors.length << 0 ]
whine: ( complaint ) -> console?.log complaint
mutate: ( mutation, delta, before, after ) ->
@owner.whine "I hate #{ delta.favorite }, I want #{ edit.favorite }!"
junior = new Kid
# We could have added listeners this way also
junior.state().on 'mutate', ( expr, delta, before, after ) -> # ...
do junior.whim # log <<< "I hate chocolate, I want strawberry!"
do junior.whim # log <<< "I hate strawberry, I want chocolate!"
do junior.whim # No whining! On a whim, junior stood pat this time.
do junior.whim # log <<< "I hate chocolate, I want Stephen Colbert’s Americone Dream!"
Through exposure of the emit
method, state instances allow any type of event to be broadcast and consumed.
function Kid () {}
state( Kid.prototype, {
Happy: state(),
Sad: state(),
events: {
gotIceCream: function () { this.be('Happy'); },
spilledIceCream: function () { this.be('Sad'); }
}
});
var junior = new Kid;
junior.state().emit('gotIceCream');
junior.state();
// >>> State 'Happy'
junior.state().emit('spilledIceCream');
junior.state();
// >>> State 'Sad'
class Kid
state @::,
Happy: state()
Sad: state()
events:
gotIceCream: -> @be 'Happy'
spilledIceCream: -> @be 'Sad'
junior = new Kid
junior.state().emit 'gotIceCream'
junior.state()
# >>> State 'Happy'
junior.state().emit 'spilledIceCream'
junior.state()
# >>> State 'Sad'
View source: State.privileged.emit
An event listener may also be expressed simply as a State name, which is interpreted as an order to transition to that State after all of an event’s callbacks have been invoked. This bit of shorthand allows for concise expression of deterministic behavior, where the occurrence of a particular event type within a particular State has a definitive, unambiguous effect on the state of the object.
function DivisibleByThreeComputer () {
state( this, 'abstract', {
s0: state( 'initial default',
{ '0':'s0', '1':'s1' } ),
s1: { '0':'s2', '1':'s0' },
s2: { '0':'s1', '1':'s2' }
});
}
DivisibleByThreeComputer.prototype.compute = function ( number ) {
var i, l, binary = number.toString(2);
this.state('->'); // reset
for ( i = 0, l = binary.length; i < l; i++ ) {
this.state().emit( binary[i] );
}
return this.state().is('s0');
}
var three = new DivisibleByThreeComputer;
three.compute( 8 ); // >>> false
three.compute( 78 ); // >>> true
three.compute( 1000 ); // >>> false
three.compute( 504030201 ); // >>> true
class DivisibleByThreeComputer
constructor: ->
state this, 'abstract'
s0: state 'initial default'
'0':'s0', '1':'s1'
s1: '0':'s2', '1':'s0'
s2: '0':'s1', '1':'s2'
compute: ( number ) ->
@state '->' # reset
@state().emit symbol for symbol in number.toString 2
@state().is 's0'
three = new DivisibleByThreeComputer
three.compute 8 # >>> false
three.compute 78 # >>> true
three.compute 1000 # >>> false
three.compute 504030201 # >>> true
Return to: Events < Concepts < Overview < top
States and transitions can be outfitted with guards that dictate whether and how they may be used.
For a transition to be allowed to proceed, it must first have satisfied any guards imposed by the states that would be its endpoints: the origin state from which it will depart must agree to release
the object to the intended target at which it will arrive, and likewise the target must also agree to admit
the object from the departed origin.
var object = {};
state( object, {
A: state( 'initial', {
admit: false,
release: { D: false }
}),
B: {
data: { bleep: 'bleep' },
release: {
'C, D': true,
'C.**': false
}
},
C: {
data: { blorp: 'blorp' },
admit: true,
C1: {
C1a: state
},
C2: state
},
D: {
enter: function () { this.$('B').removeGuard( 'admit' ); }
admit: function ( fromState ) { return 'blorp' in fromState.data() },
release: function ( toState ) { return 'bleep' in toState.data() }
}
})
state object = {},
A: state( 'initial',
admit: false
release: D: false
)
B:
admit: false
release:
'C, D': true
'C.**': false
data: bleep: 'bleep'
C:
data: blorp: 'blorp'
C1:
C1a: state
C2: state
D:
enter: -> @$('B').removeGuard 'admit'
admit: ( fromState ) -> true if 'blorp' of fromState.data()
release: ( toState ) -> true if 'bleep' of toState.data()
Here we observe state guards imposing the following restrictions:
object
initializes into stateA
, but upon leaving it may never return; we’ve also specifically disallowed direct transitions fromA
toD
.State
B
disallows entry from anywhere (for now), and releases conditionally toC
orD
but not directly to any descendant states ofC
; we also note its data itembleep
.State
C
imposes no guards, but we note its data itemblorp
.State
D
“unlocks”B
; it is also guarded by checking the opposing state’sdata
, allowing admission only from states with a data item keyedblorp
, and releasing only to states with data itembleep
.
The result is that object
is initially constrained to a progression from state A
to C
or its descendant states; exiting the C
domain is initially only possible by transitioning to D
; from D
it can only transition back into C
, however on this and subsequent visits to C
, it has the option of transitioning to either B
or D
, while B
insists on directly returning the object’s state only to one of its siblings C
or D
.
View source: StateController evaluateGuard
, StateController.prototype.getTransitionExpressionFor
Transition expressions may also include admit
and release
guards. Transition guards are used to decide which one transition amongst possibly several is to be executed as an object changes its state between a given origin
and target
.
function Scholar () {}
state( Scholar.prototype, 'abstract', {
Matriculated: state( 'initial', {
graduate: function ( gpa ) {
this.owner().gpa = gpa;
this.change( 'Graduated' );
}
}),
Graduated: state( 'final' ),
transitions: {
Summa: {
origin: 'Matriculated', target: 'Graduated',
admit: function () { return this.data().gpa >= 3.95; },
action: function () { /* swat down offers */ }
},
Magna: {
origin: 'Matriculated', target: 'Graduated',
admit: function () {
var gpa = this.data().gpa;
return 3.75 <= gpa && gpa < 3.95;
},
action: function () { /* swat down recruiters */ }
},
Laude: {
origin: 'Matriculated', target: 'Graduated',
admit: function () {
var gpa = this.data().gpa;
return 3.50 <= gpa && gpa < 3.75;
},
action: function () { /* brag to the cat */ }
},
'': {
origin: 'Matriculated', target: 'Graduated',
action: function () { /* blame rounding error, grab another beer */ }
}
}
});
var scholar = new Scholar;
scholar.graduate( 3.4999 );
class Scholar
state @::, 'abstract'
Matriculated: state 'initial'
graduate: ( gpa ) ->
@owner().gpa = gpa
@$ -> 'Graduated'
Graduated: state 'final'
transitions: do ->
t = ( o ) -> o[k] = v for k,v of origin: 'Matriculated', target: 'Graduated'; o
Summa: t
admit: -> @owner().gpa >= 3.95
action: -> # swat down offers
Magna: t
admit: -> 3.75 <= @owner().gpa < 3.95
action: -> # choose favorite internship
Laude: t
admit: -> 3.50 <= @owner().gpa < 3.75
action: -> # brag to the cat
'': t
action: -> # blame rounding error, grab another beer
scholar = new Scholar
scholar.graduate 3.4999
View source: StateController evaluateGuard
, StateController.prototype.getTransitionExpressionFor
Return to: Guards < Concepts < Overview < top
Minimal footprint
All functionality of State is to be instigated through the exported state
function — depending on the arguments provided, state()
can be used either to generate state expressions, or to implement expressed states into an existing JavaScript object. In the latter case, the newly implemented system of states is thereafter to be accessed from a single object.state()
method on the affected object.
Expressive power
As much as possible, State should aim to look and feel like a feature of the language. The interpreted shorthand syntax, simple keyword attributes, and limited interface should allow for production code that is declarative and easy to write and understand. Adopters of terse, depunctuated JavaScript dialects like CoffeeScript should only see further gains in expressiveness.
Opacity
Apart from the addition of the object.state()
method, a call to state()
must make no other modifications to a stateful object’s interface. Methods are replaced with delegators, which forward method calls to the current state. This is to be implemented opaquely and non-destructively: consumers of the object need not be aware of which states are active in the object, or even that a concept of state exists at all, and a call to object.state().root().destroy()
must restore the object to its original form.
Any state may be ordered to keep a history of its own internal state. Entries are recorded in the history anytime the given state is involved in a transition, or experiences a change to its internal content or structure. The history may be traversed in either direction, and elements replaced or pushed onto the stack at its current index. When a transition targets a retained state, it will consult that state’s history and redirect itself back to whichever of the state’s substates was most recently current.
Whereas an object’s state is most typically conceptualized as an exclusive-OR operation (i.e., its current state is always fixed to exactly one state), a state may instead be defined as concurrent, relating its substates in an “AND” composition, where occupation of the concurrent state implies simultaneous occupation of each of its immediate substates.
Optimization pathways under consideration
Forego hidden references in favor of plain public properties on members such as
superstate
,controller
, etc., to simplify the code base and avoid costs of closures.Further granularize the
State realize
function such that each of the internal data, methods, etc. objects, and their associated per-instance methods, would be dynamically added only as needed.Keep a hashtable on the root state of common
query
input strings, to avoid repeated recursive searches.Allow opt-in to ES5’s meta-programming features and Harmony Proxies to more deeply embed the state implementation into objects.
Return to: About this project < top