Quirksand

SICP

SICP 3.1.1 Solutions

April 2, 2017 17:49

Ex. 3.1

Since the accumulator gives a value upon execution, our procedure needs to produce something that is itself a procedure. To keep track of the value, I used a variable sum that is adjusted using set! on each call to the accumulator. 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 sum variable.

Ex. 3.2

Here is the procedure in full:

(define (make-monitored proc)
  (let ((count 0))
    (define (how-many-calls?)
      count
      )
    
    (define (reset-count)
      (set! count 0)
      )
    
    (define (dispatch arg)
      (cond
        ((eq? arg 'how-many-calls?)  (how-many-calls?))
        ((eq? arg 'reset-count) (reset-count))
        (else
         (set! count (+ count 1))
         (proc arg)
         )
        )
      )
    dispatch
    )
  )

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 make-account the 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.

Ex. 3.3

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.

Ex. 3.4

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:

(define old-ctc call-the-cops)

(define monitored-ctc (make-monitored 
                       (lambda (x) 
                         (old-ctc)
                         )
                       )
  )

(define (call-the-cops)
  (monitored-ctc '() )
  )

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).

Solutions 3.1.1