Whose Line is It, Anyway? By Shriram |
Say, you are part of a group of Shakespeare fans who “read” the Bard’s plays over the Internet. You like reading Shakespeare (who doesn’t?), but you hate typing in all those “quothe”s and “thine”s and what not (who doesn’t?). Being a Seasoned Schemer, you’re too smart to get suckered into typing all that text. Instead, you want to write an Internet agent that does the work for you. Let’s assume there are only three roles in the dialogs we want to read: yours and two others.
Exercise
Exercise 8.1. To wit, you will write a procedure with the following contract:
;; serve : Symbol Number Dialog String Number String Number → true
The arguments to serve
have the following meaning:
The first argument, a symbol, specifies which of the roles serve
will play.
The second argument, a port number, specifies on which port
serve
will receive messages.
The third argument is the dialog, which serve
reads from the
file that you created in exercise 7.6.
The fourth and fifth arguments designate the port number and machine name for an agent playing one of the other two characters. Which character this other agent plays doesn’t matter.
The sixth and seventh arguments designate the port number and machine name for an agent playing the remaining character.
The supplementary teachpack provides two functions:
Add the
teachpack hamsup.scm |
;;install-listener : Number (Sexpr → true) → true
;; installs a procedure that is invoked each time when a ;; message shows up on a specific port number (define (install-listener listener) ...) ;;send-message : String Number Sexpr → true
;; send a message to a machine at a certain port (define (send-message machine port msg) ...)
Here are some more specifics on install-listener
. The procedure
given to install-listener
receives the content of the message,
which can be any S-expression. To standardize, we will use the following
packet format:
(list symbol (list string) number)
The first component is the name of a character; the second is a list of strings indicating the lines in a part for that character; and the third is a counter, which we will discuss below. When your agent receives a message, it should print the name and dialog that your Internet buddies sent.
Hint: The functions display
and , available in the
io
teachpack, which we installed for
exercise 7.6. The first writes an S-expression to a port and
produces true
; the second writes a new line to a port and produces
true
, too. A TA will help you put these together.
The number dictates which part of the dialog (assumed to be shared by
all three agents) comes next—indices begin at 0. Therefore,
your agent needs to look up the dialog to determine who is supposed to
say that part. If it’s someone else, do nothing (the agent can just
return a dummy value, like 'do-nothing
). Otherwise, your
agent needs to transmit the Bard’s eternal words. It does this by
using the library procedure
send-message : machine-name port-number s-expression → <ignore>
It needs to send a message to each of the other two machine/port
combinations that were given as arguments to serve
. The
s-expression in this case is a packet in the format specified above.
Important: The number your agent specifies in the packet it
transmits must be one bigger than the number it received!
This process works well once initiated, but it’s impossible to initiate! (A message only gets sent in response to an incoming one, so if you never receive a message....) Therefore, we shall designate one kind of packet as special: ones of the form
(fake () 0)
If the number field is 0, that means the message is being sent only to initiate the dialog. Ideally, you should not bother printing out the character’s name and dialog in this case (since they are nonsensical values), but don’t worry about this initially.
As an example, a dialog using the Hamlet prose might look like this:
machineG: running
(serve 'GUILDENSTERN 7000 dialog "machineR" 7001 "machineH" 7002)
machineR: running
(serve 'ROSENCRANTZ 7001 dialog "machineG" 7000 "machineH" 7002)
machineH: running
(serve 'HAMLET 7002 dialog "machineR" 7001 "machineG" 7000)
Someone executes
(send-message "machineG" 7000 '(fake () 0))
(sending the same message to the other two should have no effect).
On receiving this, machineG transmits
(list 'GUILDENSTERN '("My honoured lord!") 1)
to the other two; machineR then transmits
(list 'ROSENCRANTZ '("My most dear lord!") 2)
to the other two; machineH then transmits
(list 'HAMLET '("My excellent good friends! How dost thou," "Guildenstern? Ah, Rosencrantz! Good lads, how do ye both?") 3)
to the other two; machineG then transmits
(list 'ROSENCRANTZ '("As the indifferent children of the earth.") 4)
to the other two; and so forth.
Your own machine always has the name "localhost"
. You can
actually run more than one agent on your own machine, just by using
different port numbers (therefore, in the example above, you could
replace "machineH"
, "machineG"
and
"machineR"
with "localhost"
).
Port numbers below 1024 are reserved for those with administrative privilege. In general, the lower the port number, the more programs likely to be vying for it. Choose large numbers below 65536.
Don’t try to write your agent all in one go! First write a very
simple listener that receives any kind of packet at all, and prints a
simple message. Use send-message
to test it. Make sure you
get a feel for how the two library procedures work. Add functionality
little-by-little.
Eventually, we’d like you to “play Hamlet” with two other people in
the class. To test, however, you will want to play all three roles
yourself. Once you’ve written your version of serve
, you
might be inclined to run it three times from the Interactions window
of DrScheme. If the main body of serve
invokes
install-listener
, you’re in luck: install-listener
creates a new thread
, so when you run serve
you
ought to see a response like this:
> (serve 'HAMLET 7002 dialog "machineR" 7001 "machineG" 7000) true >
with a fresh prompt awaiting your input.