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
3 Iteration Recipe
4 Abstraction Recipe

The Design Recipe

Last updated: Wed, 22 Nov 2023 11:14:53 -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 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,

  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 returns true for members of this data definition (if needed, define additional predicates for each enumeration or itemization).

  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).

    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.

  3. Function Description or Purpose Statement

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

    More specificly, it should explain what the function computes (but not how it computes it)

  4. Examples

    Similar to the Test-Driven Development (TDD) philosophy, these must be written before any code. (Note, however, that we have 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. They are what you would want to see in documentation and tend to be simpler.

    • Tests help verify that a function is correct. They may be more complicated and, as a result, less human-readable.

    The number of Examples needed depends on how complicated a function is. For simple functions, one example may suffice. Larger functions may require more.

    In this course, Examples should be expressed as rackunit test cases, e.g., check-equal? so they may later be used to test the code.

  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 we have already written the Examples as test cases, this step should add additional cases that are more complicated.

    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.

    In this class, put Tests in a separate file.

  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.

Examples of function definitions that follow the The Design Recipe:
;; c2f: TempC -> TempF
;; 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 17.

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 19.

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 "CS450js 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
;; 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?)
  (cond
    [(and (number? x) (number? y)) (+ x y)]
    [else (string-append (res->str x) (res->str y))])) ; other cases combined

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.

4 Abstraction Recipe

Abstraction introduced in lecture 10 and lecture 11.

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.

Warning: 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.