I have been making this FSM today. However, as this is probably the biggest practical program I have ever written in CL, I don't know if there are some things that could be improved, or if using a closure is the best thing here.
Any feedback is appreciated.
;;; States: -- EnterMineAndDigForNugget
;;; -- QuenchThirst
;;; -- GoHomeAndSleepTillRested
;;; -- VisitBankAndDepositGold
(defmacro while (test &rest body)
`(do ()
((not ,test))
,@body))
(defmacro enter (state)
`(setf state ,state))
(defun make-miner ()
(let ((wealth 0)
(thirst 0)
(rested 0)
(state 'EnterMineAndDigForNugget))
(list
(defun EnterMineAndDigForNugget ()
; (setf location 'mine)
(format t "~&Diggin' up gold!")
(incf wealth)
(incf thirst)
(cond ((>= thirst 7) (enter 'QuenchThirst))
(t (enter 'VisitBankAndDepositGold))))
(defun QuenchThirst ()
(format t "~&Drinkin' old good whiskey")
(setf thirst 0)
(enter 'EnterMineAndDigForNugget))
(defun VisitBankAndDepositGold ()
(format t "~&All this gold ought to be stored somewhere!")
(incf wealth)
(cond ((>= wealth 5) (progn
(format t "~&Too much gold for today, let's sleep!")
(enter 'GoHomeAndSleepTillRested)
(setf wealth 0)))
(t (EnterMineAndDigForNugget))))
(defun GoHomeAndSleepTillRested ()
(while (<= rested 3)
(format t "~&Sleepin'")
(incf rested))
(enter 'EnterMineAndDigForNugget)
(setf rested 0))
(defun controller ()
(dotimes (n 30)
(cond ((equal state 'QuenchThirst) (QuenchThirst))
((equal state 'VisitBankAndDepositGold) (VisitBankAndDepositGold))
((equal state 'GoHomeAndSleepTillRested) (GoHomeAndSleepTillRested))
((equal state 'EnterMineAndDigForNugget) (EnterMineAndDigForNugget))))))))
EDIT
I have applied all the suggested changes but for the flet/labels one. Everything worked fine until I changed the one of "set the state to the next function". Now, the macro enter
doesn't seem to be ever called.
This is the current state of the code, with the required code to make it work
;;; States: -- enter-mine-and-dig-for-nugget
;;; -- quench-thirst
;;; -- go-home-and-sleep-till-rested
;;; -- visit-bank-and-deposit-gold
(defmacro enter (state)
`(setf state ,state))
(defun make-miner ()
(let ((wealth 0)
(thirst 0)
(rested 0)
(state #'enter-mine-and-dig-for-nugget))
(list
(defun enter-mine-and-dig-for-nugget ()
(format t "~&Diggin' up gold!")
(incf wealth)
(incf thirst)
(if (>= thirst 7)
(enter #'quench-thirst)
(enter #'visit-bank-and-deposit-gold)))
(defun quench-thirst ()
(format t "~&Drinkin' old good whiskey")
(setf thirst 0)
(enter #'enter-mine-and-dig-for-nugget))
(defun visit-bank-and-deposit-gold ()
(format t "~&All this gold ought to be stored somewhere!")
(incf wealth)
(if (>= wealth 5)
(progn
(format t "~&Too much gold for today, let's sleep!")
(enter #'go-home-and-sleep-till-rested)
(setf wealth 0))
(enter #'enter-mine-and-dig-for-nugget)))
(defun go-home-and-sleep-till-rested ()
(dotimes (i 4)
(format t "~&Sleepin'"))
(enter #'enter-mine-and-dig-for-nugget))
(defun controller ()
(dotimes (n 30)
(funcall state))))))
(let ((a (make-miner)))
(funcall (fifth a)))
-
\$\begingroup\$ The code tag doesn't keep all the formatting, but in my real code it is a good one \$\endgroup\$gumbo– gumbo2011年09月03日 00:41:41 +00:00Commented Sep 3, 2011 at 0:41
-
\$\begingroup\$ Because there is no code tag. Just indent 4 spaces (the button or Ctrl+K) and text will be formatted as code. \$\endgroup\$Ed Swangren– Ed Swangren2011年09月03日 07:49:29 +00:00Commented Sep 3, 2011 at 7:49
-
\$\begingroup\$ well, by code tag I meant the 4-indentation. But it still shows 4 lines slightly out of place \$\endgroup\$gumbo– gumbo2011年09月03日 21:36:23 +00:00Commented Sep 3, 2011 at 21:36
-
\$\begingroup\$ THe nested DEFUN is definitely wrong. You need to get rid of that. The indenting also looks wrong. Let the editor indent the code, add four spaces in front of the each line and paste that into the text field. \$\endgroup\$Rainer Joswig– Rainer Joswig2011年09月10日 01:08:34 +00:00Commented Sep 10, 2011 at 1:08
-
\$\begingroup\$ I want to get rid of the DEFUN, but I need to make sure to change one thing at a time, to make sure everything still works. Also, the indentation is managed by EMACS+SLIME, but somehow I can't make it look right here \$\endgroup\$gumbo– gumbo2011年09月10日 09:06:49 +00:00Commented Sep 10, 2011 at 9:06
4 Answers 4
DEFUN
The most basic mistake in your code is that DEFUN
is not correct for nested functions. DEFUN
is a top-level macro, defining a top-level function and should be used in top-level forms.
Nested functions are defined in Common Lisp with FLET
and LABELS
. LABELS
is used for recursive sub-functions.
Naming
Symbols like FooBarBaz
are not use in Common Lisp. By default Common Lisp upcases all names internally, so the case information gets lost.
Usually we write foo-bar-baz
.
Checking symbols
Use CASE
(or ECASE
) instead of (cond ((equal foo 'bar) ...))
.
Architecture
Usually I would write that piece of code using CLOS, the Common Lisp Object System.
In a more functional style I would propose the following:
use
LABELS
for the local procedures.set the state to the next function. A function is written as
(function my-function)
or#'my-function
.the controller just calls the next function from the state variable. It is not necessary to list all cases.
-
\$\begingroup\$ Edited OP, need some help \$\endgroup\$gumbo– gumbo2011年09月09日 23:09:00 +00:00Commented Sep 9, 2011 at 23:09
-
\$\begingroup\$ Both answers are very good. I chose yours because it was the one I found more helpful \$\endgroup\$gumbo– gumbo2011年09月11日 22:15:04 +00:00Commented Sep 11, 2011 at 22:15
-
\$\begingroup\$ What does 'DEFUN is not thought for nested functions' mean? \$\endgroup\$James McMahon– James McMahon2013年07月29日 01:54:49 +00:00Commented Jul 29, 2013 at 1:54
-
2\$\begingroup\$ @James McMahon: DEFUN inside DEFUN is not a useful concept in Lisp. Local functions are defined by FLET or LABELS. \$\endgroup\$Rainer Joswig– Rainer Joswig2013年07月29日 06:24:28 +00:00Commented Jul 29, 2013 at 6:24
As of your second edit.
Ok, yeah now that you've shown how this is supposed to be called, CLOS is definitely the way to go here. Representing an object as a list of closures is less than optimal, if for no other reason that you then need to resort to (funcall (fifth a-miner))
in order to actually start it up (and consequently change that call every time you add a new "method" to your miner). There's also the problem that each "miner
" you instantiate carries around its own independent functions (so good luck changing behavior at runtime or using any kind of inheritance for more complex modeling).
I suggest you take a look at this CLOS tutorial as well as chapters 16 and 17 of Practical Common Lisp
Here's a transliteration of your program using CLOS to act as a learning aid:
(defpackage :cl-miner (:use :cl))
(in-package :cl-miner)
(defclass miner ()
((wealth :accessor wealth :initarg :wealth :initform 0)
(thirst :accessor thirst :initarg :thirst :initform 0)
(rested :accessor rested :initarg :rested :initform 0)
(state :accessor state :initarg :state :initform #'enter-mine-and-dig-for-nugget)))
;;;;;;;;;;;;;;; shortcuts
(defmethod enter ((m miner) a-state)
(setf (state m) a-state))
(defun make-miner ()
(make-instance 'miner))
;;;;;;;;;;;;;;; miner methods
(defmethod enter-mine-and-dig-for-nugget ((m miner))
(format t "~&Diggin' up gold!")
(incf (wealth m))
(incf (thirst m))
(if (>= (thirst m) 7)
(enter m #'quench-thirst)
(enter m #'visit-bank-and-deposit-gold)))
(defmethod quench-thirst ((m miner))
(format t "~&Drinkin' old good whiskey")
(setf (thirst m) 0)
(enter m #'enter-mine-and-dig-for-nugget))
(defmethod visit-bank-and-deposit-gold ((m miner))
(format t "~&All this gold ought to be stored somewhere!")
(incf (wealth m))
(if (>= (wealth m) 5)
(progn
(format t "~&Too much gold for today, let's sleep!")
(enter m #'go-home-and-sleep-till-rested)
(setf (wealth m) 0))
(enter m #'enter-mine-and-dig-for-nugget)))
(defmethod go-home-and-sleep-till-rested ((m miner))
(dotimes (i 4)
(format t "~&Sleepin'"))
(enter m #'enter-mine-and-dig-for-nugget))
(defmethod work ((m miner))
(dotimes (n 30)
(funcall (state m) m)))
(let ((a (make-miner)))
(work a))
I was going to compare them for performance, but I couldn't get your revised version to run (it gave me an "unhandled memory fault" error which is something I haven't seen before).
Can you comment on what the goal/output of this program is supposed to be? How you're supposed to use it? It looks like make-miner
just returns a list of functions, which doesn't sound useful on its own.
Some preliminary comments:
You can cut the while
macro. CL provides you with many iteration constructs (do
, dotimes
, dolist
, mapcar
and friends, and loop
). That last one can do what you want here (and lots more if you feel like reading);
(loop while (<= rested 3)
do (format t "~&Sleepin'")
do (incf rested))
Since you reset rested
to 0 later in the same block anyway, you could also just do
(loop repeat 4 do (format t "~&Sleepin'"))
or
(dotimes (i 4) do (format t "~&Sleepin'"))
Common Lisp convention is to use dashed-lower-case-names
rather than CamelCaseNames
.
cond
clauses don't need an explicit progn
, but read on first
Wherever you've got (cond ([test] [clause]) (t [clause]))
, you can instead write (if [test] [clause] [clause])
. For example,
(if (>= wealth 5)
(progn
(format t "~&Too much gold for today, let's sleep!")
(enter 'GoHomeAndSleepTillRested)
(setf wealth 0))
(EnterMineAndDigForNugget))
You do actually need the progn
here. In general, you should use cond
where you'd normally use if
/elseif
/else
in other languages (case
is actually the switch
analogue), and use if
/when
/unless
where you can to signal intent.
-
\$\begingroup\$ Thanks, I'll read the links you provided and correct the code. Also, I'm aware of the naming convention, and even use it myself, but I was following an AI book (Programming AI by example), so I kept it the way it appeared in the book \$\endgroup\$gumbo– gumbo2011年09月03日 22:56:36 +00:00Commented Sep 3, 2011 at 22:56
If your FSM hardly ever changes, and performance is your primary concern, you can't beat this.
Translate it into simple structured code, in a progn
or whatever, which you can do with any FSM.
Here's the "C-ish" equivalent:
w = th = 0;
while(T){
w++; th++;
// Diggin' up gold!
if (th >= 7){
// Drinkin' old good whiskey
th = 0;
} else {
// all this gold ought to be stored somewhere
if (w >= 5){
// Too much gold for today, let's sleep!
w = 0;
for (r = 0; r <= 3; r++){
// sleepin'
}
}
}
}