SICP 3.1.1 Solutions
April 2, 2017 17:49
Our procedure needs to produce something that is itself a procedure, since the accumulator adds the value upon execution. To keep track of the value, I used a variable
sum that is adjusted using
set! on each call to the procedure that is returned. Because
sum is the argument originally passed to
make-accumulator, it only exists locally in the accumulator that is produced from that call. It will therefore be independent of any other accumulators generated by this procedure, as each call to
make-accumulator creates a different local
Here is the procedure in full:
This is modeled on the bank account procedures from the text. First, a local storage variable for the count is created (using
let). Then there are sub-procedures for each operation. Finally, there is the
dispatch procedure, which is what is actually returned.
Using sub-procedures allows
dispatch to be simpler in form, with just one line for each special action performed. When none of the special messages match, the default case occurs. That means the count is incremented and the original procedure is called. About the only difference between this
dispatch procedure and the one in
make-account from the text is that here the
else-case performs a delegation to the monitored procedure, and is the ‘normal path’. In
else is a catch-all for unrecognized input.
While this monitoring function definitely provides us a powerful method for tracking how procedures are used, it is a bit simplistic in this form. The most obvious drawback is that the monitored procedures must take one (and only one) argument. One way to deal with this limitation is demonstrated in the last exercise, when we put
make-monitored to use for testing the solution.
My intent in adding the password feature to the account was to make the least amount of changes to the original
make-account. To this end, only the new required
password argument is added, and it is handled in an
if statement, separate from the
cond of the original procedure. This is probably an influence on me of working in other programming languages, as it would be functionally equivalent to include this case as the first line of the
cond statement. Either way, the password is the first thing checked, and nothing else should be processed if it is not the one provided when the account was created.
A potentially tricky aspect of this procedure is deciding how the ‘complaint’ should be reported. The text suggests that a message is returned. However, in order for the procedure to function properly, we cannot just return a message straight from
dispatch. The way the account is used is that each interaction should resolve to a procedure that operates on a numeric value. Even though in this case that value should be ignored, it’s still necessary to return a procedure. My returned procedure is a lambda expression that takes one argument but just spits out the complaint instead of doing anything with the argument.
Since the variable that counts the number of attempts must be internal to each account, we should set it up using a
let statement, as was done in
make-monitored. To keep this variable separated from the dispatched procedures, the let that defines it wraps only around the
dispatch procedure itself. The password lockout exists in the ‘gateway’ portion of the verification process, and should only be defined and accessed at that point.
There are two mechanisms that must act on the attempt variable. Firstly, each failed attempt must increment it, so that when the limit is reached the
call-the-cops procedure is invoked. Secondly, there needs to be a way to reset it, so that only consecutive failures are flagged. This demonstrates the value of local state variables — having the account proceed functionally through multiple, nearly identical states until reaching failure would be unwieldy as well as unintuitive.
One question that is not handled in the text is what exactly should happen to the account once
call-the-cops has been called. Most modern systems with a similar limitation on login attempts go into a lockout state and often require additional action (or a timeout period) to unlock the account. The text doesn’t really require it, but I implemented a lockout by using a flag to show that
call-the-cops had already been invoked. Another approach might be just a check to see if the number of attempts has exceeded the limit, but I find using a second variable to convey the actual state of the account to be clearer.
To test it, we can use the
make-monitored function, since it can monitor whether the alarm procedure was called. In order to use it, however, we need to jump through some hoops. As previously mentioned,
make-monitored only works with arguments that take one procedure. To get around that, we do this:
To make the monitored version, we use a
lambda to create a function of a single argument. But since we don’t actually need the argument in
call-the-cops, the lambda ignores it and does nothing but run the original
call-the-cops instead (which we renamed to
old-ctc). Note that we check the count (when doing the testing) by actually calling
monitored-ctc with the special monitoring messages, not our new
call-the-cops. This slight logical hole is fine as long as we are able to keep track of what the name of the monitored procedure is, and can be sure that the new
call-the-cops is the only procedure that might make a call that changes the count.
The reason we had to redefine the procedure to call our monitored version is that the account will only call a procedure named
call-the-cops. On the one hand, this makes it easy to test our code without having to change the account procedure; we simply inserted the one that works for testing instead. On the other hand, it demonstrates one problem that arises when procedures can be arbitrarily renamed — simply replacing
call-the-cops with something else could render the account security procedure impotent. (This isn’t so much a worry about malicious hacking as it is about the ability of programmers to unintentionally wreck their own or others’ code).