Copyright © 2002, 2003 by Gene Michael Stover. All rights reserved. Permission to copy, store, & view this document unmodified & in its entirety is granted.
It's a library for writing programs that are based on an event-driven architecture. Flez is written in Common Lisp. Programs which use it will probably be written in Common Lisp. Flez should be appropriate for use in simulations & games.
Several reasons:
It's a library for event-driven programs, so I thought about calling it Evie, but I already used that name for my simulated evolution library. What's like ``Evie'' but different? Well, F follows E, & Flez has the same number of letters as Evie.
If you don't like (or don't believe that explanation1), then maybe F.L.E.Z. stands for Fabulous Lisp Event Zystem.
In any case, the library is called Flez.
Gene Michael Stover wrote Flez. The same bloke maintains it.
Either way, you'll have a directory called flez-3/ in the current directory.
I guess this section is a tutorial. I'm also trying to convince people that event-driven architectures are worthwhile. (Think ``Threads bad.'' Say it again: ``Threads baaaad!'')2
It's easy to use Flez in its most basic form. Here are some examples.
Assume you've already created all your classes & objects.3 Also, you have a global function, done-p, which returns truth (non-nil) when it's time for the program to exit the event loop & quit. Then to run your program, just use a loop like the one in Figure 1.
That's about the most basic event loop you need. Its argument is an instance of class cybertiggyr-flez:flez. Each time it calls Flez's tick function, Flez processes all events that are ready. Those events might create & post other events, which Flez will queue until the next time you call tick.
The simple loop has a couple of deficiencies.
The first deficiency is that it is a busy loop. It calls Flez's tick function even if no events are ready. If we don't need to process asynchronous inputs, it would be more efficient to sleep until it's time for the next event. A loop with the well-behaved sleep behaviour is in Figure 2.
Flez's seconds-to-next function returns the number of seconds until the next event. As sleeping-basic-loop shows, your program can use that value to sleep until it's time for the event, thereby doing a better job of sharing the CPU with other programs.
If your program needs to handle asynchronous input (or output), you'll need to use some function other than sleep. You'll need a Lisp equivalent of Unix's select function to poll some I/O channels & return when a channel is ready or on a timeout. I won't give an example of that there.
sleeping-basic-loop has one more problem. What if the event queue is empty? Flez allows you to schedule repeating events, but what if you didn't do that & the event queue emptied because none of the most recently executed events generated more events? Then your program would loop forever. So we need to make the event loop recognize an empty event queue & exit when it happens. Figure 3 shows such a loop.
best-basic-loop is the best I'm going to show now. It does not take into account asynchronous input, so it wouldn't be good for a game, but it could be great for a simulation.
When the event queue is empty, cybertiggyr-flez:seconds-to-next returns 0, so if the event queue is empty after the last tick, the sleep function won't take any time, & then the until exit clause of the loop will be true, & the loop will exit.
So much for event loops. What about events themselves?
Flez knows about two kinds of events: singular & repeating. A singular event happens just once & then is discarded. A repeating event is automatically reschedule to occur again after each use. Repeating events are more complex & won't be covered in this basic section.
A singular event has two main parts: a time & a closure. (It also has a tag, but that's not for basic use.)
You schedule an event by calling Flez's post function. post needs to know the event's closure & the time until the event executes. The closure argument is required; without a closure, you don't have an event. The time argument is optional & defaults to ``soonest''. (An event will never be executed during the tick in which it is queued. See Section 7.1 for a discussion.)
The time units are your own business. If you are calling Flez's tick function without any arguments, it uses the internal real time, which are seconds, so the delays you give Flez must be seconds. You could also use your own clock, especially if you are using simulation time.
You must write your own event loop, but it's easy. There are two basic kinds of loops: real time & simulation time.
In simulation time, you control the clock. You might think of the time in seconds (or whatever), but remember that those seconds are not the same as real seconds.
With simulation time, you don't need to call sleep in your event loop because you probably want the simulation to run as quickly as possible. What's more & as I already said, even if your simulation's time is in seconds, those seconds are not the same as real seconds, so calling sleep on them is not applicable.
This section is a reference manual for the Application Programmer's Interface (API) of CyberTiggyr Flez.
Package cybertiggyr-flez contains all of the symbols for CyberTiggyr Flez.
The symbols it exports are
[1]> (load "/home/gene/lib/lisp/cybertiggyr/tigris.lisp") ;; Loading file /home/gene/lib/lisp/cybertiggyr/tigris.lisp ... ;; Loading of file /home/gene/lib/lisp/cybertiggyr/tigris.lisp is finished. T [2]> (load "src/flez.lisp") ;; Loading file src/flez.lisp ... ;; Loading of file src/flez.lisp is finished. T [3]> (use-package 'cybertiggyr-flez) T [4]>
create-flez
object
None.
Creates a Flez, which is currently implemented as a structure, & returns the new instance.
[3]> (use-package 'cybertiggyr-flez)
T
[4]> (create-flez)
#S(CYBERTIGGYR-FLEZ::FLEZ
:HEAP
#S(CYBERTIGGYR-TIGRIS:HEAP
:LESS-FN
#<CLOSURE CYBERTIGGYR-FLEZ::MESSAGE-LESS-P
(CYBERTIGGYR-FLEZ::X CYBERTIGGYR-FLEZ::Y)
(DECLARE (SYSTEM::IN-DEFUN CYBERTIGGYR-FLEZ::MESSAGE-LESS-P))
(BLOCK CYBERTIGGYR-FLEZ::MESSAGE-LESS-P
(LET
((CYBERTIGGYR-FLEZ::WHEN-X
(CYBERTIGGYR-FLEZ::MESSAGE-WHEN CYBERTIGGYR-FLEZ::X))
(CYBERTIGGYR-FLEZ::WHEN-Y
(CYBERTIGGYR-FLEZ::MESSAGE-WHEN CYBERTIGGYR-FLEZ::Y)))
(OR (< CYBERTIGGYR-FLEZ::WHEN-X CYBERTIGGYR-FLEZ::WHEN-Y)
(AND (= CYBERTIGGYR-FLEZ::WHEN-X CYBERTIGGYR-FLEZ::WHEN-Y)
(< (CYBERTIGGYR-FLEZ::MESSAGE-SERIAL CYBERTIGGYR-FLEZ::X)
(CYBERTIGGYR-FLEZ::MESSAGE-SERIAL CYBERTIGGYR-FLEZ::Y))))))>
:ORDER 2 :A #(NIL) :MAX-COUNT 0)
:NOW NIL :SERIAL 0 :CANCELLED-COUNT 0)
[5]> (setq fz (create-flez))
#S(CYBERTIGGYR-FLEZ::FLEZ
:HEAP
#S(CYBERTIGGYR-TIGRIS:HEAP
:LESS-FN
#<CLOSURE CYBERTIGGYR-FLEZ::MESSAGE-LESS-P
(CYBERTIGGYR-FLEZ::X CYBERTIGGYR-FLEZ::Y)
(DECLARE (SYSTEM::IN-DEFUN CYBERTIGGYR-FLEZ::MESSAGE-LESS-P))
(BLOCK CYBERTIGGYR-FLEZ::MESSAGE-LESS-P
(LET
((CYBERTIGGYR-FLEZ::WHEN-X
(CYBERTIGGYR-FLEZ::MESSAGE-WHEN CYBERTIGGYR-FLEZ::X))
(CYBERTIGGYR-FLEZ::WHEN-Y
(CYBERTIGGYR-FLEZ::MESSAGE-WHEN CYBERTIGGYR-FLEZ::Y)))
(OR (< CYBERTIGGYR-FLEZ::WHEN-X CYBERTIGGYR-FLEZ::WHEN-Y)
(AND (= CYBERTIGGYR-FLEZ::WHEN-X CYBERTIGGYR-FLEZ::WHEN-Y)
(< (CYBERTIGGYR-FLEZ::MESSAGE-SERIAL CYBERTIGGYR-FLEZ::X)
(CYBERTIGGYR-FLEZ::MESSAGE-SERIAL CYBERTIGGYR-FLEZ::Y))))))>
:ORDER 2 :A #(NIL) :MAX-COUNT 0)
:NOW NIL :SERIAL 0 :CANCELLED-COUNT 0)
[6]> (type-of fz)
CYBERTIGGYR-FLEZ::FLEZ
[7]> (flez-count fz)
0
[8]>
Whatever. I mean, None.
None that I know.
Whatever.
None.
flez-cancel flez tag
(values)
Remove from the event queue all events whose tag slot is EQUAL to tag. It is not an error if flez is empty. Does not remove other events from the Flez.
[8]> (setq fz (create-flez)) #S(CYBERTIGGYR-FLEZ::FLEZ ; Lots of details removed for clarity ) ;; Make some events with a tag of A [11]> (dotimes (i 3) (flez-post fz #'(lambda (flex when) nil) :tag 'a)) NIL ;; Make some events with a tag of B. [12]> (dotimes (i 2) (flez-post fz #'(lambda (flez when) nil) :tag 'b)) NIL ;; See... there are 5 events in the Flez. [13]> (flez-count fz) 5 ;; Remove the A events. The two B events will ;; remain. [14]> (flez-cancel fz 'a) NIL [15]> (flez-count fz) 2 ;; Removing the two B events. [16]> (flez-cancel fz 'b) NIL ;; The Flez is now empty. [17]> (flez-count fz) 0 [18]>
Nothing.
None.
Nothing.
None.
flez-cancel-if flez fn
(values)
Remove from the event queue all events for which FN returns true (non-NIL). It is not an error if flez is empty. Does not remove other events from the Flez.
[8]> (setq fz (create-flez)) #S(CYBERTIGGYR-FLEZ::FLEZ ; Lots of details removed for clarity ) ;; Make some events with a tag of A [11]> (dotimes (i 3) (flez-post fz #'(lambda (flex when) nil) :tag (list i 'a))) NIL ;; Make some events with a tag of B. [12]> (dotimes (i 2) (flez-post fz #'(lambda (flez when) nil) :tag (list 'b i))) NIL ;; See... there are 5 events in the Flez. [13]> (flez-count fz) 5 ;; Remove the A events. The two B events will ;; remain. [14]> (flez-cancel fz #'(lambda (tag) (member 1 tag)) NIL [15]> (flez-count fz) 2 ;; Removing the two B events. [16]> (flez-cancel fz 'b) NIL ;; The Flez is now empty. [17]> (flez-count fz) 0 [18]>
Nothing.
None.
Nothing.
None.
flez-clear flez
flez
Removes any & all items from the FLEZ. Affects the FLEZ by removing all the items from it. Returns the FLEZ. The FLEZ will be empty.
[1]> (load "/home/gene/lib/lisp/cybertiggyr/tigris.lisp") ;; Loading file /home/gene/lib/lisp/cybertiggyr/tigris.lisp ... ;; Loading of file /home/gene/lib/lisp/cybertiggyr/tigris.lisp is finished. T [2]> (load "src/flez.lisp") ;; Loading file src/flez.lisp ... ;; Loading of file src/flez.lisp is finished. T [3]> (use-package 'cybertiggyr-flez) T [4]> (defvar *fz* (create-flez)) *FZ* [5]> (dotimes (i 5) (flez-post *fz* #'(lambda (flez when) (print i)))) NIL [6]> (flez-count *fz*) 5 [7]> (flez-clear *fz*) 0 [8]> (flez-count *fz*) 0 [9]>
Whatever. I mean, None.
None that I know.
Whatever.
None.
flez-count flez
count
Returns the number of items in the Flez event queue. An empty Flez contains zero items. If you insert one item into the Flez & do not call flez-tick, do not cancel the event, & do not call flez-clear on it, FLEZ-COUNT on that Flez will return 1.
FLEZ-COUNT should never return a negative number.
[1]> (load "/home/gene/lib/lisp/cybertiggyr/tigris.lisp") ;; Loading file /home/gene/lib/lisp/cybertiggyr/tigris.lisp ... ;; Loading of file /home/gene/lib/lisp/cybertiggyr/tigris.lisp is finished. T [2]> (load "src/flez.lisp") ;; Loading file src/flez.lisp ... ;; Loading of file src/flez.lisp is finished. T [3]> (use-package 'cybertiggyr-flez) T [4]> (defvar *fz* (create-flez)) *FZ* [5]> (dotimes (i 5) (flez-post *fz* #'(lambda (flez when) (print i)))) NIL [6]> (flez-count *fz*) 5 [7]> (flez-clear *fz*) 0 [8]> (flez-count *fz*) 0 [9]>
Whatever. I mean, None.
None that I know.
Whatever.
None.
flez-empty-p flez
bool
Returns true if the Flez event queue is empty. A Flez is empty if it contains exactly zero items.
FLEZ-EMPTY-P is probably faster than ``(zerop (flez-count flez))''.
[1]> (load "/home/gene/lib/lisp/cybertiggyr/tigris.lisp") ;; Loading file /home/gene/lib/lisp/cybertiggyr/tigris.lisp ... ;; Loading of file /home/gene/lib/lisp/cybertiggyr/tigris.lisp is finished. T [2]> (load "src/flez.lisp") ;; Loading file src/flez.lisp ... ;; Loading of file src/flez.lisp is finished. T [3]> (use-package 'cybertiggyr-flez) T [4]> (defvar *fz* (create-flez)) *FZ* [5]> (flez-empty-p *fz*) T [6]> (dotimes (i 5) (flez-post *fz* #'(lambda (flez when) (print i)))) NIL [7]> (flez-empty-p *fz*) NIL [8]> (flez-empty-p (flez-clear *fz*)) T [9]>
Whatever. I mean, None.
None that I know.
Whatever.
None.
flez-next-when flez
time
Returns the time at which the next event is scheduled to execute. If the Flez event queue is empty, returns NIL.
[1]> (load (translate-logical-pathname "CL-LIBRARY:CYBERTIGGYR;TIGRIS.LISP")) ;; Loading file /home/gene/lib/lisp/cybertiggyr/tigris.lisp ... ;; Loading of file /home/gene/lib/lisp/cybertiggyr/tigris.lisp is finished. T [2]> (load (translate-logical-pathname "CL-LIBRARY:CYBERTIGGYR;FLEZ.LISP")) ;; Loading file /home/gene/lib/lisp/cybertiggyr/flez.lisp ... ;; Loading of file /home/gene/lib/lisp/cybertiggyr/flez.lisp is finished. T [3]> (use-package 'cybertiggyr-flez) T [4]> (defvar *fz* (create-flez)) *FZ* [5]> (flez-post *fz* #'(lambda (flez when) nil) :when 42) #S(CYBERTIGGYR-FLEZ::MESSAGE :WHEN 42 :SERIAL 1 :CLOSURE #<CLOSURE :LAMBDA (FLEZ WHEN) NIL> :TAG NIL :CANCELLED NIL) [6]> (flez-next-when *fz*) 42 [7]> (flez-post *fz* #'(lambda (flez when) nil) :delay 17) #S(CYBERTIGGYR-FLEZ::MESSAGE :WHEN 17 :SERIAL 2 :CLOSURE #<CLOSURE :LAMBDA (FLEZ WHEN) NIL> :TAG NIL :CANCELLED NIL) [8]> (flez-next-when *fz*) 17 [9]>
Whatever. I mean, None.
None that I know.
Whatever.
None.
flez-now flez
time
Return the time of the currently executing event or the most recently executed event.
Whatever. I mean, None.
None that I know.
Whatever.
None.
flez-post flez closure &key (delay 0)
when tag
flez
Schedules an event in a Flez event queue for execution at a specific time.
Closure is the event. It's a function of two arguments, like this:
#'(lambda (flez when)
(format t "~&I am an event.")
(format t "~&The event queue is ~A." flez)
(format t "~&The current game time is ~A." when))
The time at which the event will execute is
, where
is the keyword argument
from flez-post or is the current time (flez-now)
of the Flez if that keyword argument was not specified.
is the keyword argument from flez-post or
is zero if that keyword argument was not specified.
Both
&
should be numbers, but their
scale & range is irrelevant to Flez except that smaller
values should occur before larger ones. In other words,
Flez doesn't know or care whether the time scale is
microseconds, seconds, fortnights, or whatever, nor does it
care whether they are expressed as integers or floating
point as long as smaller numbers indicate earlier times.
The optional tag can be used to identify the event in case it should be deleted from the Flez later. It may be an atom, a list, the closure itself, or whatever. The tag value doesn't matter to flez-post; it might only matter to flez-cancel or to flez-cancel-if.
;; I need an example here.
Whatever. I mean, None.
None that I know.
Whatever.
None.
flez-task flez closure first
period tag tag
nil
Schedules an event in a Flez event queue so that it is executed at first & then every period ticks after that until or unless cancelled.
Closure is the event. It's a function of two arguments, like this:
#'(lambda (flez when)
(format t "~&I am an event.")
(format t "~&The event queue is ~A." flez)
(format t "~&The current game time is ~A." when))
The tag is not optional for tasks because it is the only way to cancel them. If you insist that you won't need to cancel the task, it can be nil.
;; I need an example here.
Whatever. I mean, None.
None that I know.
Whatever.
None.
flez-tick flez &optional time-now (count
0)
crap
Executs events that are ready. Doesn't return a useful value; maybe it should.
;; I need an example here.
Whatever. I mean, None.
None that I know.
Whatever.
None.
An event that is posted is never executed during the tick in which it is posted, even if it is scheduled for ``now'' or a time in the past. Why?
It's to avoid endless loops. (Flez can't truly prevent endless loops, but it can take measures to avoid them.)
If an event was queued immediately when it was posted, it would be possible, even easy, for a single call to Flez's tick to become an endless loop. So while tick is active, Flez places new events in a temporary queue. Just before tick returns, it moves the events from the temporary queue into the real queue.
If you post an event while not inside a tick, the event is placed in the real queue immediately.
When you ask Flez how many items are in the queue, it reports the size of the real queue & the size of the temporary queue. So from the outside of tick, you can't distinguish between the real queue & the temporary queue.
These are my notes as a programmer while creating & maintaining Flez.
Created the project by copying from library/src/skeleton/ to this directory.
Created src/flez.lisp & some test programs.
Picking up where I left-off months ago.
Created doc/flez.tex, which is this file. Wrote a lot in it.
Damn few notes, eh.
Gene Michael Stover 2008-04-20