The How to Design Functions (HtDF) recipe is a design method that enables systematic design of functions. We will use this recipe throughout the term, although we will enhance it as we go to solve more complex problems.
The HtDF recipe consists of the following steps:
Throughout the HtDF process be sure to "run early and run often"[*]. Run your program as often as you can, and always when you finish writing a definition, or a test. The more often you press run the sooner you can find mistakes. Finding mistakes one at a time is much easier than waiting until later when the mistakes can compound and be more confusing. Run, run, run!
Note that you must stick to following the steps in the order listed, except that sometimes it is easier to write examples first, then do signature and purpose. Often at some point during the design you may discover an issue or boundary condition you did not anticipate, at that point go back and update the purpose and examples accordingly. But you should never write the function definition first and then go back and to the other recipe elements -- for some of you, that will work with simple functions; no one can do that successfully for complex functions!
Write the function signature, a one-line purpose statement and a function stub.
Note that the stub is a syntactically complete function definition that produces a value of the right type. If the type is Number it is common to use 0, if the type is String it is common to use "a" and so on. The value will not, in general, match the purpose statement. In the example below the stub produces 0, which is a Number, but only matches the purpose when times2 happens to be called with 0.
;; times2 : Number -> Number ;; produces n times 2 (define (times2 n) ; this is the stub 0)
The purpose of the stub is to serve as a kind of scaffolding to make it possible to run the examples even before the function design is complete. With the stub in place check-expects that call the function can run. Most of them will fail, of course, but the fact that they can run at all allows you to find and correct small mistakes in the examples such as unbalanced parentheses, calling a function with the wrong name or number of arguments etc.
Write at least one example of a call to the function and the expected result the call should produce.
You will often need more examples, to help you better understand the function or to properly test the function. (If once your function works and you run the program some of the code is highlighted in black it means you definitely do not have enough examples.) If you are unsure how to start writing examples use the combination of the function signature and the data definition(s) to help you generate examples. Often the example data from the data definition is useful, but it does not necessarily cover all the important cases for a particular function.
The first role of an example is to help you understand what the function is supposed to do. If there are boundary conditions be sure to include an example of them. If there are different behaviours the function should have, include an example of each. Since they are examples first, you could write them in this form:
;; (times2 0) should produce 0 ;; (times2 1) should produce 2 ;; (times2 2) should produce 4
When you write examples it is sometimes helpful to write not just the expected result, but also how it is computed. For example, you might write the following instead of the above:
;; (times2 0) should produce (* 0 2) ;; (times2 1) should produce (* 1 2) ;; (times2 2) should produce (* 2 2)
While the above form satisfies our need for examples, DrRacket gives us a better way to write them, by enclosing them in check-expect. This will allow DrRacket to check them automatically for when the function is complete. (This will turn them into unit tests.)
;; times2 : Number -> Number ;; produces n times 2 (check-expect (times2 0) (* 0 2)) (check-expect (times2 1) (* 1 2)) (check-expect (times2 3) (* 3 2)) (define (times2 n) ; this is the stub 0)
The existence of the stub will allow you to run the tests. Most of the tests will fail of course, since the stub is returning the same value every time. But you will at least be able to check that the parentheses are balanced, that you have not miss-spelled function names etc.
Before coding the function body it is helpful to have a clear sense of what the function has to work with -- what are all the parts in your parts bag for this function? The template provides this.
For primitive data like numbers, strings and images the body of the template is simply (... x ...) where x is the name of the parameter to the function. Once the template is done the stub should commented out.
;; times2 : Number -> Number ;; produces n times 2 (check-expect (times2 0) (* 0 2)) (check-expect (times2 1) (* 0 2)) (check-expect (times2 3) (* 0 2)) ;(define (times2 n) 0) ; this is the stub (define (times2 n) ; this is the template (... n ...))
NOTE: Starting in week 2 templates are produced as part of the How to Design Data Definitions (HtDD) recipe. You should simply copy the template from the data definition to the function and rename it. It is also often useful to add constant values which are extremely likely to be useful to the template body at this point. For example, the template for a function that renders the state of a world program might have an MTS constant added to its body.
Now complete the function body by filling in the template.
Note that:
You should use all of the above to help you code the function body. In some cases further rewriting of examples might make it more clear how you computed certain values may make it easier to code the function.
;; times2 : Number -> Number ;; produces n times 2 (check-expect (times2 0) (* 0 2)) (check-expect (times2 1) (* 0 2)) (check-expect (times2 3) (* 0 2)) ;(define (times2 n) 0) ; this is the stub ;(define (times2 n) ; this is the template ; (... n ...)) (define (times2 n) (* 2 n))
Run the program and make sure all the tests pass, if not debug until they do.
Many of the problems you might have had will already have been fixed because of following the "run early, run often" approach. But if not, debug until everything works.
Note that each of these steps build on all the ones that precede it.