11 Mutable State
Purpose: Learn about set! and set-box! in ASL by building a big-bang wrapper
Wrapping big-bang
In this lab, we are going to construct a debugging helper for writing big-bang programs. This will allow us to add logging messages, display them on the screen (next to the normal output of big-bang), pause & resume the debug log (in case many messages are being logged), and filter messages.
Goals:
Understand how mutation can be used to express programs that would be difficult (or impossible) to write otherwise. In this case, the key is that we are able to use unchanged big-bang programs within our wrapper.
Exercise 1 First, we want you to find an existing big-bang program. If you aren’t sure which to choose, we suggest starting with the Conway’s game of life simulation from Lab 9. Copy all the code you wrote (either partners) into your file to start.
Exercise 2 Next, we want to start defining our wrapper. We can’t define the special notation that big-bang uses, so instead, we will write a function with normal positional arguments for all the parameters to big-bang that we will support. Unlike with big-bang, none of these will be optional, but clients can always pass in handlers that don’t do anything (e.g., identity for on-tick, etc).
For this wrapper, we want to support on-tick, with an interval, on-mouse, on-key, and stop-when (with the optional draw argument). Write the signature for a function, wrapped-big-bang that takes an initial world state (of type %X), a draw handler, a tick handler, a tick interval, a mouse handler, a key handler, a stop predicate, and a draw handler to use after stopping. For now, all the wrapper should do is invoke big-bang with all of those arguments.
Exercise 3 Now, rewrite your main from the big-bang program you imported to use your wrapped version. If you don’t need particular handlers, pass in versions that don’t do anything.
Make sure that you can run the program using your wrapper before moving on.
Exercise 4 Our first change is to add some space in our display for our debugging output. You should do that by making a wrapped-draw that will be what you pass to big-bang. This should call the actual draw handler, and then it should add a "debugging panel" to the image before returning it. Our recommendation is to make the panel the same height, and next to, the output of the program being wrapper (this will make certain things easier later). You can decide how wide it should be: at least a couple hundred pixels.
Exercise 5 Since you’ve made your canvas bigger, you’ve now made there be more places that the user can click. But clicking anywhere in your debugging panel really shouldn’t pass on mouse events to the wrapped program (if you hadn’t added the panel, those events wouldn’t have existed). So add a wrapped-mouse that checks the x,y coordinate and only passes through events that should go through. Depending on where you placed your panel, you may have to modify the values that you pass through to the original mouse handler so that it thinks its canvas is the whole display.
Exercise 6 Next, let’s add debugging messages. First, add a global variable that will hold messages, and functions to add a message and a function to read all messages. We did this in class on Monday, if you want to review.
Exercise 7 Now that you can add messages from the program, we want to display them in the debugging panel. Think about how to take a list of messages and render them into an image that fits in the panel. A few suggestions:
(text/font some-string 18 "color" "monospace" "default" 'normal 'normal #f)
Will render text that is 11 pixels wide per character (if you change the size to something other than 18, it may be different. Also, those tests were on our machine: it may be worth verifying using image-width that this is true on yours). It will render multi-line text fine if there are "\n" inserted in the text, so you may want to add those to messages that are too long. Finally, crop/align will allow you to cut an image that is too large into an appropriate size, and cut from the bottom if you want.
At this point, you should be able to run your program and see log messages on the side, assuming you add some to your program! Great!
Exercise 8 Let’s make the messages a little more readable: let’s include when they were sent. Add the number of seconds since the beginning of the big-bang program to the beginning of the message. Think about how you could include that. Can you add it when rendering? You can get the current time, in seconds (since a very long time ago) with (current-seconds); how do you use that to give the number of seconds since the start of the wrapped program?
Exercise 9 Now let’s add the ability to pause the log. It might be that you see a message that you wonder about, so you want to be able to click a button and have the log stop accepting messages until you click it again. In your debugging panel, shrink the space for your messages by a bit to make room for a "button": our recommendation is to just make this be the top or bottom N pixels of the panel. Render this as "pause" (or a picture equivalent) if the log is running, and "resume" (or a picture equivalent) if the log is paused (how can the log be paused or resumed? More state!).
Ensure that the log behaves correctly when paused (ignores messages) or running (as before). Now you want to make clicking on the button switch from paused to running and back. To do this, we need to go back to our wrapped-mouse handler: now in the case you aren’t passing on a mouse event, you actually need to do something if the event is "button-down" and it is in the area that corresponds to your button. If so, toggle the pause state.
Exercise 10 This is pretty cool, but we can do better. Let’s add the ability to filter messages. Again, we’ll shrink the space for messages and give ourselves a spot for adding a filtering message. We now want to capture key events and add them to a message to filter, and use that to restrict which log messages we display. But how do we decide if a key event (say, the letter "x") should cause our filtering predicate to add an "x" to the end, vs passing "x" to the wrapped program?
One way is to have a notion of "selecting" the filtering box, which we could do by clicking on it. So add "adding-filter" state, and if you click on the filter box, toggle it on. This should work just like your pause/resume button. But now, in your wrapped-key, if that state is #t, you add keys to the current filter, whereas if it is #f, you pass them to the original key handler. You can also make it so that when you toggle it off, it clears the filter text.
Before you go...
If you had trouble finishing any of the exercises in the lab or homework, or just feel like you’re struggling with any of the class material, please feel free to come to office hours and talk to a TA for additional assistance.