I'm teaching from HtDP2 right now and I've got a bit of a... complaint? Maybe more of a request. The top-down design process starts to create a significant cognitive load when combined with the test-driven approach, especially in beginner students. But I might have a solution.
Here's the problem. Assuming I'm solving a non-toy problem, assuming I haven't yet fully broken down the problem, I'm doing top-down design as I go. And when I do get to the "write code" step of writing a function, I may see that there's something complex to be done---and I "wish" for a helper function. Now what? What does wishing entail? Well, I could do nothing but write it down on a piece of paper or in a comment. This is basically the technique described in HtDP. Unfortunately, this doesn't mesh well with iterative design; I want to be able to click Run frequently to verify that I don't have any "red-letter errors" (compiler errors and run-time errors), just test case failures. But functions that are just written on a wish list are not known to Racket and can't be called. As alternatives, I have two main options. 1) I can write a function stub, with minimal description and no test cases, just to make the error messages go away, and make sure I'm done with the original function (at least for now) before continuing on to write the helper functions. 2) I can write a description, signature, stub, and test cases for the helper function, and only then set it aside to add another item to the wish list or finish writing the original function. #2 is better in many ways, more in keeping with the test-driven philosophy and much less likely to be forgotten later (since the test cases will keep failing until I've fixed the function). However, if the first place I can "pause" writing the helper function is after I've worked out the test cases, I have *completely* lost my train of thought on the original function. Also, if I do anything wrong in terms of type mismatches, argument mismatches, or paren problems, by the time I get to click Run I have a lot more places to look for problems. In practice, #1 requires holding a lot less context in my head and eliminates the need to "pop the stack" (it is, if you will, a tail call); it's just that it's, well, unsafe. In a non-test-driven system I'd use something like #1, add a comment /* XXX */, and move on, but this is fairly antithetical to the HtDP style. Here's an example from a program I'm developing right now---I'm implementing the Space Invaders assignment I just gave to my students. I write this much: ;; defender-key : Defender KeyEvent -> Defender ; computes new defender position for given defender and given ; key; responds to left and right; stops at edges; moves by 10s (check-expect (defender-key 100 " ") 100) (check-expect (defender-key 100 "left") 90) (check-expect (defender-key 100 "right") 110) (check-expect (defender-key 0 "left") 0) (check-expect (defender-key WIDTH "right") WIDTH) (define (defender-key current key) (cond [(string=? key "left") ... and as I go to fill in the ... I see that it's slightly complex so I want to fill in with (defender-left current). I make a note of the need for a defender-left : Defender -> Defender in a comment, maybe with a little description. But if that's all I do, then when I've finished this function (where I also add a defender-right to the wish-list before I'm finished), if I click Run I still have errors. And even if I write out a bit of defender-left, including test cases, I can't test that until I've also written out a bit of defender-right. Before I'm done I've written more than twenty lines of stuff before I could click Run. And while I personally can handle that without trouble, the odds of any but my best students getting through that without any syntax errors is pretty low, and then they are in debugging hell. The solution is to make the experience more like #1 above, but without the unsafety. Here are three ways to do so, in decreasing level of implementation difficulty. PROPOSAL A: wish-for Add a construct to the tester library that reserves a to-be-defined function name and declares it as "wished for": (wish-for defender-left) (wish-for defender-right) This has two effects: first, the Test Results window/pane will report that these functions are wished for, and the implementation is therefore incomplete. Second, execution of any function that *calls* a wished-for function immediately halts with a check failure, giving a message like defender-key depends on unimplemented function defender-left in space-invaders.rkt, line 19, column 0 But this is a check failure, not a runtime error, and so subsequent tests can be run (i.e. the student can continue working on other parts of their program. This proposal is minimally intrusive into the thought process and lets the programmer finish the original function while just leaving a note to themselves about the helper; it corresponds most closely to the design process suggested in HtDP while preserving both "click-Run-ability" and check safety (where "check safety" is a property of a function that might be defined as "either the programmer would be satisfied that it is complete or else at least one test case fails"). PROPOSAL B: wish-stub Add a construct to the tester library that stubs a to-be-defined function name with a dummy value, and declares it as "wished for": (wish-stub defender-left 0) (wish-stub defender-right 0) Similar to A but includes a dummy value of the appropriate type. Implicitly declares a vararg function that always returns the stubbed dummy value a la (define defender-left (lambda arglst 0)) ; autodefined by wish-stub but also causes the Test Results window/pane to report these functions as wished-for. Compared to A, this version is only a slight added cognitive load for the programmer (having to understand the role of the helper well enough to give a valid dummy output value of the correct type), but it removes the need for program tracing and/or exception handling---since the helper will now return a valid value, the original function will simply have a normal check failure. However, the programmer doesn't have to worry about half-assing some test cases or omitting test cases and forgetting to fill in the stub later. PROPOSAL C: check-stub Add a construct to the tester library that declares a particular function as "wished for", essentially just an always-failing test case. (define (defender-left current) 0) (check-stub defender-left) This is a simplification of B that puts the onus back on the user to actually declare the function. The reason I prefer B to this is that it's more of an intrusion on the design process, and in particular, you need to either come up with good argument names right away or else toss in something you haven't thought about enough---and the whole goal of this proposal is to *minimise* the amount of stack that the (student) programmer needs to maintain. Also, if the user forgets to remove a wish-stub declaration when they define the function for real, this is easily identified as a conflict (because both are trying to define the function), while it's harder for a lingering check-stub to notice that the corresponding function has a "real" function body now. Sorry, that got long; I've been thinking about this for a few months now. Let me know what y'all think; I may try to put together a teachpack over winter break. -- -=-Don blaheta-=-dblah...@monm.edu-=-=-<http://www.monmsci.net/~dblaheta/>-=- This sentence contradicts itself---no actually it doesn't. --Douglas Hofstadter _________________________________________________ For list-related administrative tasks: http://lists.racket-lang.org/listinfo/users