Coding state machines (was Re: Case-logic)
Ned Konz
ned at squeakland.org
Fri Oct 29 18:36:52 UTC 2004
On Tuesday 26 October 2004 10:15 pm, Blake wrote:
> Can anyone recommend some state-machine type stuff in the codebase for me
> to look at? Or thoughts on how you'd code something like the code below?
Yes. In my Connectors 2 package (available on SqueakMap) there is code for a
hierarchical state machine (supporting the full Harel semantics), and a Morph
class that uses these state machines for event handling.
You can add to or modify existing state machine definitions in two directions:
you can define sub-states to provide specialization, and/or you can inherit
from existing event handler classes and override their event handling
methods.
The stock QHsmMorph has the following state hierarchy. Note that it does not
use the MouseClickState state machine, handling click and double-click
detection in its own handlers.
Top
Global
WaitingForDoubleClick
FirstClickUp (MouseDown)
WaitingForDoubleClickWithMouseDown
SecondClickDown (MouseUp)
FirstClickDown (MouseUp)
Idle
Dragging
Initial
Events that occur in a sub- (nested) state like FirstClickUp can be handled in
the state handlers for FirstClickUp, its parent WaitingForDoubleClick, or its
parent Global. There is one method per state. Optionally, you can specialize
by providing a method for a given kind of event in a given state. There are
three examples of this here (they are shown by parentheses).
This is based on the idea (strange as it seems) of generating method selectors
based on the current state and maybe also the type of the event.
So in this case, the corresponding method selectors are:
Top => stateTop
Global => stateGlobal
WaitingForDoubleClick => stateWaitingForDoubleClick
FirstClickUp (MouseDown) => handleMouseDownInFirstClickUp
WaitingForDoubleClickWithMouseDown =>
stateWaitingForDoubleClickWithMouseDown
SecondClickDown (MouseUp) => handleMouseUpInSecondClickDown
FirstClickDown (MouseUp) => handleMouseUpInFirstClickDown
Idle => stateIdle
Dragging => stateDragging
Initial => stateInitial
Each event has a symbolic type and an optional event (perhaps a MorphicEvent)
associated with it.
When an event is handled, first we make a specialized selector using the
symbolic type of the event pasted together with the current state name:
'handle' + <event type> + 'In' + <current state name>
If there is no interned symbol matching that, or if the event handler does not
understand that selector, then we try the more general state handler:
'state' + <current state name>
If neither selector matches, there is a catch-all state handler for unhandled
states.
As goofy as this sounds, it results in a clean separation of state handling
code.
Of course, I sometimes end up with a case switch inside the state handler
methods to handle the individual event responses. Here's a snippet from the
above QHsmMorphEventHandler (event is an instance variable that holds the
MorphicEvent being handled):
stateDragging: evt
^evt caseOf: {
[#entry] -> [self startDrag: event. nil].
[#exit] -> [self stopDrag: event. nil].
[#mouseUp] -> [ self returnToIdleHistory ]}
otherwise: [self state: #Global]
It's possible to hold states in variables, like here where we return to
history:
returnToIdleHistory
"Idle history has been saved; transition back to it (and clear it)"
| history |
history _ idleHistory ifNil: [ #Idle].
idleHistory _ nil.
mySource ~= myState
ifTrue: [self log: #newState items: {myState stateName. mySource stateName.
history. 'idle history' }]
ifFalse: [self log: #newState items: {myState stateName. history. 'idle
history'}].
^self newDynamicState: history.
The return value from the state handlers is either nil if there is no change
in state, or a QState object representing the new state when a transition is
desired.
Hope this helps,
--
Ned Konz
http://bike-nomad.com/squeak/
More information about the Squeak-dev
mailing list
|