Coursenotes index | CSC 123 Introduction to Community Action Computing

The Function Design Recipe

This page presents (a version of) the Function Design Recipe from the textbook How to Design Programs. Specifically it is a TypeScript re-telling of this document from the Racket website.

We have so far learned about what functions are and how to read code involving functions. However, to build software systems, you also need to design and implement functions.

When faced with a programming problem prompt, it can be tempting to just start coding. However, this can lead to all sorts of trouble:

This page presents a recipe (a set of steps) to follow when designing and implementing functions. When you’re working on a problem and you get stuck, try following this recipe before asking for help. Even if that doesn’t get you unstuck, this recipe can help you articulate the specific step with which you need help.

For the remainder of this document, let’s use the following programming problem prompt as a running example:

Write a function that takes in a temperature in degrees Fahrenheit and converts it to degrees Celsius.

Steps in the Design Recipe

1. Function signature

The signature of your function is everything that describes what your function does, but not how it does it. This includes:

For our function, the pieces would be:

We can write all of this down to build up our function.

/**
 * Converts a temperature in degrees Fahrenheit to degrees Celsius.
 */
const fahrenheitToCelsius = (fahrenheit: number): number => {

};

The purpose statement is written as a comment above the function. Notice that it doesn’t give information about the formula for the conversion. It only says WHAT the function does, not HOW it does it.

The name, parameter type, and return type are included in the function’s declaration and type annotations. (The benefit of explicitly including these annotations in TypeScript is that our code becomes self-documenting.)

2. Examples

We have already said what our function is expected to do. However, to make this “what” explicit, we can include examples.

In the comment above the function, include a few examples showing what the correct output of the function is for an example input (or inputs, if the function has multiple parameters). For example:

/**
 * Converts a temperature in degrees Fahrenheit to degrees Celsius.
 * fahrenheitToCelsius(86) should be 30
 */
const fahrenheitToCelsius = (fahrenheit: number): number => {

};

There is no specific formatting requirement for these examples. What’s important is that they are clear and precise.

Examples are effective for two reasons:

3. Implementation template

In this step, we finally shift our focus to the body of the function. Going from the signature and examples to a working implementation can be a big leap. It’s often useful to break down our plan for solving the problem by writing down a template.

For our fahrenheitToCelsius function, the implementation is rather small: we need to use the formula for converting a temperature from Fahrenheit to Celsius.

/**
 * Converts a temperature in degrees Fahrenheit to degrees Celsius.
 * fahrenheitToCelsius(86) should be 30
 */
const fahrenheitToCelsius = (fahrenheit: number): number => {
  // Use the formula (F - 32) * 5/9 to convert `fahrenheit` to a value in celsius
  // Return the result
};

Again, there is no specific formatting requirement. I like to write steps as comments in my code—I often remove the comments when the time comes for implementation.

Templates tend to be more useful when a function is more complex, e.g., when it requires more steps, or when the input data is more complex.

4. Implementation

In this step, we fill out the function body. The template from the previous step should give us our implementation plan. If you find yourself not knowing how to implement a specific step, consider going back to the template and breaking down the step into smaller steps.

/**
 * Converts a temperature in degrees Fahrenheit to degrees Celsius.
 * fahrenheitToCelsius(86) should be 30
 */
const fahrenheitToCelsius = (fahrenheit: number): number => {
  // Use the formula (F - 32) * 5/9 to convert `fahrenheit` to a value in celsius
  // Return the result
  return (fahrenheit - 32) * 5 / 9;
};

5. Testing

We’re not done yet! We still need to be sure that our function works. That means we need to test it.

We can test our function manually by calling it with different inputs, knowing what we expect the outputs to be, and checking that our function’s output matches our expectations.

We can express our tests as expectations for what function should do.

The code below compares our actual value (the value returned by calling the function) to our expected value (the literal value we write down).

For example,

We can compare the two using the === operator to check if they’re equal: fahrenheitToCelsius(86) === 30.

Even better, we can put that comparison in a place where we’ll be alerted if it evaluates to false, i.e., if our implementation doesn’t match our expectation.

We can do this using the console.assert statement:

console.assert(fahrenheitToCelsius(86) === 30);

console.assert takes any boolean expression as a parameter, and if the expression is false, it prints an error message when we run our tests. It won’t print anything if our test passes, i.e., if the boolean expression was true.

Here are a couple more example tests:

console.assert(fahrenheitToCelsius(32) === 0);
console.assert(fahrenheitToCelsius(212) === 100);