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)))