I'm writing my first Go program, an SMTP server, and I thought that using a FSM to represent the state transitions of the network protocol would be elegant. I really liked this haskell SMTP FSM example so I loosely modeled it after that.
I've created a simple FSM type which takes a transition table as its constructor argument and has a Run method which takes an event and will call the appropriate handler function it matches in the state table. I then have a "Session" type which is what needs to utilize the FSM after processing incoming SMTP commands from its connection.
Here is what a FSM Transition looks like:
type Transition struct {
from State
event Event
to State
handler func() string
}
Then, in my Session object, I'm forced to define the transition table in its constructor so I can have access to its methods for the transition actions:
func (s *SmtpSession) NewSession() {
transitions := []Transition{
{Initial, Rset, Initial, sayOk},
{HaveHelo, Rset, HaveHelo, sayOk},
{AnyState, Rset, HaveHelo, resetState},
...
{Initial, Data, Initial, needHeloFirst},
{HaveHelo, Data, HaveHelo, needMailFromFirst},
{HaveMailFrom, Data, HaveMailFrom, needRcptToFirst},
{HaveRcptTo, Data, HaveData, startData},
}
smtpFsm = StateMachine.NewMachine(transitions)
}
This seems wasteful to have to create an instance of this FSM as part of each session, when all sessions will have essentially the same FSM. I'd much rather just have some sort of "static" FSM which can be given a transition table, and then the Run method will take a current state, and an event and return the resulting "action" function.
However, that is where I run into trouble. Because all of the handler functions are actually methods of the Session object, I must define it within Session. I can't think of a way I can define this transition table only once for all sessions and still have the appropriate access to the Session handler functions I need.
If I wrote this program in a straight procedural style then I wouldn't have any of these problems. The FSM would have access to all the handler functions directly.
The only thing I can think of is altering my FSM to not return function pointers, but instead return some arbitrary constant that the Session will then map to the appropriate function:
var transitions = []Transition{
{Initial, Rset, Initial, "sayOk"},
{HaveHelo, Rset, HaveHelo, "sayOk"},
{AnyState, Rset, HaveHelo, "resetState"},
...
}
var smtpFsm = NewStateMachine(transitions)
func (s *Session) handleInput(cmd string) {
event := findEvent(cmd)
handler := findHandler(smtpFsm.Run(s.currentState, event))
handler(cmd)
}
func (s *Session) findHandler(handlerKey string) {
switch handlerKey {
case "sayOk":
return sayOk
case "resetState":
return resetState
}
}
This will fix the problem of having to re-intialize a new FSM for each session but it also feels a little hackish. Does anyone have any suggestions for how I might avoid this problem? Here is a link to the incomplete Session.go that demonstrates the problem.