Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generics #86

Draft
wants to merge 33 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.16
go-version: 1.18

- name: Test
run: go test -coverprofile=coverage.out ./...
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,6 @@ _testmain.go

# Testing
.coverprofile
coverage.out

.vscode
.vscode
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ default: services test

.PHONY: test
test:
go test ./...
CGO_ENABLED=1 go test ./... -v -race -coverprofile=coverage.out -covermode=atomic && go tool cover -func=coverage.out

.PHONY: lint
lint:
Expand Down
19 changes: 12 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@ import (
)

func main() {
fsm := fsm.NewFSM(
fsm := fsm.New[string, string](
"closed",
fsm.Events{
fsm.Events[string, string]{
{Name: "open", Src: []string{"closed"}, Dst: "open"},
{Name: "close", Src: []string{"open"}, Dst: "closed"},
},
fsm.Callbacks{},
fsm.Callbacks[string, string]{},
)

fmt.Println(fsm.Current())
Expand Down Expand Up @@ -77,14 +77,19 @@ func NewDoor(to string) *Door {
To: to,
}

d.FSM = fsm.NewFSM(
d.FSM = fsm.New[string, string](
"closed",
fsm.Events{
fsm.Events[string, string]{
{Name: "open", Src: []string{"closed"}, Dst: "open"},
{Name: "close", Src: []string{"open"}, Dst: "closed"},
},
fsm.Callbacks{
"enter_state": func(e *fsm.Event) { d.enterState(e) },
fsm.Callbacks[string, string]{
fsm.Callback[string, string]{
When: fsm.AfterAllStates,
F: func(cr *fsm.CallbackContext[MyEvent, MyState]) {
d.enterState(e)
},
},
},
)

Expand Down
20 changes: 11 additions & 9 deletions event.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,27 @@

package fsm

// Event is the info that get passed as a reference in the callbacks.
type Event struct {
import "golang.org/x/exp/constraints"

// CallbackContext is the info that get passed as a reference in the callbacks.
type CallbackContext[E constraints.Ordered, S constraints.Ordered] struct {
// FSM is an reference to the current FSM.
FSM *FSM
FSM *FSM[E, S]

// Event is the event name.
Event string
Event E

// Src is the state before the transition.
Src string
Src S

// Dst is the state after the transition.
Dst string
Dst S

// Err is an optional error that can be returned from a callback.
Err error

// Args is an optional list of arguments passed to the callback.
Args []interface{}
Args []any

// canceled is an internal flag set if the transition is canceled.
canceled bool
Expand All @@ -44,7 +46,7 @@ type Event struct {
// Cancel can be called in before_<EVENT> or leave_<STATE> to cancel the
// current transition before it happens. It takes an optional error, which will
// overwrite e.Err if set before.
func (e *Event) Cancel(err ...error) {
func (e *CallbackContext[E, S]) Cancel(err ...error) {
e.canceled = true

if len(err) > 0 {
Expand All @@ -57,6 +59,6 @@ func (e *Event) Cancel(err ...error) {
// The current state transition will be on hold in the old state until a final
// call to Transition is made. This will complete the transition and possibly
// call the other callbacks.
func (e *Event) Async() {
func (e *CallbackContext[E, S]) Async() {
e.async = true
}
60 changes: 35 additions & 25 deletions examples/alternate.go
Original file line number Diff line number Diff line change
@@ -1,66 +1,76 @@
//go:build ignore
// +build ignore

package main

import (
"fmt"

"github.com/looplab/fsm"
)

func main() {
fsm := fsm.NewFSM(
f := fsm.New(
"idle",
fsm.Events{
{Name: "scan", Src: []string{"idle"}, Dst: "scanning"},
{Name: "working", Src: []string{"scanning"}, Dst: "scanning"},
{Name: "situation", Src: []string{"scanning"}, Dst: "scanning"},
{Name: "situation", Src: []string{"idle"}, Dst: "idle"},
{Name: "finish", Src: []string{"scanning"}, Dst: "idle"},
fsm.Transistions[string, string]{
{Event: "scan", Src: []string{"idle"}, Dst: "scanning"},
{Event: "working", Src: []string{"scanning"}, Dst: "scanning"},
{Event: "situation", Src: []string{"scanning"}, Dst: "scanning"},
{Event: "situation", Src: []string{"idle"}, Dst: "idle"},
{Event: "finish", Src: []string{"scanning"}, Dst: "idle"},
},
fsm.Callbacks{
"scan": func(e *fsm.Event) {
fmt.Println("after_scan: " + e.FSM.Current())
fsm.Callbacks[string, string]{
fsm.Callback[string, string]{When: fsm.BeforeEvent, Event: "scan",
F: func(e *fsm.CallbackContext[string, string]) {
fmt.Println("after_scan: " + e.FSM.Current())
},
},
"working": func(e *fsm.Event) {
fmt.Println("working: " + e.FSM.Current())
fsm.Callback[string, string]{When: fsm.BeforeEvent, Event: "working",
F: func(e *fsm.CallbackContext[string, string]) {
fmt.Println("working: " + e.FSM.Current())
},
},
"situation": func(e *fsm.Event) {
fmt.Println("situation: " + e.FSM.Current())
fsm.Callback[string, string]{When: fsm.BeforeEvent, Event: "situation",
F: func(e *fsm.CallbackContext[string, string]) {
fmt.Println("situation: " + e.FSM.Current())
},
},
"finish": func(e *fsm.Event) {
fmt.Println("finish: " + e.FSM.Current())
fsm.Callback[string, string]{When: fsm.BeforeEvent, Event: "finish",
F: func(e *fsm.CallbackContext[string, string]) {
fmt.Println("finish: " + e.FSM.Current())
},
},
},
)

fmt.Println(fsm.Current())
fmt.Println(f.Current())

err := fsm.Event("scan")
err := f.Event("scan")
if err != nil {
fmt.Println(err)
}

fmt.Println("1:" + fsm.Current())
fmt.Println("1:" + f.Current())

err = fsm.Event("working")
err = f.Event("working")
if err != nil {
fmt.Println(err)
}

fmt.Println("2:" + fsm.Current())
fmt.Println("2:" + f.Current())

err = fsm.Event("situation")
err = f.Event("situation")
if err != nil {
fmt.Println(err)
}

fmt.Println("3:" + fsm.Current())
fmt.Println("3:" + f.Current())

err = fsm.Event("finish")
err = f.Event("finish")
if err != nil {
fmt.Println(err)
}

fmt.Println("4:" + fsm.Current())
fmt.Println("4:" + f.Current())

}
32 changes: 18 additions & 14 deletions examples/data.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
//go:build ignore
// +build ignore

package main
Expand All @@ -9,23 +10,26 @@ import (
)

func main() {
fsm := fsm.NewFSM(
fsm := fsm.New(
"idle",
fsm.Events{
{Name: "produce", Src: []string{"idle"}, Dst: "idle"},
{Name: "consume", Src: []string{"idle"}, Dst: "idle"},
fsm.Transistions[string, string]{
{Event: "produce", Src: []string{"idle"}, Dst: "idle"},
{Event: "consume", Src: []string{"idle"}, Dst: "idle"},
},
fsm.Callbacks{
"produce": func(e *fsm.Event) {
e.FSM.SetMetadata("message", "hii")
fmt.Println("produced data")
fsm.Callbacks[string, string]{
fsm.Callback[string, string]{When: fsm.BeforeEvent, Event: "sproduce",
F: func(e *fsm.CallbackContext[string, string]) {
e.FSM.SetMetadata("message", "hii")
fmt.Println("produced data")
},
},
"consume": func(e *fsm.Event) {
message, ok := e.FSM.Metadata("message")
if ok {
fmt.Println("message = " + message.(string))
}

fsm.Callback[string, string]{When: fsm.BeforeEvent, Event: "consume",
F: func(e *fsm.CallbackContext[string, string]) {
message, ok := e.FSM.Metadata("message")
if ok {
fmt.Println("message = " + message.(string))
}
},
},
},
)
Expand Down
61 changes: 61 additions & 0 deletions examples/generic.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//go:build ignore
// +build ignore

package main

import (
"fmt"

"github.com/looplab/fsm"
)

type MyEvent string
type MyState string

const (
Close MyEvent = "close"
Open MyEvent = "open"
Any MyEvent = ""

IsClosed MyState = "closed"
IsOpen MyState = "open"
)

func main() {
fsm := fsm.New(
IsClosed,
fsm.Transitions[MyEvent, MyState]{
{Event: Open, Src: []MyState{IsClosed}, Dst: IsOpen},
{Event: Close, Src: []MyState{IsOpen}, Dst: IsClosed},
},
fsm.Callbacks[MyEvent, MyState]{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can more types be inferred here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean with this ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven’t used generics in Go so much, it just felt there was some repetition in specifying the types. Thought maybe more types could be inferred, but that was more of an open question.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, type inference is the most complicated part in the generics of the go compiler, i tried making this a bit lighter by have a fluent interface for the FSM creation, something like:

fsm := fsm.New(initialState).WithTransitions(transitions).WithCallbacks(callbacks)

Methods usually make inference easier for the compiler, but it didnt work out.

fsm.Callback[MyEvent, MyState]{
When: fsm.AfterEvent, Event: Open,
F: func(cr *fsm.CallbackContext[MyEvent, MyState]) {
fmt.Printf("callback: event:%s src:%s dst:%s\n", cr.Event, cr.Src, cr.Dst)
},
},
fsm.Callback[MyEvent, MyState]{
When: fsm.AfterAllEvents,
F: func(cr *fsm.CallbackContext[MyEvent, MyState]) {
fmt.Printf("callback after all: event:%s src:%s dst:%s\n", cr.Event, cr.Src, cr.Dst)
},
},
},
)
fmt.Println(fsm.Current())
err := fsm.Event(Open)
if err != nil {
fmt.Println(err)
}
fmt.Println(fsm.Current())
err = fsm.Event(Close)
if err != nil {
fmt.Println(err)
}
fmt.Println(fsm.Current())
// Output:
// closed
// open
// closed
}
12 changes: 7 additions & 5 deletions examples/simple.go
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
//go:build ignore
// +build ignore

package main

import (
"fmt"

"github.com/looplab/fsm"
)

func main() {
fsm := fsm.NewFSM(
fsm := fsm.New(
"closed",
fsm.Events{
{Name: "open", Src: []string{"closed"}, Dst: "open"},
{Name: "close", Src: []string{"open"}, Dst: "closed"},
fsm.Transistions[string, string]{
{Event: "open", Src: []string{"closed"}, Dst: "open"},
{Event: "close", Src: []string{"open"}, Dst: "closed"},
},
fsm.Callbacks{},
fsm.Callbacks[string, string]{},
)

fmt.Println(fsm.Current())
Expand Down
Loading