On this page:
1 Data Design Recipe
1.1 Data Definitions
1.2 Kinds of Data Definitions
2 Function Design Recipe
2.1 Accumulators
2.2 Multi-argument Templates
2.3 A Note on Random Functions
3 Iteration Recipe
The Incremental Programming Pledge
4 Abstraction Recipe
5 Generative Recursion

The Design Recipe🔗

Last updated: Tue, 24 Feb 2026 12:39:55 -0500

A central concept in this course is a Design Recipe.

A Design Recipe is a systematic way to write readable and correct high-level code (no matter what programming language you’re using).

The Textbook describe some steps of the recipes, but this page summarizes the official recipe(s) we use in this course.

There are two main recipes: one for Data Design and one for Function Design.

1 Data Design Recipe🔗

The first step of any programming task is to determine what kind of data the program operates on.

Examples of data are numbers, strings, or some combination of values like coordinates.

Since a program’s purpose is to accomplish a real-world task, this data must be a represention, i.e., a data representation, of some real-world concept.

More specifically, a data representation consists of one or more data definitions.

1.1 Data Definitions🔗

A data definition consists of the following parts:

  1. a name. Even though we define data definitions with comments in this course, the name should be considered a formal definition that can be referenced later.

  2. a description of the set of all possible values of this data,

  3. an interpretation that explains what real-world concepts are represented by this data definition,

  4. A predicate is a function that returns a Boolean value.

    See below for various refinements to predicate requirements for different kinds of data definitions, e.g., enumerations and itemizations from lecture 5.

    a single-argument predicate function that can be used to reject (some) non-members of this data definition (if needed, define additional predicates for each enumeration or itemization).

    A predicate enables some "error checking" of the data definition, often with define/contract, but it is necessarily conservative.

    We won’t always want a predicate to completely check every value in a data definition, e.g., for performance or duplication of work reasons, particularly with more complicated data structures, but a data definition’s predicate should never reject a value that is a valid member of the data definition (i.e., no false negatives) (see also compound data or recursive data, which use "shallow" checks, for more specific examples).

  5. Templates introduced in lecture 5.

    For some kinds of data, a template sketches what the code for a function that processes this kind of data looks like.

    In this course, we use a four-dot ellipsis (....) as template placeholders, to be filled in later, for unknown parts of the function.

    The template will use the predicates defined in the previous step (with define/contract or cond).

  6. Compound data introduced in lecture 6.

    Some kinds of data definitions, e.g., compound data, require additional definitions—like a struct declaration or a constructor function—that enable defining instances of the data.

See Kinds of Data Definitions for more details.

1.2 Kinds of Data Definitions🔗

2 Function Design Recipe🔗

The recipe for designing a function requires the following steps.

  1. Name of the function

  2. Signature

    The Signature specifies the number of arguments and the types of the inputs and outputs.

    It should use the Data Definitions defined using the Data Design Recipe. New Data Definitions should be defined at this point, if needed.

    Coming up with the Signature should always be the second step, and they should be initially written with comments.

    But they should eventually be replaced with contracts and predicates in later steps, at which time the initial comments can be removed.

  3. Function Description or Purpose Statement

    This briefly describes, in English prose, what the "purpose" of the function is.

    More specifically, it should explain what the function computes, and how it uses the input arguments to do so, but not the details of how it computes it.

    A purpose statement should be clear and concise. It should strive to be as short as possible without being vague.

  4. Examples

    Similar to the Test-Driven Development (TDD) philosophy, these must be written before any code. (Note, however, that there is a separate Tests step after the Code step.)

    One way to think about the difference between Examples and Tests is:

    • Examples help explain what a function does, including first and foremost to the programmer themselves. For other programmers, they are what you would want to see in documentation. Thus they should be illustrative and tend to be simpler.

      Since one of the main purposes of Examples is to help a programmer understand the problem they are trying to solve, one should come up with as many Examples as needed to reach this clear understanding. As such, the exact number of Examples will vary depending on the complexity of a problem or function. For simple functions, one example may suffice. Larger functions may require more.

      In this course, Examples should come before a function define and be written as runnable tests, e.g., check-equal? so they may later be used to help test the code.

    • In contrast, Tests help verify that a function is correct. They may be more complicated and, as a result, may be less human-readable. An exact number of tests will also vary depending on the problem, but in general one should write as many tests as needed to verify they correctness of the written code. See also Tests.

  5. Template step added in lecture 5.

    Choose Template

    Specifically, choose one argument from the signature that this function will "process". The code for this function must use the template for that kind of data. If there are multiple arguments, then choose one only.

  6. Code

    If all previous steps have been followed, then this step should be straightforward (if you find yourself struggling at this step, it often means you did not properly do the previous The Design Recipe steps).

    Specifically, this step involves filling in the template with (a combination of nested) arithmetic expressions (it should not contain any statements.)

  7. Tests

    Tests are written after the Code step and are in addition to the Examples.

    While you should have already turned Examples into basic test cases, this step should add additional test cases that are more complicated and more thorough.

    They should follow basic software testing principles, e.g.,

    • maximum code coverage, and

    • sufficient testing of all the possible input values. One important aspect of this second point is coming up with corner cases.

    The number of Tests needed depends on how complicated a function is. For simple functions, two tests may suffice. More complicated functions may require more. For example, if the input is an itemization, then there should be at minimum one test for each of the itemizations.

    To emphasize their distinctness from Examples, Tests are often put in a separate files, e.g. in this course we will often create a tests.rkt for tests.

  8. Refactor

    Even after completing all the recipe steps, it’s rare for a program to be complete. At this point it’s common to refactor a program, if needed.

    We will talk about various possible refactorings this semester. One possibility is abstraction.

    Refactoring can be dangerous if too many changes are made at once. A thorough test suite, which should have already been written, can help with this. To help avoid further problems during refactoring, we will make sure to follow the The Incremental Programming Pledge.

Examples of function definitions that follow the The Design Recipe:
;; c2f : Converts the given Celsius temperature to an equivalent Fahrenheit one.
(check-equal? (c2f 0) 32)
(check-equal? (c2f 100) 212)
(check-equal? (c2f -40) -40)
 
(define/contract (c2f ctemp)
  (-> TempC? TempF?)
  (+ (* ctemp (/ 9 5) 32)))

;; in a separate Tests file
(check-equal? (c2f 1) 33.8)
2.1 Accumulators🔗

Accumulators introduced in lecture 11.

Often, a function can process one argument, independently of previous function calls.

For example, we can use map to add one to every element of a list of numbers.

(map add1 (list 1 2 3)) ; => (list 2 3 4)

Sometimes, however, a function needs to remember additional information from other parts of the program, e.g., previous calls to the function.

In these cases, we need an accumulator.

Functions that use an accumulator must follow the accumulator template.

In particular, an accumulator must be specified with the following information

Summary of Accumulator Template

Example

An example where an accumulator is needed is a max function that computes the maximum value in a list of numbers.

;; lst-max : NonEmptyList<Int> -> Int
;; Returns the largest value in a given non-empty list of ints
(define (lst-max lst0)
 
  ;; lst-max/a : List<Int> Int -> Int
  ;; max-so-far : Int
  ;; invariant: is the largest number in the list elements seen so far
  ;;  = (drop-right lst0 (length rst-lst))
  (define (lst-max/a rst-lst max-so-far)
    (cond
      [(empty? rst-lst) max-so-far]
      [else (lst-max/a (rest rst-lst)
                       (if (> (first rst-lst) max-so-far)
                           (first rst-lst)
                           max-so-far))]))
 
  (lst-max/a (rest lst0) (first lst0)))
2.2 Multi-argument Templates🔗

Multi-argument templates introduced in lecture 10.

When a function has multiple argument, the design recipe usually calls for selecting one of the argument’s data definitions to use as the code template.

Sometimes, however, multiple arguments must be processed simultaneously. In these situations, the template for two data definitions must be combined.

Example

Here is a function (from our "CS450 Lang") that performs "addition" on either numbers or strings, in the same manner as JavaScript.

;; A Result is one of:
;; - Number
;; - String
(define (Result? x) (or (number? x) (string? x)))
;; 450+: Result Result -> Result
;; Adds numbers or appends strings, following JS semantics.
 
(check-equal? (450+ 1 2) 3)
(check-equal? (450+ "1" "2") "12")
(check-equal? (450+ 1 "2") "12")
 
(define/contract (450+ x y)
  (-> Result? Result? Result?)
  (cond
    [(and (number? x) (number? y)) (+ x y)]
    [else (string-append (res->str x) (res->str y))])) ; other cases combined
2.3 A Note on Random Functions🔗

Interactive programs, to be more interesting, may occasionally wish to use randomness, i.e., the random function.

Such functions can mostly still follow the Design Recipe, with a few tweaks
  • the function may not have any inputs (like random itself)

  • precise Examples are not possible (especially if there are no inputs)

  • precise Tests may also be trickier, but it’s still possible to verify certain properties of the function output, e.g., that it’s in a the expected range

3 Iteration Recipe🔗

The Data Design Recipe and Function Design Recipe are not meant to be carried out only once. Instead, like all software development, the steps should be part of an iterative loop.

For example, while coming up with the signature of a function, you may realize that a new data definition is needed.

Or, writing a test may reveal a bug in a function’s code.

Thus, the recipe steps should be repeated as many times as needed in order to create a correct and readable program.

The Incremental Programming Pledge🔗

When iterating on a program, it’s important to not to rush, i.e., don’t change too much as a time. Failure to follow this advice is how programmers wind up doing marathon "debug" sessions.

Instead, programmers should practice Incremental Programming. Programmers who follow this practice should keep in mind the Incremental Programming Pledge, which states the following.

The following should be true about your code at all times:
  • Comments, e.g., data defs, signatures, etc., are consistent with code.

  • Code is free of syntax errors, e.g., missing or extra parens.

  • Code runs with no runtime errors or exceptions, e.g., undefined variable references, calling a non-function, division-by-zero, etc.

  • All tests pass.

If you make a code edit that makes one of the above criteria false, you should STOP and not make any more changes until all the above statements are true again.

If any problems arise, programmers who program in this incremental way will instantly know the offending line of code, and will thus be able to avoid tediously long debugging sessions.

4 Abstraction Recipe🔗

Abstraction introduced in lecture 9 and lecture 10.

Abstraction is the process of creating a definition, e.g., a data definition or function, that contains a repeated pattern, with the goal of making the program easier to read and maintain by eliminating duplication.

  1. Find similar patterns in a program.
    • minimum: 2

    • ideally: 3+

  2. Identify differences. These will be parameters of the abstraction.

  3. Create a reusable abstraction with the discovered parameters.

    Types of abstraction:
    • function abstraction

    • data abstraction, e.g., Listof<X>, where X is the parameter

    Warning, Warning!: An abstraction must have a short, clear name and "be logical". Not all "repeated" code warrants creating an abstraction. In fact, creating a bad abstraction could be much more detrimental than simply leaving the duplicate code alone, so this step should be done with some caution.

  4. Use the abstraction by giving concrete arguments for the parameters.

5 Generative Recursion🔗

Generative Recursion introduced in lecture 12.

Functions that use generative recursion are recursive, but don’t exactly follow the template of a recursive data definition. These functions should follow the The Design Recipe, with the following additions:
  1. Name

  2. Signature

  3. Description

    This step must specify a termination argument, which explains why the function will not go into an infinite loop. More specifically, it explains how every recursive call is "smaller", so that the base case will be reached eventually.

  4. Examples

  5. Code

    Generative recursive functions won’t be able to follow the normal (structural) template, but they can use a "general" template that has the following components:
    1. Divide problems into smaller problems and recursively solve them!

    2. Combine solutions to smaller problems into one solution

    3. Base case is the "trivially solvable" (i.e., smallest) problem

  6. Test