On this page:
4.1 Comments
4.2 Definitions
4.3 Conditionals
4.4 Expressions
4.5 Structs vs Lists
4.6 Lambda vs Define
4.7 Identity Functions
4.8 Traversals
4.9 Functions vs Macros
4.10 Exceptions
4.11 Parameters
4.12 Plural

4 Choosing the Right Construct

Racket provides a range of constructs for the same or similar purposes. Although the Racket designers don’t think that there is one right way for everything, we prefer certain constructs in certain situations for consistency and readability.

4.1 Comments

Following Lisp and Scheme tradition, we use a single semicolon for in-line comments (to the end of a line) and two semicolons for comments that start a line. This request does not contradict the programs in this document. They use two semicolons for full-line comments in source but scribble renders only one. Think of the second semicolon as making an emphatic point.

Seasoned Schemers, not necessarily Racketeers, also use triple and quadruple semicolons. This is considered a courtesy to distinguish file headers from section headers.

In addition to ;, we have two other mechanisms for commenting code: #|...|# for blocks and #; to comment out an expression. Block comments are for those rare cases when an entire block of definitions and/or expressions must be commented out at once. Expression comments#;apply to the following S-expression. This makes them a useful tool for debugging. They can even be composed in interesting ways with other comments, for example, #;#; will comment two expressions, and a line with just ;#; gives you a single-character “toggle” for the expression that starts on the next line. But on the flip side, many tools don’t process them properly—treating them instead as a # followed by a commented line. For example, in DrRacket S-expression comments are ignored when it comes to syntax coloring, which makes it easy to miss them. In Emacs, the commented text is colored like a comment and treated as text, which makes it difficult to edit as code. The bottom line here is that #; comments are useful for debugging, but try to avoid leaving them in committed code. If you really want to use #;, clarify their use with a line comment (;).

4.2 Definitions

Racket comes with quite a few definitional constructs, including let, let*, letrec, and define. Except for the last one, definitional constructs increase the indentation level. Therefore, favor define when feasible.

good

#lang racket
 
(define (swap x y)
  (define t (unbox x))
  (set-box! x (unbox y))
  (set-box! y t))

bad

#lang racket
 
(define (swap x y)
  (let ([t (unbox x)])
    (set-box! x (unbox y))
    (set-box! y t)))

Warning A let* binding block is not easily replaced with a series of defines because the former has sequential scope and the latter has mutually recursive scope.

works

#lang racket
(define (print-two f)
  (let* ([_ (print (first f))]
         [f (rest f)]
         [_ (print (first f))]
         [f (rest f)])
    ; IN
    f))

does not

#lang racket
 
(define (print-two f)
   (print (first f))
   (define f (rest f))
   (print (first f))
   (define f (rest f))
   ; IN
   f)

4.3 Conditionals

Like definitional constructs, conditionals come in many flavors, too. Because cond and its relatives (case, match, etc) now allow local uses of define, you should prefer them over if.

good

#lang racket
 
(cond
  [(empty? l) #false]
  [else
   (define f (first l))
   (define r (rest l))
   (if (discounted? f)
       (rate f)
       (curved (g r)))])

bad

#lang racket
 
(if (empty? l)
    #false
    (let ([f (first l)]
          [r (rest l)])
      (if (discounted? f)
          (rate f)
          (curved (g r)))))

Also, use cond instead of if to eliminate explicit begin.

The above “good” example would be even better with match. In general, use match to destructure complex pieces of data.

You should also favor cond (and its relatives) over if to match the shape of the data definition. In particular, the above examples could be formulated with and and or but doing so would not bring across the recursion as nicely.

4.4 Expressions

Don’t nest expressions too deeply. Instead name intermediate results. With well-chosen names your expression becomes easy to read.

good

#lang racket
(define (next-month date)
  (define day (first date))
  (define month (second date))
  (if (= month 12)
      `(,(+ day 1) 1)
      `(,day ,(+ month 1))))

bad

#lang racket
(define (next-month d)
  (if (= (cadr d) 12)
      `(,(+ (car d) 1)
        1
        ,(caddr d))
      `(,(car d)
        ,(+ (cadr d) 1))))
Clearly “too deeply” is subjective. On occasion it also isn’t the nesting that makes the expression unreadable but the sheer number of subexpressions. Consider using local definitions for this case, too.

4.5 Structs vs Lists

Use structs when you represent a combination of a small and fixed number of values. For fixed length (long) lists, add a comment or even a contract that states the constraints.

If a function returns several results via values, consider using structs or lists when you are dealing with four or more values.

4.6 Lambda vs Define

While nobody denies that lambda is cute, defined functions have names that tell you what they compute and that help accelerate reading.

good

#lang racket
 
(define (process f)
  (define (complex-step x)
    ... 10 lines ...)
  (map complex-step
       (to-list f)))

bad

#lang racket
 
(define (process f)
  (map (lambda (x)
         ... 10 lines ...)
       (to-list f)))

Even a curried function does not need lambda.

good

#lang racket
 
(define ((cut fx-image) image2)
  ...)

acceptable

#lang racket
 
(define (cut fx-image)
  (lambda (image2)
    ...))
The left side signals currying in the very first line of the function, while the reader must read two lines for the version on the right side.

Of course, many constructs (call-with ..) or higher-order functions (filter) are made for short lambda; don’t hesitate to use lambda for such cases.

4.7 Identity Functions

The identity function is values:

Examples:
> (map values '(a b c))

'(a b c)

> (values 1 2 3)

1

2

3

4.8 Traversals

With the availability of for/fold, for/list, for/vector, and friends, programming with for loops has become just as functional as programming with map and foldr. With for* loops, filter, and termination clauses in the iteration specification, these loops are also far more concise than explicit traversal combinators. And with for loops, you can decouple the traversal from lists.

See also for/sum and for/product in Racket.

good

#lang racket
 
; [Sequence X] -> Number
(define (sum-up s)
  (for/fold ((sum 0)) ((x s))
    (+ sum x)))
 
; examples:
(sum-up '(1 2 3))
(sum-up #(1 2 3))
(sum-up
  (open-input-string
    "1 2 3"))

bad

#lang racket
 
; [Listof X] -> Number
(define (sum-up alist)
  (foldr (lambda (x sum)
            (+ sum x))
          0
          alist))
 
; example:
(sum-up '(1 2 3))
In this example, the for loop on the left comes with two advantages. First, a reader doesn’t need to absorb an intermediate lambda. Second, the for loop naturally generalizes to other kinds of sequences. Naturally, the trade-off here is a loss of efficiency; using in-list to restrict the good example to the same range of data as the bad one speeds up the former.

Note: for traversals of user-defined sequences tend to be slow. If performance matters in these cases, you may wish to fall back on your own traversal functions.

4.9 Functions vs Macros

Define functions when possible, Or, do not introduce macros when functions will do.

good

#lang racket
...
 
(define (name msg)
  (first (second msg)))

bad

#lang racket
...
; Message -> String
(define-syntax-rule (name msg)
  (first (second msg)))
A function is immediately useful in a higher-order context. For a macro, achieving the same goal takes a lot more work.

4.10 Exceptions

When you handle exceptions, specify the exception as precisely as possible.

good

#lang racket
...
; FN [X -> Y] FN -> Void
(define (convert in f out)
  (with-handlers
      ((exn:fail:read? X))
    (with-output-to out
      (writer f))))
 
; may raise exn:fail:read
(define ((writer f))
 (with-input-from in
   (reader f)))
 
; may raise exn:fail:read
(define ((reader f))
 ... f ...)

bad

#lang racket
...
; FN [X -> Y] FN -> Void
(define (convert in f out)
  (with-handlers
      (((lambda _ #t) X))
    (with-output-to out
      (writer f))))
 
; may raise exn:fail:read
(define ((writer f))
 (with-input-from in
   (reader f)))
 
; may raise exn:fail:read
(define ((reader f))
 ... f ...)
Using (lambda _ #t) as an exception predicate suggests to the reader that you wish to catch every possible exception, including failure and break exceptions. Worse, the reader may think that you didn’t remotely consider what exceptions you should be catching.

It is equally bad to use exn? as the exception predicate even if you mean to catch all kinds of failures. Doing so catches break exceptions, too. To catch all failures, use exn:fail? as shown on the left:

good

#lang racket
...
; FN [X -> Y] FN -> Void
(define (convert in f out)
  (with-handlers
      ((exn:fail? X))
    (with-output-to out
      (writer f))))
 
; may raise exn:fail:read
(define ((writer f))
 (with-input-from in
   (reader f)))
 
; may raise exn:fail:read
(define ((reader f))
 ... f ...)

bad

#lang racket
...
; FN [X -> Y] FN -> Void
(define (convert in f out)
  (with-handlers
      ((exn? X))
    (with-output-to out
      (writer f))))
 
; may raise exn:fail:read
(define ((writer f))
 (with-input-from in
   (reader f)))
 
; may raise exn:fail:read
(define ((reader f))
 ... f ...)

Finally, a handler for a exn:fail? clause should never succeed for all possible failures because it silences all kinds of exceptions that you probably want to see:

bad

#lang racket
...
; FN [X -> Y] FN -> Void
(define (convert in f out)
  (with-handlers ((exn:fail? handler))
    (with-output-to out
      (writer f))))
 
; Exn -> Void
(define (handler e)
  (cond
    [(exn:fail:read? e)
     (displayln "drracket is special")]
    [else (void)]))
 
; may raise exn:fail:read
(define ((writer f))
 (with-input-from in
   (reader f)))
 
; may raise exn:fail:read
(define ((reader f))
 ... f ...)
If you wish to deal with several different kind of failures, say exn:fail:read? and exn:fail:network?, use distinct clauses in with-handlers to do so and distribute the branches of your conditional over these clauses.

4.11 Parameters

If you need to set a parameter, use parameterize:

good

#lang racket
...
(define cop
  current-output-port)
 
; String OPort -> Void
(define (send msg op)
  (parameterize ((cop op))
    (display msg))
  (record msg))

bad

#lang racket
...
(define cop
  current-output-port)
 
; String OPort -> Void
(define (send msg op)
  (define cp (cop))
  (cop op)
  (display msg)
  (cop cp)
  (record msg))

As the comparison demonstrates, parameterize clearly delimits the extent of the change, which is an important idea for the reader. In addition, parameterize ensures that your code is more likely to work with continuations and threads, an important idea for Racket programmers.

4.12 Plural

Avoid plural when naming collections and libraries. Use racket/contract and data/heap, not racket/contracts or data/heaps.