ljpurcell

The Little Schemer: Chapter 1

#scheme #functional programming

Chapter notes from The Little Schemer, which I am reading as part of my self-study CS curriculum and exploration into functional programming. In particular, I’m aiming to better understand recursion and symbolic manipulation, key topics in functional languages.

Not everything written here is covered in the chapter—some of it I had to research to make better sense of the material. As such, the notes are not purely a summary but also include supplementary explanations.

Primitive Data Types

Atom

An atom is any indivisible value in Scheme, meaning it is not a list or pair. Atoms include numbers, strings, booleans, and symbols:

123  
"word"
#t
x

List

Lists are collections of symbolic expressions (S-expressions) wrapped in parentheses. Lists can contain other lists as well as atoms:

'(123 word)  
'(protein (carbs fat))
'('(we) '(live) '(in) '(a) '(society))  

This includes the null or empty list () .

Unquoted lists are evaluated:

(+ 1 2 3) ;; 6

Quoted lists are treated as data and not evaluated:

'(+ 1 2 3) ;; (+ 1 2 3)

Pair

Pairs allow us to combine two Scheme objects into a single entity.

As the GNU reference states:

Pairs are used to combine two Scheme objects into one compound object. Hence the name: A pair stores a pair of objects.

The data type pair is extremely important in Scheme, just like in any other Lisp dialect. The reason is that pairs are not only used to make two values available as one object, but that pairs are used for constructing lists of values.

Pairs

The syntax for pairs uses parentheses and a dot to separate the two objects. There must be whitespace on either side of the dot:

'(1 . "a") ;; (1 . "a")
'("hello" . ()) ;; ("hello")

We can test if an object is a pair using the pair? primitive:

(pair? '("The" . "Simpsons")) ;; #t
(pair? "The Simpsons") ;; #f
(pair? '()) ;; #f

Symbolic Expressions

An S-expression (short for symbolic expression) is a way of representing data in Scheme, as well as in LISP and computer science in general. Both atoms and lists are S-expressions.

The uniform structure of S-expressions makes them especially convenient for representing recursive data, which will become very useful as we explore more of Scheme’s power.

Procedures

car returns the first element of a list and cannot be applied to atoms:

(car '(12 24 36)) ;; 12
(car (car '((1 2 3 4) (5 6 7)))) ;; 1
(car '(+ (* 2 2) (/ 3 3))) ;; +

The Law of Car: The primitive car is only defined for non-empty lists.

cdr (“could-er”) removes the first element of the list and returns the rest:

(cdr '(x y z)) ;; (y z)
(cdr (cdr '(x y z))) ;; (z)
(cdr '(gone)) ;; ()

The Law of Cdr: The primitive cdr is only defined for non-empty lists. The cdr of any non-empty list is always another list.

cons takes an S-expression (which can be an atom or list) and a list as parameters, appending the S-expression to the front of the list:

(cons 'zoo '(lake forest)) ;; (zoo lake forest)
(cons '+ '(1 2)) ;; (+ 1 2)

The Law of Cons: The primitive cons takes two arguments. The second argument to cons must be a list. The result is a list.

null? takes a list and returns #t (true) if it is empty, otherwise #f (false):

(null? '()) ;; #t
(null? (/ 7 1)) ;; #f
(null? 1) ;; #f

The Law of Null?: The primitive null? is defined only for lists.

eq? takes in two non-numeric atoms and tests them for equality

(eq? 'abc 'xyz) ;; #f
(eq? 'same 'same) ;; #t
(eq? (car (car '((first) second third))) (car (cons 'first '()))) ;; #t

The Law of Eq?: The primitive eq? takes two arguments. Each must be a non-numeric atom.

Note: In some Scheme implementations, eq? may behave unexpectedly with numbers and strings. For numeric equality, use =.

Binding

To bind a symbol to a value, we use the define procedure:

(define one-hundred 100)
(define name "Lyndon")
(define age one-hundred)
(define empty-list ())
(define pod '("pea" . "another pea"))

All the procedures we’ve seen so far can work with symbols that have been bound to values in this way:

(null? empty-list) ;; #t
(cons 1 empty-list) ;; (1)
(eq? name "Lyndon") ;; #t

We can also define procedures. The first argument is the procedure signature (name and parameters), and the second argument is the procedure body:

(define (square x) (* x x))

(square 2) ;; 4

Alternatively, we can use lambda to define anonymous procedures:

(define add-one
  (lambda (n) (+ n 1)))
  
(add-one 99) ;; 100

Putting it All Together

As a helper function in the book, the authors suggest defining an atom? procedure that returns a boolean indicating whether an object is an atom:

(define atom?
  (lambda (object)
    (and (not (pair? object)) (not (null? object)))))

This definition serves as a useful summary of many of the constructs we’ve covered, and providing a clearer understanding of what an atom actually is.

Here, the and and not procedures act as logical operators, functioning as you would expect.