vendor/state-machine.lisp @ 9fada4d535fc
State machine, vendoring, etc
author |
Steve Losh <steve@stevelosh.com> |
date |
Tue, 02 Aug 2016 14:31:39 +0000 |
parents |
(none) |
children |
(none) |
;;;; https://bitbucket.org/tarballs_are_good/lisp-random/src/a57dc2cd93ee6aa689d9b68f3f6eb62f7dd67cd2/state-machine.lisp
(defpackage #:state-machine
(:use #:cl)
(:export
#:state-machine
#:invoke-state-machine
#:transition))
(in-package #:state-machine)
;;;; state-machine.lisp
;;;;
;;;; Copyright (c) 2015 Robert Smith
;;;;
;;;; This file is an attempt to build a relatively efficient state
;;;; machine abstraction.
;;;; Bugs: SBCL complains about modifying constant data. I don't know if it is right or if I am right.
;;; (ql:quickload :alexandria)
(defclass state-machine ()
((states :initarg :states
:reader state-machine-states
:documentation "A list of the names of each state.")
(transition-graph :initarg :transition-graph
:reader state-machine-transition-graph
:documentation "A hash table containing the state names as keys and a list of possible transitions as values.")
(invocation-function :initarg :invocation-function
:reader state-machine-invocation-function
:documentation "The multivariate function to invoke to enter the state machine.")))
(defgeneric invoke-state-machine (sm &rest args)
(:method ((sm state-machine) &rest args)
(apply (state-machine-invocation-function sm) args)))
(eval-when (:compile-toplevel :load-toplevel :execute)
(defun contains-duplicates-p (list &key (test 'eql))
(/= (list-length list)
(list-length (remove-duplicates list :test test))))
)
(defmacro transition (state &rest args)
(declare (ignore state args))
(error "Transition is an internal macro to DEFINE-STATE-MACHINE."))
(defmacro state-machine (args state-definitions &body body)
"Create a state machine which takes the arguments ARGS the body BODY. Return a state machine object which can be invoked with the arguments ARGS.
STATE-DEFINITIONS is a list of state definitions. It looks much like the definition of a LABELS function definition:
(state-name arguments
[documentation]
[declarations]
forms ...)
Within the state definitions, however, there is a macro defined called TRANSITION which transitions to one of the other states immediately without return. (The use of TRANSITION ensures that a tail call will happen.)
BODY can enter the state machine
\(It is allowed, but discouraged, to bypass the use of TRANSITION by simply calling the state name as a function instead.)"
(let ((states (mapcar #'first state-definitions))
(transition-graph (make-hash-table)))
(labels ((construct-state (state-def)
(destructuring-bind (name args . body) state-def
(multiple-value-bind (forms decls doc)
(silt.quickutils:parse-body body :documentation t)
;; This is a LABELS function definition.
`(,name ,args
,@(and doc (list doc))
,@decls
(macrolet ((transition (state &rest args)
(unless (member state ',states)
(error "The state ~S in the transition ~
occurring in the state ~S ~
is not a valid."
state
',name))
(pushnew state
(gethash ',name ,transition-graph))
`(return-from ,',name
(,state ,@args))))
,@forms))))))
(when (contains-duplicates-p states)
(warn "There are duplicate state names in the state machine."))
(multiple-value-bind (forms decls doc)
(silt.quickutils:parse-body body :documentation t)
`(make-instance
'state-machine
:states ',(remove-duplicates states)
:transition-graph ',transition-graph
:invocation-function
(lambda ,args
,@(and doc (list doc))
,@decls
(labels ,(mapcar #'construct-state state-definitions)
(macrolet ((transition (state &rest args)
(unless (member state ',states)
(error "The state ~S in the initial transition ~
is not a valid."
state))
(pushnew state
(gethash nil ,transition-graph))
`(,state ,@args)))
,@forms))))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; Examples ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defparameter *beer-machine*
(state-machine (count)
((decide (current-count)
(if (= 1 current-count)
(transition last-case)
(transition general-case current-count)))
(general-case (current-count)
(format t "~D bottles of beer on the wall, ~D bottles of beer.~%~
Take one down, pass it around, ~D bottle~:P of beer on ~
the wall...~%"
current-count
current-count
(1- current-count))
(transition decide (1- current-count)))
(last-case ()
(format t "If that one bottle should happen to fall, what a waste ~
of alcohol!")))
(transition decide count)))
(defparameter *tail-call-test*
(state-machine (x)
((even (x)
(if (zerop x)
t
(transition odd (1- x))))
(odd (x)
(if (zerop x)
nil
(transition even (1- x)))))
(even x)))