On this page:
Document Structure - General Exercises
General
Differences In Style
Document Structure - Large Programs
Data Definitions
Functions
Local Functions
Designing I/  O Functions For Testability
6.6

The Style

This web page is deprecated.

Please see the main page for Fundamentals I.

In addition to following the design recipe, all code must adhere to the following style guidelines:

Document Structure - General Exercises

  1. Organize your program top-down. This means that when you write a solution that contains multiple functions, the primary function should come first, followed by helpers. The functions should be in order of where they appear in the primary function. For example, the following code is organized top-down:
    ; my-function : Number String -> Number
    ; Add double the string-length to twice the number cubed
    (check-expect (my-function 2 "hi") 20)
    (check-expect (my-function 3 "hello") 64)
    (define (my-function n s)
      (+ (double (cube-num n)) (double-length s)))
     
    ; double : Number -> Number
    ; Computes 2n
    (check-expect (double 4) 8)
    (define (double n) (* n 2))
     
    ; cube-num : Number -> Number
    ; Produces the cube of this number
    (check-expect (cube-num 5) 125)
    (define (cube-num n) (expt n 3))
     
    ; double-length : String -> Number
    ; Produces twice the length of this string
    (check-expect (double-length "goodbye") 14)
    (define (double-length s)
      (double (string-length s)))

    Please note that many of the above helper functions were written solely to illustrate the top-down organization we expect.

  2. Title your exercises. Above every exercise please note which exercise it is. Note this does not apply to project homeworks.

  3. Separate data definitions. Data definitions (and their corresponding examples/templates) should be placed at the beginning of the relevant exercise. Data definitions do not need to be repeated if used in multiple exercises.

General

  1. Use names that make sense with respect to the problem, for your data definitions, field names, functions, constants, and parameters.

  2. Use proper indentation. Use the indentation style of DrRacket in your program. You can go to "Racket" > "Reindent All" to indent your entire file properly. Press tab to reindent the current line, or the currently selected selected portion of your file.

  3. Keep lines narrow. Do not exceed 102 columns for code or comments. DrRacket will show you the current line and column number in the bottom right of the window. You can also use its Edit -> Find Longest Line menu item. Or, you can go to Edit -> Preferences -> Editing -> General Editing, check the Maximum character width guide option and set it to 102.

  4. Do not use dangling parentheses: the closing right-parenthesis should be on the same line as the last expression of your code.

      ;; ------------------------ GOOD

      (define (f l)

        (cond [(empty? l) 0]

              [else (f (rest l))])) ;; HERE

      ;; ------------------------ BAD

      (define (f l)

        (cond [(empty? 1) 0]

              [else (f (rest l))]

         ) ;; NOT HERE

       )

    The dangling parentheses in the second code excerpt are considered extremely bad style.

  5. Break lines to break up logically distinct tasks. Consider these examples of simple function calls:

      ;; ----------------- GOOD

      (define (foo x y z)

        (max (* x y)      ;; Break after each argument to max,

             (* y z)      ;; and align all arguments in a column

             (* x z)      ;; (This works best with short-named functions)

             (* x y z)))

      ;; ----------------- OK

      (define (foo x y z)

        (max          ;; Break after max itself

         (* x y)      ;; Then indent each argument 1 space

         (* y z)      ;; (This works better when function names are long)

         (* x z)

         (* x y z)))

      ;; ----------------- BAD

      (define (foo x y z)

        (max (* x y)

         (* y z)     ;; This indentation is an inconsistent

         (* x z)     ;; mix of the previous two styles

         (* x y z)))

      ;; ----------------- BAD

      (define (foo x y z)

        (max (* x y) (* y    ;; This linebreak is just weird.

                        z)

             (* x z) (* x    ;; This is ugly. And avoidable!

                        y

                        z)

                        )

                        )

    By breaking after each argument, you will more often keep complete expressions together, and need fewer line breaks overall.

    In rare cases, you can keep two arguments on a line, when they logically belong together. For example, the x- and y-coordinates in a call to place-image might easily fit on one line, and logically form a pair of coordinates, and so could stay on one line in good style.

    Here are some more examples:

      ;; ---------------- BAD

      (define            ;; Don't break here

        (foo x y z)

        ...)

      ;; ---------------- BAD

      (define-struct      ;; Don't break here

        foo [x y z])

      (define-struct foo  ;; or here

        [x y z])

      ;; -------------------------- GOOD

      (define (foo l)

        (if (some-condition ...)

            (then-expression ...)

            (else-expression ...)))

      ;; -------------------------- BAD

      (define (foo l)

        (if (some-condition ...)

          (then-expression ...)    ;; Not aligned with condition

          (else-expression ...)))

      ;; --------------------------------- VERY GOOD

      (define (f l)                        ;; Aligning the responses

        (cond [(empty? l)  0]              ;; in a column is very legible.

              [else        (f (rest l))])) ;; ...if there's room for it

      ;; --------------------------------- GOOD

      (define (f l)

        (cond [(empty? l) 0]

              [else (f (rest l))]))

      ;; --------------------------------- GOOD

      (define (f l)

        (cond

          [(empty? l) 0]

          [else (f (rest l))]))

      ;; --------------------------------- OK

      (define (f l)

        (cond

          [(empty? l)

           0]                    ;; Only use this style if necessary

          [else

           (f (rest l))]))

Differences In Style

As one moves from codebase to codebase, one will find that different standards exist even for the same language. Such is the case between the accelerated and regular sections of the course. This section is here to help clarify the differences for students changing between sections.

Document Structure - Large Programs

  1. Organize your program top-down, regardless of how you actually work through your wish list. The phrase "top down" means that project files consist of a data definition and a constant definition section, a main function, followed by sections for handler functions, and wrapped up by general utility functions (functions used by multiple handlers). Within these sections please use top-down organization as defined above.

    The main function is the one that uses big-bang, read-file, write-file, and so on. A good purpose statement for the main function explains how to use it. For example,
    ; main : Number -> String
    ; (main n) runs the game with tick-rate n
    (define (main tick-speed)
      (... (big-bang WORLD-STATE-0
        [on-tick update-world tick-speed]
        [to-draw render-world]
        ...)))

  2. Title your sections. Title your data definition section, your constant section, and each of your handler sections. Good names for your handler sections are the names of your handlers! Also label your utility functions section.

Note: the exception to this structure is when we have you work through the homeworks in a bottom-up fashion. Essentially, the "organize homeworks in order of exercise" rule takes precedence over this structure.

Data Definitions

  1. Interpret your data. A data definition comes with two parts: the definition and the interpretation. The interpretation should tell the user what your data means in plain English. In some rare cases this interpretation is extraneous. For example, when defining a list of numbers, it would be redundant to write "This represents a list of numbers". Err on the side of caution as it is never wrong to include an interpretation.

  2. Remember your examples and templates. Each new data definition should come with its own corresponding examples and template.

Functions

  1. Remember your signature and purpose. Each new function should come with its own corresponding signature and purpose statement. These should be written directly above the function.

  2. Write proper signatures. Remember that the only types of data that you can use for a signature are pre-defined types (e.g. String, Number, etc.) or types you have defined yourself. Signatures have one or more input types followed by an arrow and one output type (see above for examples).

  3. Write clear purpose statements. A purpose statement should NOT repeat the information given by the signature. It should be succinct and give the reader an understanding of what the function is doing with its inputs. It is good style for the purpose statement of a predicate (a function that outputs a Boolean) to be written as a question.

  4. When writing generative recursion functions, don’t forget to write a termination statement which clearly and concisely explains why you believe your function will not run forever, OR, warns the user that the function may run forever.

  5. When writing functions that use an accumulator, don’t forget to write an accumulator statement which describes what the accumulator in your function is keeping track of.

  6. One task, one function. Functions should not be excessively long. If it is, split up the tasks into helper functions.

    If a function consumes a complex argument and must perform several different tasks, design several functions that all consume the argument, produce a value in the same data collection, and hand it over to the next function:
    ; HungryHungryHippoWorld -> HungryHungryHippoWorld
    (define (update-hippo-world hhhw)
      (update-time
        (move-marbles
           (eat-marbles hw))))
     
    ; HungryHungryHippoWorld -> HungryHungryHippoWorld
    (define (update-time hhhw) ...)
     
    ; HungryHungryHippoWorld -> HungryHungryHippoWorld
    (define (move-marbles hhhw) ...)
     
    ; HungryHungryHippoWorld -> HungryHungryHippoWorld
    (define (eat-marbles hhhw) ...)
    Piling the code from these three functions into the first one would yield a confusing mess.

  7. Test thoroughly. You should have at LEAST enough tests to cover all your code (none of your code should be highlighted in black when you run it). The exception is any function which produce side effects, such as functions that call big-bang or I/O functions like write-file.

Local Functions

Local functions must contain signatures and purpose statements. For example:
; distances-to-origin : [List-of Posn] -> [List-of Number]
; The distances of each posn to the origin
(check-expect (distances-to-origin (list (make-posn 3 4) (make-posn 0 0) (make-posn -5 12)))
              (list 5 0 13))
(define (distances-to-origin lop)
  (local [; distance-to-origin : Posn -> Number
          ; Distance to origin of p
          (define (distance-to-origin p)
            (sqrt (+ (sqr (posn-x p)) (sqr (posn-y p)))))]
    (map distance-to-origin lop)))
Also, if a function is sufficiently complex, it should be written outside of local so it can be tested.

Designing I/O Functions For Testability

Consider the following block of code:
(define FILE "my-file.csv")
 
; write-posn-csv-file : [List-of Posn] -> String
; Write posns to the csv file
(define (write-posn-csv-file lop)
  (local [; posn-row : Posn -> String
          ; Convert a posn to a comma-delimited string
          (define (posn-column p)
            (string-append (posn-x p) "," (posn-y p)))
          ; append-posn-column : Posn String -> String
          ; Append the row-version of the posn to s, separated by a new line
          (define (append-posn-column p s)
            (string-append (posn-column p) "\n" s))]
    (write-file FILE (foldr append-posn-column "" lop))))
What’s the problem here? There are no tests, of course. No test can be written for write-posn-csv-file since it calls write-file. The fix to turn this from untestable to testable code is to only call write-file in a very simple function that calls a helper function which does the interesting work.
(define FILE "my-file.csv")
 
; write-posn-csv-file : [List-of Posn] -> String
; Write posns to the csv file
(define (write-posn-csv-file lop)
  (write-file FILE (convert-posns-to-csv-file lop)))
 
; convert-posns-to-csv-file : [List-of Posn] -> String
; Convert this list of posns to a csv file
(check-expect (convert-posns-to-csv-file '()) "")
(check-expect (convert-posns-to-csv-file (list (make-posn 0 1)
                                               (make-posn 3 4)))
              "0,1\n3,4\n")
(define (convert-posns-to-csv-file lop)
  (local [; posn-row : Posn -> String
          ; Convert a posn to a comma-delimited string
          (define (posn-column p)
            (string-append (number->string (posn-x p)) "," (number->string (posn-y p))))
          ; append-posn-column : Posn String -> String
          ; Append the row-version of the posn to s, separated by a new line
          (define (append-posn-column p s)
            (string-append (posn-column p) "\n" s))]
    (foldr append-posn-column "" lop)))

The same is true for functions which read a file, or even functions that call big-bang: call a helper function which does the interesting work, so only a very small portion of the code remains untested.