SEN2001 · Week 13 · Functional Programming

Functional programming: Scheme, lambda, map and recursion

The idea of a mathematical function, LISP data structures (atom, list, cons cell), car/cdr/cons in Scheme, define and lambda, if/cond/let, recursive list functions, higher-order functions (map, filter, compose) and tail recursion.

functional programmingmathematical functionreferential transparencyside effectLISPSchemeatomlistS-expressioncons cellcarcdrconsquotelambda expressiondefinepredicate functioncondlethigher-order functionfunctional formmapfilterreducefunction compositionclosuretail recursion

Functional programming sees a program as a combination of mathematical functions. No variables, no assignment, no loops; a function always gives the same result with the same argument. The functional language of this course is Scheme (a small, clean dialect of LISP). Two question types come up on exams: finding the value of a Scheme expression (car, cdr, cons chains) and tracing what a short recursive function returns.

Golden rules:

  1. Evaluate from the inside out. Rewrite the list at each step.
  2. car = first element, cdr = the rest of the list, cdr always returns a list (for suitable input).
  3. cons adds to the front of the second argument. If the second argument is not a list, it is a dotted pair.
  4. Recursion pattern: the empty list is the base case, process with car, continue with cdr.
  5. The quote (') stops evaluation.

Mathematical functions

A mathematical function maps each element of its domain to an element of its range. The order of evaluation is determined only by the arguments, not by side effects:

cube(x)≡x⋅x⋅xcube(2)=8\text{cube}(x) \equiv x \cdot x \cdot x \qquad \text{cube}(2) = 8

Lambda notation writes an anonymous function: λ(x) x⋅x⋅x\lambda(x)\, x \cdot x \cdot x. Application: (λ(x) x⋅x⋅x)(2)=8(\lambda(x)\, x \cdot x \cdot x)(2) = 8. Scheme’s lambda comes directly from here.

A functional form (higher-order function): a function that takes functions as parameters or returns a function. Three classics:

  • Composition: h≡f∘gh \equiv f \circ g, h(x)=f(g(x))h(x) = f(g(x)).
  • Apply-to-all: apply a function to every element of a list (map).
  • Construction: apply a list of functions to one argument, list the results.

The difference from imperative languages: in an imperative program, variables are memory cells and the state changes. In a pure functional program there is no state; that is why referential transparency holds and concurrency becomes easier. The cost: efficiency on a von Neumann machine.

LISP data structure

In LISP (1958, John McCarthy) there are two kinds of data objects: atoms (a, 17, hello) and lists ((a b c), (a (b c) d)). Lists are held internally as a chain of cons cells: each cell has two fields, car (the element) and cdr (the next cell). The cdr of the last cell is the empty list ().

(a (b c) d)

[a|•]──►[•|•]──►[d|/]
          │
          ▼
        [b|•]──►[c|/]

The program is written in the same form too: (+ 2 3) is a list and evaluates to 5. That code and data have the same form is the most distinctive feature of LISP.

Scheme basics

The interpreter is a read-evaluate-print loop. Evaluation rule: when you see a list, take the first element as the function, evaluate the other elements, and apply the function. Numbers evaluate to themselves.

ExpressionValueNote
(+ 2 (* 3 4))14Prefix notation
'(a b c)(a b c)quote: don’t evaluate
(car '(a b c))aThe first element
(cdr '(a b c))(b c)The rest
(cons 'a '(b c))(a b c)Add to the front
(list 'a (quote b) '(c))(a b (c))A list from the arguments
(null? '())#tIs it the empty list?

car, cdr, cons

(car (cdr '(a b c)))          ; b
(cdr '((a b) c d))            ; (c d)
(car '((a b) c d))            ; (a b)
(cdr '(x))                    ; ()
(cons '(a) '(b))              ; ((a) b)
(cons 'a 'b)                  ; (a . b)
(car (car (cdr '(1 (2 3) 4))))  ; 2
b
(c d)
(a b)
()
((a) b)
(a . b)
2

Trace of the last expression:

(cdr '(1 (2 3) 4))   →  ((2 3) 4)
(car ((2 3) 4))      →  (2 3)
(car (2 3))          →  2

PITFALL · the difference between cons and list. (cons '(a) '(b)) gives the two-element ((a) b); (list '(a) '(b)) gives ((a) (b)). cons takes the second argument as a list and adds to its front; list makes every argument a separate element. (car '()) is an error: the empty list has no first element.

Defining functions and control

(define pi 3.14159)                         ; binding a name
(define (square x) (* x x))                 ; a function
(define square2 (lambda (x) (* x x)))       ; the same, with lambda

(if (> x 0) x (- x))                        ; two-way selection
(cond ((< x 0) 'neg)
      ((= x 0) 'zero)
      (else 'pos))                          ; multiple-way selection

(let ((a 2) (b 3)) (+ a (* a b)))           ; 8, local names

Predicate functions return a boolean (#t, #f): null?, eq?, equal?, pair?, number?, zero?. eq? asks the equality of two symbols or the same object, equal? asks the structural equality of two lists: (equal? '(1 (2)) '(1 (2))) → #t.

Recursive list functions

No loops; everything is recursion. The pattern is always the same:

(define (f lst)
  (cond ((null? lst) <base value>)
        (else (<combine> (car lst) (f (cdr lst))))))
(define (sum lst)
  (if (null? lst) 0 (+ (car lst) (sum (cdr lst)))))
(sum '(3 5 7))                               ; 15

(define (member atm lis)
  (cond ((null? lis) #f)
        ((eq? atm (car lis)) #t)
        (else (member atm (cdr lis)))))
(member 'c '(a b c d))                       ; #t

(define (rev l)
  (if (null? l) '() (append (rev (cdr l)) (list (car l)))))
(rev '(1 2 3 4))                             ; (4 3 2 1)
15
#t
(4 3 2 1)

Trace of (sum '(3 5 7)):

(sum '(3 5 7)) = 3 + (sum '(5 7))
               = 3 + 5 + (sum '(7))
               = 3 + 5 + 7 + (sum '())
               = 3 + 5 + 7 + 0 = 15

In nested lists the car can also be a list; then you recurse in two directions:

(define (count-atoms l)
  (cond ((null? l) 0)
        ((pair? (car l)) (+ (count-atoms (car l)) (count-atoms (cdr l))))
        (else (+ 1 (count-atoms (cdr l))))))
(count-atoms '(a (b c) ((d)) e))             ; 5

Higher-order functions

(map (lambda (x) (* x x)) '(1 2 3))          ; (1 4 9)

(define (filter p lst)
  (cond ((null? lst) '())
        ((p (car lst)) (cons (car lst) (filter p (cdr lst))))
        (else (filter p (cdr lst)))))
(filter (lambda (x) (> x 2)) '(5 1 4 2 3))   ; (5 4 3)

(define (compose f g) (lambda (x) (f (g x))))
((compose car cdr) '(1 2 3))                 ; 2

(define (adder n) (lambda (x) (+ x n)))
(map (adder 3) '(1 2 3))                     ; (4 5 6)

(define (twice f) (lambda (x) (f (f x))))
((twice (lambda (x) (* x 3))) 2)             ; 18
(1 4 9)
(5 4 3)
2
(4 5 6)
18

The same ideas exist today in imperative languages too:

console.log([1, 2, 3].map((x) => x * x), [5, 1, 4, 2, 3].filter((x) => x > 2), [1, 2, 3, 4].reduce((a, b) => a * b, 1));
[ 1, 4, 9 ] [ 5, 4, 3 ] 24

Java 8 streams (list.stream().map(...).filter(...)), Python’s map, filter and lambdas, and C#‘s LINQ were taken from functional programming.

Tail recursion

(define (fact1 n) (if (= n 0) 1 (* n (fact1 (- n 1)))))
(define (fact2 n acc) (if (= n 0) acc (fact2 (- n 1) (* n acc))))
(fact2 5 1)                                  ; 120

In fact1, a multiplication is waiting after the recursive call: the ARI of every call must stay on the stack, and if n is large the stack overflows. In fact2 the call is the last job: its result is returned directly. Scheme turns this into a loop (the new call reuses the old ARI). The way to make a function tail recursive is usually to add an accumulator parameter.

Other functional languages

LanguageFeature
Common LISPLarge, a merger of many LISP dialects; macros
MLStatically typed, type inference, syntax close to Pascal
HaskellPure (no side effects), lazy evaluation: values are not computed until needed, infinite lists are possible
F#The ML family on .NET

Common mistakes

WrongCorrect
(cdr '(a b c)) = b(b c); cdr returns a list
(cdr '(x)) = x()
(cons '(a) '(b)) = (a b)((a) b)
(cons 'a 'b) = (a b)(a . b), not a list
Unquoted (a b c) as list dataa is taken as a function; write '(a b c)
Comparing lists with eq?Use equal?
Every recursion is tail recursiveNot if there is work after the call
filter keeps 2 > 2> is strict; 2 is dropped

Test yourself

Exam-style questions. Solve them on paper first, then reveal the answer.

Question 1easy

What is the value of the Scheme expression?

(car (cdr '(a b c)))
Show answer
b

From the inside out: (cdr '(a b c)) gives what comes after the first element: (b c). car of this is the first element: b. car = first element, cdr = the rest of the list.

Question 2easy

What are the values of these Scheme expressions?

(cdr '((a b) c d))
(car '((a b) c d))
(cdr '(x))
Show answer
(c d), (a b), ()

The first element can be a list: (a b). car returns it as is; cdr returns the rest: (c d). The cdr of a one-element list is the empty list ().

Question 3medium

What are the values of these Scheme expressions?

(cons 'a '(b c))
(cons '(a) '(b))
(cons 'a 'b)
Show answer
(a b c), ((a) b), (a . b)

cons creates a new cons cell: the car part is the first argument, the cdr part is the second. If the second argument is a list, the result is a list with an element added at the front. (a) is added as one element: ((a) b). If the second argument is not a list, the result is not a list but a dotted pair: (a . b).

Question 4medium

What is the value?

(car (car (cdr '(1 (2 3) 4))))
Show answer
2

(cdr '(1 (2 3) 4)) = ((2 3) 4). car → (2 3). car → 2. In questions like this, work from the inside out and rewrite the list at each step.

Question 5easy

What does (f 5) return?

(define (f n)
  (if (= n 0)
      1
      (* 2 (f (- n 1)))))
Show answer
32

f(n) = 2 · f(n - 1), f(0) = 1. That is 2n2^n. f(5) = 2 · 2 · 2 · 2 · 2 · 1 = 32. In functional languages recursion is used instead of loops.

Question 6easy

What is the value?

(map (lambda (x) (* x x)) '(1 2 3))
Show answer
(1 4 9)

lambda is an anonymous function: it takes x and returns x · x. map applies this function to each element of the list and collects the results in a new list. The original list does not change. The JavaScript equivalent is [1, 2, 3].map(x => x * x).

Question 7easy

What is the value?

(let ((a 2) (b 3)) (+ a (* a b)))
Show answer
8

let binds local names to values (not variables, unchangeable names). a = 2, b = 3 → 2 + 2 · 3 = 8. let is actually shorthand for a lambda application: ((lambda (a b) (+ a (* a b))) 2 3).

Question 8medium

Using the function below, what is (filter (lambda (x) (> x 2)) '(5 1 4 2 3))?

(define (filter p lst)
  (cond ((null? lst) '())
        ((p (car lst)) (cons (car lst) (filter p (cdr lst))))
        (else (filter p (cdr lst)))))
Show answer
(5 4 3)

cond tries the conditions in order. If the list is empty, (). If the first element satisfies p, cons it onto the filtered rest; if not, skip it. 5 ✔, 1 ✘, 4 ✔, 2 ✘ (2 > 2 is false), 3 ✔ → (5 4 3). The order is preserved.

Question 9hard

What is the value?

(define (compose f g) (lambda (x) (f (g x))))
((compose car cdr) '(1 2 3))
Show answer
2

compose is a functional form (a higher-order function): it takes two functions and returns a new function. (compose car cdr) applies first cdr and then car to x: cdr → (2 3), car → 2. In mathematics, (f∘g)(x)=f(g(x))(f \circ g)(x) = f(g(x)).

Question 10hard

What does (count-atoms '(a (b c) ((d)) e)) return?

(define (count-atoms l)
  (cond ((null? l) 0)
        ((pair? (car l)) (+ (count-atoms (car l)) (count-atoms (cdr l))))
        (else (+ 1 (count-atoms (cdr l))))))
Show answer
5

If the first element is a list, recurse both into it and into the rest; if it is an atom, count 1 and continue. The atoms: a, b, c, d, e → 5. This "double recursion on car and cdr" pattern is standard for nested lists. A len that counts only the top level would give 4.

Question 11medium

Which of these two factorial definitions is tail recursive, and why does it matter?

(define (fact1 n) (if (= n 0) 1 (* n (fact1 (- n 1)))))
(define (fact2 n acc) (if (= n 0) acc (fact2 (- n 1) (* n acc))))
Show answer
fact2; the recursive call is the last operation, so Scheme can run it in constant stack space

In fact1, after the recursive call returns, a multiplication is still to be done, so the ARI of every call must be kept. In fact2 the result of the call is returned directly; since no work remains, the new ARI can take the place of the old one (like a loop). The Scheme standard requires tail calls to be implemented this way. (fact2 5 1) = 120.

Question 12medium

What is the value?

(define (adder n) (lambda (x) (+ x n)))
(map (adder 3) '(1 2 3))
Show answer
(4 5 6)

(adder 3) returns a closure: a function that adds 3 to x, carrying n = 3 along. map applies it to each element. That functions can be returned as values and passed as parameters (first-class functions) is the foundation of functional programming.

Verification: Since Scheme is not installed, a small Scheme interpreter was written in node (verify/SEN2001/lib/scheme.mjs: quote, lambda, define, if, cond, let, let*, car/cdr/cons/list, null?, eq?, equal?, map, apply) and every Scheme expression on the page was run in this interpreter (verify/SEN2001/13.mjs). List notation follows Scheme's standard output format. The JavaScript equivalents were run with node.

Review card

pure functional: no variables, no assignment, no loops → recursion same argument → always the same result (referential transparency), no side effects LISP data: atom or list. list = chain of cons cells, last cdr = () 'x = (quote x): don't evaluate, take as is (car '(a b c)) = a (cdr '(a b c)) = (b c) (cdr '(x)) = () (cons 'a '(b c)) = (a b c) (cons '(a) '(b)) = ((a) b) (cons 'a 'b) = (a . b) (list 'a 'b '(c)) = (a b (c)) (define (f x) body) (lambda (x) body) (let ((a 1)) body) if: (if test then else) cond: ((test result) ... (else result)) predicate: null? eq? equal? pair? number? zero? (#t / #f) eq? symbol/same object; equal? compares structure recursion pattern: (null? l) base, process with (car l), continue with (cdr l) map: apply to each element; filter: keep those satisfying the condition; compose: f(g(x)) tail recursion: the call is the last job → constant stack. use an accumulator parameter