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:
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.
a description of the set of all possible values of this data,
an interpretation that explains what real-world concepts are represented by this data definition,
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).
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).
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.
1.2 Kinds of Data Definitions
basic data values, e.g., integers, strings.
Occasionally, we can directly use "built-in" data definitions, e.g., Number or String.
The "built-in" data definitions are the ones with built-in predicates, e.g., number? or string?.
The function template for built-in data is straightforward. It consists of just a function header and a (partial) contract to validate the input.
Example:
(define/contract (int-fn x) (-> integer? ....) ....) Functions that process basic data typically compute with straightforward arithmetic. Thus the template, uses a .... as a placeholder, to be filled in later.
An important piece of information that is missing from built-in templates, however, is the interpretation.
Thus, it is often better to define new data definition, even for data that are basic values.
Examples:
;; A TempC is an Integer ;; Represents: a temperature in degrees Celsius. (define (TempC? x) (exact-integer? x)) (define/contract (tempc-fn x) (-> TempC? ....) ....) ;; A TempF is a Real ;; Represents: a temperature in degrees Fahrenheit. (define (TempF? x) (real? x)) (define/contract (tempf-fn x) (-> TempF? ....) ....) ;; A TempK is a non-negative Integer ;; Represents: a temperature in degrees Kelvin. (define (TempK? x) (exact-nonnegative-integer? x)) (define/contract (tempk-fn x) (-> TempK? ....) ....) enumeration: data that is one of a finite set of constant values.
Enumerations introduced in lecture 5.
Example:
;; A TrafficLight is one of: (define RED-LIGHT "RED") (define GREEN-LIGHT "GREEN") (define YELLOW-LIGHT "YELLOW") ;; Represents: possible colors of a traffic light Predicates for this kind of data should define smaller predicates for each of the values, when needed.Itemizations introduced in lecture 5.
itemization (this data definition generalizes enumerations): data whose values are from a list of possible other data definitions, e.g., either a string or number. The template for this kind of data should also use a cond, with one clause for each of the items.Compound Data introduced in lecture 6.
compound data: data that combines multiple values from other data definitions, e.g., structs
Example:
;; A Posn is a (mk-Posn [x : Integer] [y : Integer]) ;; Represents: a big-bang animation coordinate where: ;; - x is a circle center horizontal position ;; - y is a circle center vertical position Compound data often requires additional definitions that enable defining values of the data:(struct Posn [x y]) (define/contract (mk-Posn x y) (-> integer? integer? Posn?) (Posn x y)) Here the struct definition implicitly defines a constructor function named Posn for creating Posn values. But this default constructor does not check the type of its arguments. So we additionally define mk-Posn, which is a checked version that is a wrapper for the implicitly defined Posn constructor created by struct.
Note that compound data predicates should only do a shallow check, e.g., the Ball example’s predicate would be Posn?. The checking of field types are deferred to the mk-Posn constructor.
For compound data, the Template should extract the pieces of the compound data, which will later be combined with arithmetic.
(define/contract (Posn-fn p) (-> Posn? ....) .... (Posn-x p) .... .... (Posn-y p) ....) Recursive Data introduced in lecture 8.
recursive data: a data definition that references itself.
This kind of data is typically an itemization with at least a base case and a recursive case, where the second is a compound data, e.g., a list.
Example:
;; A ListofInt is one of: ;; - empty ;; - (cons Int ListofInt) ;; Represents: a list of CS class numbers For recursive data, the predicate should again be a shallow check, e.g., list?. If a more specific list type is known, then the listof contract combinator can be used for a more precise check.
The Template should include a recursive function call (in addition the template pieces for other kinds of data definitions, e.g. itemization and compound data).
;; TEMPLATE for list-fn : ListOfInt -> ??? (define/contract (list-fn lst) (-> (listof integer?) ....) (cond [(empty? lst) ....] [(cons? lst) .... (first lst) .... .... (list-fn (rest lst)) ....])) Data Definitions with invariants introduced in lecture 06, and further explored in lecture 14.
Invariants specify constraints on the set of values that a Data Definition represents.
It is up to each function that creates that kind of data to ensure that the invariants are always maintained.
Special dependent contracts may be used to check invariants but some care must be taken to avoid unnecessary overhead and redundant checking.
Example: Binary Search Tree (see lecture 14)
Intertwined data introduced in lecture 16.
intertwined data: a set of data definitions that reference each other. Their templates should also reference each other’s template (when needed).Example:
;; An Sexpr is one of: ;; - Atom ;; - ProgTree ;; Represents: syntax of Racket programs (define (Sexpr? x) (or (Atom? x) (ProgTree? x))) (define/contract (sexpr-fn s) (-> Sexpr? ....) (cond [(Atom? s) .... (atom-fn s) ....] [(ProgTree? s) .... (ptree-fn s) ....])) ;; An Atom is one of: ;; - Number ;; - String ;; - Symbol ;; Represents: Racket programs that are basic values (define (Atom? x) (or (number? x) (string? x) (symbol? x))) (define/contract (atom-fn a) (-> Atom? ....) (cond [(number? a) ....] [(string? a) ....] [else ....])) ;; A ProgTree is one of: ;; - empty ;; - (cons Sexpr ProgTree) ;; Represents: non-atomic Racket programs (define (ProgTree? x) (listof Sexpr?)) (define/contract (ptree-fn t) (-> ProgTree? ....) (cond [(empty? t) ....] [else .... (sexpr-fn (first t)) .... .... (ptree-fn (rest t)) ....]))
2 Function Design Recipe
The recipe for designing a function requires the following steps.
Name of the function
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.
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.
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.
Template step added in lecture 5.
Choose TemplateSpecifically, 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.
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.)
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.
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.
;; 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.
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
name
signature: specifies what kind of data the accumulator value is
invariant: explains what the accumulator represents, in terms of a statement that must always be true
Summary of Accumulator Template
Specify the accumulator (see above)
In the original function, define an internal "helper" function that has the extra accumulator argument.
If the signature is the same as the original function, then it does not need to be re-written.
If the signature differs from the original function, then the new one should be specified.
In the original function, call the "helper" function with an initial accumulator value.
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.
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.
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.
- Find similar patterns in a program.
minimum: 2
ideally: 3+
Identify differences. These will be parameters of the abstraction.
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.
Use the abstraction by giving concrete arguments for the parameters.
5 Generative Recursion
Generative Recursion introduced in lecture 12.
Name
Signature
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.
Examples
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:Divide problems into smaller problems and recursively solve them!
Combine solutions to smaller problems into one solution
Base case is the "trivially solvable" (i.e., smallest) problem
Test