Skip to content

Instantly share code, notes, and snippets.

@jackfirth
Last active July 16, 2021 16:01
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jackfirth/81336ed48a0668643c9f0c680d5d56ad to your computer and use it in GitHub Desktop.
Save jackfirth/81336ed48a0668643c9f0c680d5d56ad to your computer and use it in GitHub Desktop.
The Function Design Recipe from How to Design Programs (second edition)

The Function Design Recipe

The function design recipe is a series of steps to follow when creating a function. The primary purpose of these steps is to help you think through the problem you're trying to solve. A secondary purpose is to make you write down explanations of what you're trying to do so that other programmers, especially programmers you ask for help, can understand your code.

In this article, we will be applying the function design recipe to the following example problem:

The state of Tax Land has created a three-stage sales tax to cope with its budget deficit. Inexpensive items, those costing less than $1,000, are not taxed. Luxury items, with a price of more than $10,000, are taxed at the rate of eight percent (8.00%). Everything in between comes with a five percent (5.00%) markup.

Design a function for a cash register that, given the price of an item, computes the sales tax.

Step 1: Choose Your Data Representation

Write a comment expressing how you wish to represent information as data. When the problem statement distinguishes different classes of input information, you need carefully formulated data definitions.

A data definition must use distinct clauses for each subclass of data. Each clause specifies a data representation for a particular subclass of information. The key is that each subclass of data is distinct from every other class, so that our function can proceed by analyzing disjoint cases.

Our sample problem deals with prices and taxes, which are usually positive numbers. It also clearly distinguishes three ranges:

; A price is a number representing how many US dollars an item costs.
; Prices fall into one of three intervals:
; — 0 through 1000
; — 1000 through 10000
; — 10000 and above.

; A sales tax is a number representing how many US dollars should be
; added on to the price of an item to pay for taxes.

Step 2: Signature, Header, and Purpose Statement

Before implementing a function, it's important to simply describe the function. This means giving the function and its parameters meaningful names, writing a comment describing what kinds of data the function consumes and produces, and writing a comment describing the function's purpose. These pieces of information are called the function's header, signature, and purpose statement, respectively.

A function signature is a comment that tells the readers of your design how many inputs your function consumes, from which classes they are drawn, and what kind of data it produces. In our Tax Land problem, we want a function that takes the prices of an item and returns the sales tax of the item. As we wrote down in Step 1, both price and sales tax are represented by numbers. Therefore, our function's signature states that it accepts one number and returns one number.

; Number -> Number

A header is a simplistic function definition, also called a stub. Pick the name of the function, then pick one variable name for each class of input in the signature. The body of the function can be any piece of data from the output class. Note that we're not trying to code the function yet, we just want to get the names and data classes right. It's fine to have a function body that is obviously useless as long as it matches the signature's return type. Put the header just below the signature comment.

; Number -> Number
(define (compute-sales-tax price)
  0)

Finally, a purpose statement is a comment that summarizes the purpose of the function in a single sentence. If you are ever in doubt about a purpose statement, write down a short answer to the question "what does the function compute?" Every reader of your program should understand what your functions compute without having to read the function itself. The purpose statement should go after the signature but before the header.

; Number -> Number
; Computes the amount of sales tax charged for a given price.
(define (compute-sales-tax price)
  0)

If you completed Step 2 correctly, you should be able to hit the Run button in DrRacket without any errors.

Step 3: Examples

Illustrate the signature and the purpose statement with some functional examples. To construct a functional example, pick one piece of data from each input class from the signature and determine what you expect back.

It is imperative that you pick at least one example from each sub-class in the data definition. Also, if a sub-class is a finite range, be sure to pick examples from the boundaries of the range and from its interior. Since our sample data definition involves three distinct intervals, let’s pick all boundary examples and one price from inside each interval and determine the amount of tax for each: 0, 537, 1000, 1282, 10000, and 12017. Try to calculate the tax for each of these prices by hand. Here is our first attempt:

0 537 1000 1282 10000 12017
0 0 ??? 64.1 ??? 961.36

The question marks point out that the problem statement uses the vague phrase “those costing less than $1,000” and “more than $10,000” to specify the tax table. While a programmer may jump to the conclusion that these words mean “strictly less” or “strictly more,” the lawmakers may have meant to say “less than or equal to” or “more than or equal to,” respectively. Being skeptical, we decide here that Tax Land legislators always want more money to spend, so the tax rate for $1,000 is 5% and the rate for $10,000 is 8%. A programmer at a tax company would have to ask a tax-law specialist.

Once we've worked out examples, write each example as a comment after the function's purpose statement and before the function's header. Each example should look like an interaction with the DrRacket REPL.

; Number -> Number
; Computes the amount of sales tax charged for a given price.
; Examples:
; > (compute-sales-tax 0)
; 0
; > (compute-sales-tax 537)
; 0
; > (compute-sales-tax 1000)
; 50
; > (compute-sales-tax 1282)
; 64.1
; > (compute-sales-tax 10000)
; 800
; > (compute-sales-tax 12017)
; 961.36
(define (compute-sales-tax price)
  0)

You should be able to copy the example expressions like (compute-sales-tax 1282) into the DrRacket REPL to see if your function works as you expect it to.

Step 4: Write a Template

The next step is to take inventory, to understand what are the givens and what we need to compute. For the simple functions we are considering right now, we know that they are given data via parameters. While parameters are placeholders for values that we don’t know yet, we do know that it is from this unknown data that the function must compute its result. To remind ourselves of this fact, we replace the function’s body with a template.

The choice of template depends on the kind of data the function accepts. In general, the template mirrors the organization of data subclasses with a cond.

This slogan means two concrete things. First, the function’s body must be a conditional expression with as many clauses as there are distinct subclasses in the data definition. If the data definition mentions three distinct subclasses of input data, you need three cond clauses; if it has seventeen subclasses, the cond expression contains seventeen clauses. Second, you must formulate one condition expression per cond clause. Each expression involves the function parameter and identifies one of the subclasses of data in the data definition:

(define (compute-sales-tax price)
  (cond
    [(and (<= 0 price) (< price 1000)) ...]
    [(and (<= 1000 price) (< price 10000)) ...]
    [(>= price 10000) ...]))

The ellipses represent the parts of the template that will be filled in during the next step.

Step 5: Fill In the Template

When you have finished the template, you are ready to define the function. This step involves filling in each of the ellipses from the template in the previous step. In our example, each cond line has an ellipses representing the result for that case of the input price. For each line, first determine which examples matter for that line. Then, write down the computation for an expression that involves the function parameter, checking your work by running the examples in the DrRacket REPL. Ignore all other possible kinds of input data when you work on one line; the other cond clauses take care of those.

(define (compute-sales-tax price)
  (cond
    [(and (<= 0 price) (< price 1000)) 0]
    [(and (<= 1000 price) (< price 10000)) (* price 0.05)]
    [(>= price 10000) (* price 0.08)]))

Step 6: Test Cases

Once you're confident you have a working function that satisfies your examples, it's important to turn the examples into test cases. If you are using one of student languages in the How to Design Programs book, such as Beginning Student Language, use check-expect to check that each example's input produces the example's expected output:

(check-expect (compute-sales-tax 0) 0)
(check-expect (compute-sales-tax 537) 0)
(check-expect (compute-sales-tax 1000) 50)
(check-expect (compute-sales-tax 1282) 64.1)
(check-expect (compute-sales-tax 10000) 800)
(check-expect (compute-sales-tax 12017) 961.36)

If you aren't using the HtDP student languages and are using ordinary #lang racket, instead use the rackunit testing library. Additionally, put your tests into a test submodule and group them into a test case named after your function:

(module+ test
  (require rackunit)

  (test-case "compute-sales-tax"
    (check-expect (compute-sales-tax 0) 0)
    (check-expect (compute-sales-tax 537) 0)
    (check-expect (compute-sales-tax 1000) 50)
    (check-expect (compute-sales-tax 1282) 64.1)
    (check-expect (compute-sales-tax 10000) 800)
    (check-expect (compute-sales-tax 12017) 961.36)))

In either case, after writing your tests click the Run button in DrRacket. This will run your tests, automatically checking that your function produces the correct output for each input.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment