Frame's mission is to be the easiest and most intuitive way to specify and implement Finite State Machines (FSMs) - aka "Automata" - in any object-oriented language.
FMN Syntax | Definition | Example | Gist |
---|---|---|---|
# | Controller | #LoggedOut | |
-block- | Block Section | -machine- | |
$ | Frame State | $Begin | |
@ | Frame Event | @ | |
@|message| or |message| | Frame event message selector | |buttonClick| | |
@|>>| | Frame start message | @|>>| | |
@|<<| | Frame stop message | @|<<| | |
@|>| | Frame enter state message | @|>| | |
@|<| | Frame exit state message | @|<| | |
@[param] or [param] | Frame Event parameter attribute | @[firstName] | |
^ | Return statement | ^ | |
@^ or ^(expr) | Frame Event return attribute | ^("YES!") | |
> | Continue (break) | > | |
-> | Transition operator | -> $Working | Transition |
->> | Change state operator | ->> $Working | Change State |
// this is a comment
Frame Events are essential to the notation and the implementation. The notation assumes three mandatory fields for a FrameEvent - (message, parameters, return value). Frame Events have a special type symbol @
that refers to the current event in scope (there is almost always only one).
The pseudocode for the implementation is:
class FrameEvent {
var _msg:String
var _params:Object
var _return:Object
FrameEvent(msg:String, params:Object = null) {
_msg = msg
_params = params
}
}
A controller is a Frame machine, or automaton, that contains blocks (or sections) that define the internal structure of the machine. From an implementation standpoint, the reference architecture for implementing a Frame controller is defined as an object-oriented class, but this is for general applicability and not a requirement.
// FMN
#MyController
-interface-
-machine-
-actions-
-domain-
## // optional
// Pseudocode implementation
class MyController {
// -interface-
// -machine-
// -actions-
// -domain-
}
// FMN
-interface- // interface block declaration
m1 // no parameters or return value
m2[param1 param2] // typeless parameters
m3[param1:string param2:int] // typed parameters
m4:boolean // typed return value
m5[param4 param5:string]:int // mixed parameters and return value
m6 @(|6m|) // message alias
// Pseudocode implementation
func m1() {
var e = new FrameEvent("m1")
_state(e)
}
func m2(param1 param2) {
var params = { "param1":param1, "param2":param2 }
var e = new FrameEvent("m2", params)
_state(e)
}
func m3(param1:string, param2:int) {
var params = { "param1":param1, "param2":param2 }
var e = new FrameEvent("m3", params)
_state(e)
}
func m4():boolean {
var e = new FrameEvent("m2")
_state(e)
return (boolean) e._return
}
func m5(param4, param5:string):int {
var params = { "param1":param1, "param2":param2 }
var e = new FrameEvent("m2", params)
_state(e)
return (int) e._return
}
func m6() {
var e = new FrameEvent("6m")
_state(e)
}
// FMN
-machine- // machine block declaration
$Begin // $ -- token indicates state definition
|>>| // |>>| -- start event selector
-> $Working // -> -- transition token
^ // ^ -- return token
$Working => $Default // => -- Dispatch operator to send event to $Default
|>| enterWorking() ^ // |>| -- enter event selector
|<| exitWorking() ^ // |<| -- exit event selector
$End // $End state has no event handlers.
$Default // $Default parent state or "mode"
|<<| -> $End ^ // |<<| -- stop event selector token
// Pseudocode Implementation
// -machine-
func Begin(e:FrameEvent) { // $Begin
if (e._msg == ">>") { // |>>|
_transition(Working) // -> $Working
return // ^
}
}
func Working(e:FrameEvent) { // $Working
if (e._msg == ">") { // |>|
enterWorking() // enterWorking()
return // ^
}
if (e._msg == "<") { // |<|
exitWorking() // exitWorking()
return // ^
}
Default(e) // $Working => $Default
}
func End(e:FrameEvent) { // $End
}
func Default(e:FrameEvent) { // $Default
if (e._msg == "<<") { // |<<|
_transition(End) // -> $End
return // ^
}
}
//-- machinery and mechanisms --//
var _state(e:FrameEvent) = Begin // initialize start state
func _transition(newState:FrameState) {
_state(new FrameEvent("<")) // send exit event
_state = newState // change state
_state(new FrameEvent(">")) // send enter event
}
// FMN
$Working
|>>| startMachine() ^ // start machine event
|<<| stopMachine() ^ // stop machine event
|>| enterState() ^ // enter state event
|<| exitState() ^ // exit state event
|e1| processE1(@[x] @[y]) ^ // packed param passing
|e2|[x y] processE2(x y) ^ // unpacked param passing
// Pseudocode Implementation
func Working(e:FrameEvent) { // $Working
if (e._msg == ">>") { // |>|
startMachine() // startMachine()
return // ^
}
if (e._msg == "<<") { // |<<|
stopMachine() // stopMachine()
return // ^
}
if (e._msg == ">") { // |>|
enterState() // enterState()
return // ^
}
if (e._msg == "<") { // |<|
exitState() // exitState()
return // ^
}
if (e._msg == "e1") { // |e1|
processE1( // processE1(@[x] @[y])
e._params['x']
) e._params['y']
return // ^
}
if (e._msg == "e2") { // |e1|
var x = e._params['x'] // [x y]
var y = e._params['y']
processE2(x,y) // processE2(x y)
return // ^
}
}
The Frame notation for routing control flow is inspired by the C ternary operator. However it has been modified to handle a wide range of branching scenarios.
The first routing syntax is for simple Boolean tests.
Boolean expressions can be enclosed in parenthesis for clarity:
// FMN
(x < 10) ? small() :>
(x > 100) ? large() :
justRight()
::
// Pseudocode implementation
or omitted:
// FMN
isSmall(x) ? small() :>
isLarge(x) ? large() :
justRight()
::
// Pseudocode implementation
if (isSmall(x)) {
small()
} else if (isLarge(x)) {
large()
} else {
justRight()
}
or mixed:
// FMN
(isSmall(x)) ? small() :>
x > 100 ? large() :
justRight()
::
// Pseudocode implementation
if (isSmall(x)) {
small()
} else if (x > 100) {
large()
} else {
justRight()
}
Here is an example demonstrating a series of validation tests on an updated name:
// FMN
$UnvalidatedName
|updateName|
validateFirstName(@[firstName]) ?! alert("First Name Error") ^ :>
validateLastName(@[lastName]) ?! alert("Last Name Error") ^ :
-> $ValidatedName ^ ::
^
// Pseudocode implementation
func UnvalidatedName(e:FrameEvent) {
if (e._message == "updateName") {
if (!validateFirstName(e._params["firstName"])) {
alert("First Name Error")
return
} else if (!validateLastName(e._params["lastName"])) {
alert("Last Name Error")
return
} else {
_transition(ValidatedName)
return
}
return
}
}
Frame also has syntax for matching values to determine the control flow. The general syntax is:
<test_value> ?<match_type>
/<match_value>/ <behavior>
/<match_value>/ <behavior>
/_/ <null_behavior>
/*/ <default_behavior>
::
The possible match types are:
~
: match string
#
: match number or range
*
: match regular expression .
For instance:
// FMN
userName ?~
/bill/ matchedBill()
/steve/ matchedSteve()
::
// Pseudocode implementation
if (userName == "bill") {
matchedBill()
} else if (userName == "steve") {
matchedSteve()
}
Tests can be done for the union of matches:
// FMN
userName ?~
/bill/steve/ matchedBillOrSteve()
::
// Pseudocode implementation
if (userName == "bill" || userName == "steve") {
matchedBillOrSteve()
}
Two special string matches exist that can be escaped using \:
/\_/
: match single underscore
/\*/
: match single star
// FMN
userName ?~
/bill/steve/ matchedBillOrSteve()
/_/ matchedNull()
/\_/ matchedUnderscore()
/\*/ matchedStar()
/*/ matchedDefault()
::
// Pseudocode implementation
if (userName == "bill" || userName == "steve") {
matchedBillOrSteve()
} else if (userName == null) {
matchedNull()
} else if (userName == "_") {
matchedUnderscore()
} else if (userName == "*") {
matchedStar()
} else {
matchedDefault()
}
Number matches can be on discrete values:
// FMN
x ?#
/1/ small()
/10/ medium()
/100/ large()
/*/ otherSize()
::
// Pseudocode implementation
if (x == 1) {
small()
} else if (x == 10) {
medium()
} else if (x == 100) {
large()
} else {
otherSize()
}
Frame supports number ranges through the following range operator syntax:
min..max // inclusive range [min, max]
min... // min to positive infinity [min, infinity)
...max // negative infinity to max (-infinity, max]
min..<max // min up to max [min, max)
min<..max // after min up to max (min, max]
min<..<max // between min and max (min, max)
An example:
// FMN
x ?#
/...<10/ small() // negative infinity to < 10
/10..<100/ medium() // 10 to < 100
/100.../ large() // 100 to positive infinity
::
// Pseudocode implementation
if (x < 10) {
small()
} else if (10 =< x && x < 100) {
medium()
} else if (100 =< x) {
large()
}
Frame also can support regular expression matching, but does not currently provide a detailed specification for what flavor of regex language to use. This aspect of Frame will be more tightly defined in future versions of the notation:
animals ?*
/dogs?/ dogOrPack()
/cats?/ catOrCowder()
::