Separate how data is used from how it's represented. Constructors and selectors form an abstraction barrier. You can even implement pairs with nothing but functions.
Rational numbers — make-rat, numer, denom
A rational number is a pair: numerator and denominator. The constructor make-rat and selectors numer, denom define the interface. Everything above the barrier uses only these three operations.
The rational number layer never touches cons, car, cdr directly. The programs that use rationals never touch numer or denom directly — they use add-rat, mul-rat. Each layer talks only to the layer below it. Change the representation, and only one layer needs updating.
Scheme
; Barrier discipline: swap the representation,; nothing above the barrier changes.; Representation 1: reduce on construction
(define (make-rat-v1 n d)
(let ((g (gcd (abs n) (abs d))))
(cons (/ n g) (/ d g))))
(define (numer-v1 x) (car x))
(define (denom-v1 x) (cdr x))
; Representation 2: reduce on access
(define (make-rat-v2 n d) (cons n d))
(define (numer-v2 x)
(let ((g (gcd (abs (car x)) (abs (cdr x)))))
(/ (car x) g)))
(define (denom-v2 x)
(let ((g (gcd (abs (car x)) (abs (cdr x)))))
(/ (cdr x) g)))
; Both produce the same results:
(define r1 (make-rat-v1 68))
(define r2 (make-rat-v2 68))
(display "v1: ") (display (numer-v1 r1)) (display "/") (display (denom-v1 r1)) (newline)
(display "v2: ") (display (numer-v2 r2)) (display "/") (display (denom-v2 r2))
; Both print 3/4
Python
# Two representations behind the same interfacefrommathimport gcd
# V1: reduce on constructiondef make_rat_v1(n, d):
g = gcd(abs(n), abs(d))
return (n // g, d // g)
# V2: store raw, reduce on accessdef make_rat_v2(n, d):
return (n, d)
def numer_v2(x):
g = gcd(abs(x[0]), abs(x[1]))
return x[0] // g
def denom_v2(x):
g = gcd(abs(x[0]), abs(x[1]))
return x[1] // g
r1 = make_rat_v1(6, 8)
r2 = make_rat_v2(6, 8)
print(f"v1: {r1[0]}/{r1[1]}")
print(f"v2: {numer_v2(r2)}/{denom_v2(r2)}")
What is data? — procedural representation of pairs
The deepest insight in this chapter: you can implement cons, car, and cdr with nothing but lambda. Data isn't a thing stored in memory — it's any set of constructors and selectors that satisfies the contract: (car (cons x y)) = x and (cdr (cons x y)) = y.
Scheme
; Pairs implemented with nothing but procedures.; No data structures โ just closures.
(define (my-cons x y)
(lambda (m)
(cond ((= m 0) x)
((= m 1) y)
(else (error "Argument not 0 or 1")))))
(define (my-car z) (z 0))
(define (my-cdr z) (z 1))
; Test the contract:
(define p (my-cons 37))
(display "(car (cons 3 7)) = ")
(display (my-car p)) (newline)
(display "(cdr (cons 3 7)) = ")
(display (my-cdr p)) (newline)
; Nest them โ pairs of pairs:
(define nested (my-cons (my-cons 12) (my-cons 34)))
(display "car(car(nested)) = ")
(display (my-car (my-car nested))) (newline)
(display "cdr(cdr(nested)) = ")
(display (my-cdr (my-cdr nested)))
Python
# Pairs from closures โ no tuples, no classesdef my_cons(x, y):
def dispatch(m):
if m == 0: return x
if m == 1: return y
raiseValueError("Argument not 0 or 1")
return dispatch
def my_car(z): return z(0)
def my_cdr(z): return z(1)
p = my_cons(3, 7)
print(f"car(cons(3, 7)) = {my_car(p)}")
print(f"cdr(cons(3, 7)) = {my_cdr(p)}")
nested = my_cons(my_cons(1, 2), my_cons(3, 4))
print(f"car(car(nested)) = {my_car(my_car(nested))}")
print(f"cdr(cdr(nested)) = {my_cdr(my_cdr(nested))}")
Interval arithmetic
Another data abstraction: intervals with upper and lower bounds. Same pattern — constructors, selectors, and operations that respect the barrier. The operations propagate uncertainty: the result interval contains all possible values.
Scheme
; Interval arithmetic: another abstraction barrier.
(define (make-interval a b) (cons a b))
(define (lower-bound x) (car x))
(define (upper-bound x) (cdr x))
(define (add-interval x y)
(make-interval (+ (lower-bound x) (lower-bound y))
(+ (upper-bound x) (upper-bound y))))
(define (mul-interval x y)
(let ((p1 (* (lower-bound x) (lower-bound y)))
(p2 (* (lower-bound x) (upper-bound y)))
(p3 (* (upper-bound x) (lower-bound y)))
(p4 (* (upper-bound x) (upper-bound y))))
(make-interval (min p1 p2 p3 p4)
(max p1 p2 p3 p4))))
(define (print-interval x)
(display "[")
(display (lower-bound x))
(display ", ")
(display (upper-bound x))
(display "]")
(newline))
(define a (make-interval 2.02.2))
(define b (make-interval 3.03.1))
(display "a + b = ") (print-interval (add-interval a b))
(display "a * b = ") (print-interval (mul-interval a b))
Church encoding — the procedural pairs idea taken to its limit
Translation notes
Scheme's cons/car/cdr are primitive; Python uses tuples or lists. The procedural pairs example (section 3) shows that the distinction is superficial — closures suffice. Python's gcd lives in math; Scheme's is typically built in. The interval arithmetic section foreshadows the "dependency problem" that SICP poses as an exercise: algebraically equivalent expressions give different interval widths because repeated variables are treated as independent. That subtlety is omitted here but worth exploring.