define vs let in Racket

Short answer

define binds a name in the enclosing body — module level or inside a function. let creates a binding visible only inside its own body.

racket
(define pi 3.14159)                 ; visible for the rest of the module

(define (area r)
  (define r2 (* r r))               ; visible for the rest of this function
  (* pi r2))

(define (area-let r)
  (let ([r2 (* r r)])               ; visible only inside the let
    (* pi r2)))

Internal define is generally preferred in modern Racket — it reads more linearly and avoids a level of nesting.

#let, let* and letrec

racket
(let ([a 1]
      [b (+ a 1)])    ; ERROR — a is not visible yet
  b)

(let* ([a 1]
       [b (+ a 1)])   ; fine — sequential
  b)

(letrec ([even? (lambda (n) (if (= n 0) #t (odd? (- n 1))))]
         [odd?  (lambda (n) (if (= n 0) #f (even? (- n 1))))])
  (even? 10))         ; fine — mutually recursive
  • let evaluates all its bindings in the outer scope, simultaneously.
  • let* evaluates them in order, each seeing the previous ones.
  • letrec lets them refer to each other, which is what recursion needs.

Use let* when in doubt; the sequential behaviour is what people usually expect.

#Shadowing

racket
(define x 10)
(let ([x 20])
  x)        ; 20
x           ; 10 — unchanged

The binding is scoped to the let, which is exactly the property that makes it safe.