We have extended Scheme in two main
directions: Turtle graphics and multithreading:
The turtle object lives on a board on which it can move and draw.
A turtle can also communicate with the board and "see" colors
or other turtles. Galapagos Scheme provides a set of primitives to create
and manage such turtles and the boards they live on.
A set of additional primitives enable the programmer to create multiple
interpreters that run concurrently. The environment model was extended
to support shared or thread-specific environments, and primitives to handle
multiple environments were also added.
A frame is a table which maps (or binds) names to values. An environment is a chained list of frames in which the interpreter works.
For example the command:
(define x 7)
creates a binding between x and the value 7.
A typical environment looks like this:
When a variable is evaluated, the interpreter first tries to find it
in the current frame. If a suitable binding can't be found, the interpreter
moves to the next frame in the environment and searched for a binding there.
In our example if we write x in the command line we'll get 12
as the answer because all such computations take place at the global environment.
A function application is always evaluated with respect to the environment the in which it was defined. (A function "holds" its environment, hence the name "closure") This means that if we write
(define (f) x)
and then
(f)
the result will be
12.
Under Galapagos's environment model, every interpreter works on its
branch of an "environments tree", but can also evaluate an expression
in any other environment in the tree. If two interpreters have a shared
node on the tree, then from that point up (towards the global environment)
all the bindings are the same for the two interpreters. The global environment
is shared by all interpreters. If an interpreter changes the value of some
variable, the binding is changed in all other interpreters that have access
to the frame where the variable was declared. Obviously, caution should
be taken when writing to a shared variable.
As an example, if a user created two new threads, called A and B, and then evaluated:
First thread> (define x 7)
(define (f) ...)
Thread A> (define (g) x)
Thread B> (define x 4)
this is how the environment model would look like:
Now consider what happens when the user evaluates this:
Thread A> (set! f g)
Now, evaluating (f) in thread B will call the function defined
in thread A. It will return 12 which is the value of x in f's environment.
If thread A defines a new x (using define), it will affect the result
of subsequent calls to f.
Sharing environments and frames are Galapagos's way of allowing shared
data. By default, however, most calculations are done at thread-specific
frames. (More on this subject in the next section). Primitives are supplied
to explicitly reference and manage environments: clone-environment
can create a copy of an environment (so changing a binding in one copy
does not alter the other); extend-environment and pop-environment
can add and remove frames to and from environments. More environment functions
are found in the next section of this document.
A thread is a SCHEME interpreter. The base environment of a thread
is the thread's topmost environment. All forms entered at the thread's
console or as arguments to new-thread are evaluated in the base
environment. In traditional SCHEME there is only one thread of execution.
It is a single thread and its base environment is the global environment.
In Galapagos many interpreters can run concurrently, and each have its
own environment (changing dynamically as computation progresses) and a
base environment (which can be explicitly changed).
One thread is created by Galapagos upon startup. It is called the main
thread, and it lasts for the whole Galapagos session. Its base environment
is the global one - much like a traditional Scheme interpreter. Additional
threads can be created using new-thread. These have their base environment
set to a new environment, containing a single fresh frame pointing to the
global environment. Any calculation done at the new thread's console takes
place in this thread-local environment. However, if closure functions are
used, then their evaluation may take place at the global environment or
at some other thread's space, depending on where that function was defined.
When a thread finishes the execution of its commands it terminates. There are two exceptions to this rule:
Threads can communicate using the predicate tell-thread or by
using the asynchronous message-queues system described later. In addition,
SCM's arbiters were improved to be multithread safe. An arbiter
object can serve as a guard on a critical section (where only one thread
can work at a given time) of the program. The relevant primitves are make-arbiter,
try-arbiter, and release-arbiter.
Note: Galapagos supports Scheme's notation of continuations. However, two restrictions apply:
A console is a text window where the user can type commands (when
the interpreter is idle) and see output. A console has a name which can
be changed dynamically, which appears on its title bar. A thread may or
may not be bound to a console. Information printed by a non-bound thread
is lost; a non-bound thread waiting for input is stuck (unless provisions
were made to allow a new console to be created by means of inter-thread
communications.) If a non-bound thread has nothing to do, instead of waiting
for input like a bound thread, it will terminate.
The main thread is always bound to a console. To bind a thread to a
console use the command bind-to-console from within the thread.
A board (window) is the graphical board on which the turtles and the
user draw. It is a bitmap on which the user can draw interactively, by
using the supplied GUI, or by creating turtles and instructing them to
do the drawings.
The controls of the graphical user interface are located on the toolbar.
The "shapes" of drawing are either a rectangle, a line or free
hand drawing. The user pen can have three widths which are found under
the User Pen menu. The numbers next to the sizes are the widths
in pixels.
The colors of the user pen can be changes either from the User Pen
menu or from the toolbar's color buttons. When choosing from a toolbar
button, the user can know exactly what color (in RGB format) is used.
Each window has a name which is written on its title bar which can be
changed using the rename-window! command. The board can be cleaned
from all drawings using the clear-window command.
Since the board is a bitmap it can be saved to a BMP file using the
save-bitmap command. A new background from a bitmap file can be
loaded using the load-bitmap command.
A turtle is an object which is connected to a certain board. Each turtle
has a pen with which it can draw on that board. The user can change the
state of the turtle's pen including giving it the color of the board's
background color (using the pen-erase! command). The pen will draw
when it is "down" and will not draw when it is "up".
Turtles can move between boards using the move-turtle-to-window
command.
Each turtle holds its location as the a pair of coordinates where 0,0
is the upper left corner of the board and 800,600 is the bottom-right corner
of the board. The turtle's location can be changed by giving the turtle
commands such as forward!, backwards!, move-to! or by using the
move button from the toolbar.
A turtle also has a heading which is the direction it will move on the
forward! command. The heading is in degrees, 0 meaning upwards and
increasing clockwise, thus 90 means rightwards and so on. A turtle's heading
can be changed by commands such as right!, left!, and set-heading!
or interactively, by using the move button on the toolbar and pressing
the control key.
A turtle can be visible on the board or be hidden. When it is visible
the user can decide if it wants a "solid" or "hollow"
turtle using the make-hollow! and make-solid! commands.
A turtle is always connected to a certain board, which is the board
it is drawing on. To create a new turtle on a board use the make-turtle
or the clone-turtle commands. A new turtle (created with the make-turtle
command) will be black, solid, heading north and with it's pen down, in
the center of the board. A cloned turtle will have exactly the same inner
state as its parent.
A turtle can also look at the board and see other turtles or colors. By using the command look the user can tell the turtle the area in which to look and what to look in this area.
Example:
The command
(look t 50 20 '(0 0 0))
will cause the turtle t to look for black pixels in the area in radius 50 from the turtle and 20 degrees to each side of the heading line, shown here in blue:
A turtle can have interrupts. Each interrupt is defined in terms of
turtle's vision. Whenever a turtle starts or stops seeing a given target,
a predefined message is sent to the thread which asked the interrupt. The
user can determine if the interrupt will notify on every change (like a
"can see"- "can't see" toggle) or only when one change
happens (only "can see" or "can't see" toggle). A message
can be any valid SCHEME object.
Example:
The command
(turtle-notify t "I-see" "I-don't-see" 50 20 color-black)
tells the turtle t that every time it sees the color black it should send the message "I-see" and when it doesn't see the color black any more it should send the message "I-don't-see".
Below is an example of what messages (or no message) t will send
while it is moving forward, encountering two black lines on its way:
If the interrupt was of kind "can see" (notify-when command)
only the "I-see" messages were sent, and if it was a the
"can't see" interrupt (notify-when-not command) only the
"I-can't-see" style messages were sent.
A special "user interrupt" is invoked when the user right-clicks
the turtle or a window. The notify-on-click command is used to enable
this.
A thread can tell the turtle to delete a certain interrupt using the
turtle-no-notify command. It can also tell the turtle to disable
all interrupts using the stop-notifying command.
In order to process the messages sent from a turtle's interrupt (or
from another thread, as presented later) the thread must install a message
handler which is a one argument function. If no handler is installed
the turtle's messages are lost. There can be only one handler per thread.
If the handler is installed then every time an interrupt is invoked, the
handler function in called with the message sent by the interrupt as its
argument.
Example:
We declare the function:
(define (my-print x) (write x) (newline))
and then install my-print as the current handler with:
(install-handler my-print)
from now on every message a turtle's interrupt sends will be printed
on screen. If my-print was installed in the previous example then
we would have seen:
"I-see"
"I-don't-see"
"I-see"
on the screen.
A good example to view the interrupts in action is to make a turtle
turn each time it sees a color and then let it wander aimlessly. Then use
the user pen to draw lines the turtle's way and watch it change direction.
See the PINGPONG.SCM file for a demo program.
The message-handler concept can be used as a mean of synchronous inter-thread
communications. The tell-thread functions sends a message to a thread,
which will be treated as an interrupt-generated message: It will cause
the thread's message handler function to be called with tell-thread's argument
as its own. This allows one thread to initiate a computation in a different
thread's environment and CPU space synchronously and without sharing environments.
In order to let threads communicate between themselves asynchronously
a system of message queues was developed. A queue is an object which stores
and relieves messages, stored in FIFO (First In First Out) style, meaning
messages are read in the order of arrival to the queue. Supported messages
are pairs of type and body, both of which can be any valid SCHEME object.
Looking for messages of a certain type is also supported, allowing implementation
of more flexible communication schemes than plain FIFO.
A thread can check if there are any messages in a given queue, if it
does get-message without checking first if there are messages in
the queue it will wait a given time-out or forever if no time-out is given.
If by the end of the time-out no message were in the queue the result will
be false.
Example:
In this example a queue q is created then a message is posted
into the queue and read from it.
(define q (make-queue))
(post-message q (cons 'type-circus 'bozo-the-clown))
(define msg (get-message q))
now the command
(car msg)
will give
type-circus
and the command
(cdr msg)
will yield
bozo-the-clown.