CPS 343/543 Lecture notes:
Functional programming in Scheme
Coverage: [EOPL] §§1.1-1.2 (pp. 3-27)
Lists
- what is a set? bag? list?
- we need to cultivate the habit of
inductively (i.e., recursively) specifying data structures
- how do you define a list recursively?
- all functional languages use lists, not just LISP
(see [COPL] pp. 585 and 595)
Hallmarks of functional languages
- `a functional programming language gives the simplest model of programming:
one value, the result, is computed
on the basis of others, the inputs' [COFP]
- functional programming means that programs work
by returning values rather than modifying variables (which is
how imperative programs work)
- functions are first-class
- can be set to a variable
- can be passed to a function
- can be returned from a function
- they also have types and
- are easy to test in isolation (of the rest of the
program; called interactive or incremental testing)
- anonymous functions
- called closures (lead to LISP macros)
- you have anonymous (nameless) variables
in all your favorite programming languages;
so why not have anonymous functions?
- allows us to refer to functions literally
- no distinction between program, procedure, and function
- no statements, only expressions (all of which return a value)
- no or few side-effects
- of course, there is I/O
- as a result, bugs have only a local effect
- interactive, simple read-eval-print loop (debugging)
- no iteration, only recursion
- automatic garbage collection
- no direct (programmer) manipulation of pointers
- lists are the primary built-in data structure
- values, not variables, have types
- manifest typing
- any variable can hold a value of any type
- less planning can actually lead to a better design
- bottom-up programming vs. top-down programming
- oil painting analogy
- involves pattern recognition (design patterns)
- based a λ-calculus:
a deep mathematical theory of functions attributed to
mathematician and logician Alonzo Church
- not necessarily only for AI (stereotype)
- usually interpreted
- usually have dynamic features (though Smalltalk has several as well,
recall, OOP has roots in the functional paradigm)
Lambda calculus
`a simple mini-language which is often used to study the
theory of programming languages' [EOPL] p. 6
<expression> ::= <identifier>
<expression> ::= (lambda (<identifier>) <expression>)
<expression> ::= (<expression> <expression>)
LISP
- LISP is LISt Processing
- LISP is the second oldest programming language (which is the first?)
- developed by John McCarthy and his students at MIT in 1958 (and it
is still around?)
- two mainstream dialects: Scheme (what we are using in class) and COMMON LISP
- difference between Scheme and COMMON LISP (Paul Graham, Viaweb ... robust)
- extremely simple, uniform, and consistent syntax:
atoms and lists and that's it!
- uses prefix notation for expressions
- uses applicative-order evaluation (as opposed to lazy evaluation)
- theme in LISP: blurred distinctions between program and data (program
and data have same syntax and representation in memory)
- LISP programs are expressed as lists
(the fundamental LISP data structure);
means a LISP program can generate LISP code
and interpret it on the fly at run-time!
- C is a programming language for writing UNIX;
LISP is a language for writing LISP
- nil and () represent false
- to avoid unnecessary frustration, use an editor which matches parentheses
- use vi or emacs in UNIX
- in vi, ":set sm"
- the PLT Scheme (Racket) editor matches parentheses, whew!
Scheme
- in PLT Scheme (Racket), set language to [EOPL]
- MzScheme is the command-line interface to PLT Scheme (Racket)
- semicolon introduces a comment (to the end of the line)
- simple expressions:
1
2
3
(+ 1 2)
(+ 1 2 3)
(lambda (x) (+ x 1))
((lambda (x) (+ x 1)) 2)
(define inc (lambda (x) (+ x 1)))
(inc 2)
- predicates are functions which return a boolean
and typically end with ?
- a function to find non-negative powers of numbers
;;; notice no side effects below
(define exp
(lambda (x n)
(cond
((zero? n) 1)
(else (* x (exp x (- n 1)))))))
Lists in LISP
List-box diagrams
'(a . b)
'(a . (b)) = '(a b)
'(a . (b c)) = '(a . (b . (c))) = '(a b c)
'((a) (b) ((c)))
'((a . b) . c)
'(((a) b) c)
'((a b) c)
Other than language,
what else can we define using context-free grammars (BNF)?
- data structures (inductive or recursive specification)
- algorithms (naturally reflect the data structure)
- most fundamental theme of a course on data structures and algorithms:
data structures and algorithms are natural reflections of each other
- see rule on [EOPL] p. 12 (top)
Simple functions
- BNF for a list of numbers:
<list-of-numbers> ::= () | (<number> . <list-of-numbers>)
-
(define list-of-numbers?
(lambda (lst)
(cond
((null? lst) #t)
(else (and
(number? (car lst))
(list-of-numbers? (cdr lst)))))))
- called top-down or recursive descent parsing
(contrast with bottom-up or shift-reduce parsing)
- length of a list:
(define length
(lambda (l)
(cond
((null? l) 0)
(else (+ 1 (length (cdr l)))))))
-
(define nth-elt
(lambda (lst n)
(cond
((null? lst)
(eopl:error 'nth-elt "List too short by ~s elements.~%" (+ n 1)))
((zero? n) (car lst))
(else (nth-elt (cdr lst) (- n 1))))))
- fragile vs. robust programs (we will tend to write fragile programs)
- classic function to concatenate two lists (a Scheme built-in):
(define append
(lambda (x y)
(cond
((null? x) y)
(else (cons (car x) (append (cdr x) y))))))
- what is the run-time complexity of append? O(n). Why?
- append copies all arguments except the last
- therefore never use append when cons would suffice
- reversing a list:
(define reverse
(lambda (l)
(cond
((null? l) '())
(else (append (reverse (cdr l)) (cons (car l) '()))))))
- what is the run-time complexity of reverse?
O(n2). Why?
- develop a linear-time
version of reverse
(hint: use cons
instead of append; use difference lists technique
used in function car&cdr; see also The Eleventh
Commandment [TSS])
atoms, lat's, and S-expressions
- atom? predicate (courtesy [TLS] p. xii)
(define atom?
(lambda (x)
(and (not (pair? x)) (not (null? x)))))
- BNF for a list of atoms:
<list-of-atoms> ::= () | (<atom> . <list-of-atoms>)
-
(define list-of-atoms?
(lambda (lst)
(cond
((null? lst) #t)
((atom? (car lst)) (list-of-atoms? (cdr lst)))
(else #f))))
- use list? predicate to determine a S-expression
- contrast with list function
> (list 'a 'b 'c)
(a b c)
More car and cdr
- where do car and cdr
they derive their names from? the IBM 704 computer
(see [COPL] p. 594)
- a word in the IBM 704 had two fields, named address
and decrement, which each could store a memory address
- car = contents of address register
- cdr = contents of decrement register
- (caddr lst) = car of cdr of cdr or
(car (cdr (cdr lst)))
- cxr where x is string of up to four a's or d's
- cadr = car of the cdr or (car (cdr lst))
- (caddr lst) means (car (cdr (cdr lst)))
Binary tree with numeric leaves
- BNF: <bintree> ::= <number> |
(<symbol> <bintree> <bintree>)
- examples:
1
2
(foo 1 2)
(bar 1 (foo 1 2))
(baz (bar 1 (foo 1 2)) (biz 4 5))
- ever create a binary tree in C++ or Java this painlessly?
-
(define count-nodes
(lambda (s)
(cond
((number? s) 1)
(else (+ (count-nodes (cadr s))
(count-nodes (caddr s))
1)))))
- traversals:
- preorder:
(define preorder
(lambda (bintree)
(cond
((number? bintree) (cons bintree '()))
(else
(cons (car bintree) (append (preorder (cadr bintree))
(preorder (caddr bintree))))))))
-
;;; if inorder returns a sorted list,
;;; then its parameter is a binary search tree
(define inorder
(lambda (bintree)
(cond
((number? bintree) (cons bintree '()))
(else
(append (inorder (cadr bintree))
(cons (car bintree) (inorder (caddr bintree))))))))
- postorder: self-study
- making programs more readable:
(define root
(lambda (bintree)
(car bintree)))
(define left
(lambda (bintree)
(cadr bintree)))
(define right
(lambda (bintree)
(caddr bintree)))
(define preorder
(lambda (bintree)
(cond
((number? bintree) (cons bintree '()))
(else
(cons (root bintree) (append (preorder (left bintree))
(preorder (right bintree))))))))
- binary search tree
- BNF: () | (<key> <bin-search-tree> <bin-search-tree>)
- context?
- more examples of context:
- concept of setness
- concept of order (e.g., a sorted list)
lat's vs. S-expressions
(define rember
(lambda (a lat)
(cond
((null? lat) '())
((eqv? a (car lat)) (cdr lat))
(else (cons (car lat) (rember a (cdr lat)))))))
(define remove
(lambda (a lat)
(cond
((null? lat) '())
((eqv? a (car lat)) (remove (cdr lat)))
(else (cons (car lat) (remove a (cdr lat)))))))
(define rember*
(lambda (a l)
(cond
((null? l) '())
((atom? (car l))
(cond
((eqv? a (car l)) (rember* a (cdr l)))
(else (cons (car l) (rember* a (cdr l))))))
(else (cons (rember* a (car l)) (rember* a (cdr l)))))))
- theme: follow The First Commandment [TLS]
- two problems with rember*:
- computing (car l) and (cdr l) multiple times
for the same value of l (hint: follow
The Fifteenth Commandment [TSS])
- passing a to every invocation of rember*
even though it does not change (hint: follow
The Twelfth Commandment [TSS])
- self-study: member and member*
let and let*
(let ((a 1) (b 2))
(+ a b))
((lambda (a b) (+ a b)) 1 2)
; will not work
(let ((a 1) (b (+ a 1)))
(+ a b))
(let* ((a 1) (b (+ a 1)))
(+ a b))
(let ((a 1))
(let ((b (+ a 1)))
(+ a b)))
((lambda (a)
((lambda (b) (+ a b)) (+ a 1)))
1)
- let evaluates its bindings in parallel
- let* evaluates its bindings in sequence
- let* is just syntactic sugar
(term courtesy Peter Landin [SICP] p. 11) for let
- let is just syntactic sugar for lambda
- let does not violate the spirit of functional programming
let and letrec
;; courtesy [TSPL] pp. 62-63
;; will not work
(let ((sum (lambda (l)
(cond
((null? l) 0)
(else (+ (car l) (sum (cdr l))))))))
(sum '(1 2 3)))
(letrec ((sum (lambda (l)
(cond
((null? l) 0)
(else (+ (car l) (sum (cdr l))))))))
(sum '(1 2 3)))
(let ((sum (lambda (s l)
(cond
((null? l) 0)
(else (+ (car l) (s s (cdr l))))))))
(sum sum '(1 2 3)))
((lambda (sum) (sum sum '(1 2 3)))
(lambda (s l)
(cond
((null? l) 0)
(else (+ (car l) (s s (cdr l)))))))
letrec is just syntactic sugar for let (pass
recursive function to itself)
lambda is foundational
let's fix rember*:
saving results of common subexpressions
to avoid re-computation
Now, we can fix the first problem with rember* by
following The Fifteenth Commandment [TSS]).
(define rember*
(lambda (a l)
(cond
((null? l) '())
(else (let ((head (car l)) (tail (cdr l)))
(cond
((atom? head)
(cond
((eqv? a head) (rember* a tail))
(else (cons head (rember* a tail)))))
(else (cons (rember* a head) (rember* a tail)))))))))
Factoring the parameter a
from rember*
Now, we can fix the second problem with rember* by
following the The Twelfth Commandment [TSS].
(define rember*
(lambda (a l)
(letrec ((rember1* (lambda (l)
(cond
((null? l) '())
(else (let ((head (car l)) (tail (cdr l)))
(cond
((atom? head)
(cond
((eqv? a head) (rember1* tail))
(else (cons head (rember1* tail)))))
(else (cons (rember1* head) (rember1* tail))))))))))
(rember1* l))))
Using let and letrec
to define a function local to a function
- nesting functions
- use
(lambda
(letrec ...))
if the nested function needs to know about one of the arguments
to the outer function (The Twelfth Commandment [TSS])
- use
(letrec
(lambda ...))
if the nested function does not need to know about one of the arguments
to the outer function (because it takes it as an argument itself)
to the outer function (The Thirteenth Commandment [TSS])
Different styles, but functionally equivalent
The following two expressions are functionally equivalent.
The first calls the local function in the body of the letrec.
The second returns the local function in the body of the letrec
and then subsequently calls it.
(letrec ((sum (lambda (l)
(cond
((null? l) 0)
(else (+ (car l) (sum (cdr l))))))))
(sum '(1 2 3 4 5)))
((letrec ((sum (lambda (l)
(cond
((null? l) 0)
(else (+ (car l) (sum (cdr l))))))))
sum) '(1 2 3 4 5))
More syntactic sugar: the named let
Any named let expression can be rewritten as a functionally
equivalent letrec expression. See examples below. Some
think the named let uses cleaner syntax than the
equivalent letrec since it is less verbose.
((letrec ((A (lambda (x) (+ x 1))))
A) 1)
; named let
(let A ((x 1))
(+ x 1))
((letrec ((A (lambda (x l)
(cond
((zero? x) l)
(else (A (- x 1) (cons 'a l)))))))
A) 6 '())
(let A ((x 6) (l '()))
(cond
((zero? x) l)
(else (A (- x 1) (cons 'a l)))))
Speed of execution vs. speed of development
- `I just want to get my work done!'
- don't make me include multiple header files,
change, compile, change, re-compile, ad infinitum
- C vs. LISP
- speed of development worth the loss of efficiency?
- consider Moore's Law
- compare with improvement in our development methodologies over
the years
Recap
list-boxes
s-expressions
atom?
list vs. list?
Never use append or list where cons will
suffice.
binary trees and tree traversals
making code more readable
;; remove first occurrence of a from lat
rember a lat
;; remove all occurrences of a from lat
remove a lat
;; remove all from l
;; has 2 problems
rember* a l
;; self-study
member and member*
let
let*
- let evaluates its bindings in parallel
- let* evaluates its bindings in sequence
- let* is just syntactic sugar for let
- let is just syntactic sugar for lambda
- let does not violate the spirit of functional programming
Bind head and tail with let
in rember* to avoid re-computing common subexpressions
(The Fifteenth Commandment [TSS]).
sum with let incorrect
sum with let correct
(passing recursive function)
letrec
letrec
is just syntactic sugar for let (pass recursive function to itself).
Realize λ-calculus is all we need to create powerful programs.
Using letrec to factor the a parameter
from rember*
(The Twelfth Commandment [TSS]).
- pattern-oriented programming (The First Commandment [TLS])
- use let to avoid computing common subexpressions more
than once (The Fifteenth Commandment [TSS])
- use letrec to factors out arguments which
do not change across recursive applications
(The Twelfth Commandment [TSS])
- use letrec nested inside a lambda
to hide and protect functions (The Thirteenth Commandment [TSS])
- use The Eleventh Commandment [TSS] to develop a linear-time
version of reverse
- only simplify once correct (The Sixth Commandment [TLS])
Levels of functional programming
Concurrent programming
Without side-effects, modifications to shared memory are
impossible, thereby making functional programs natural
candidates for parallelization.
Concurrent functional programming languages include Erlang and
Concurrent Haskell.
References
| [COFP] |
S. Thompson. The Craft of Functional Programming.
Addison-Wesley, Harlow, England, Second edition, 1999.
|
| [COPL] |
R.W. Sebesta. Concepts of Programming Languages.
Addison-Wesley, Boston, MA, Sixth edition, 2003. |
| [EOPL] |
D.P. Friedman, M. Wand, and C.T. Haynes.
Essentials of Programming Languages.
MIT Press, Cambridge, MA, Second edition, 2001. |
| [TSPL] |
R.K. Dybvig.
The Scheme Programming Language.
MIT Press, Cambridge, MA, Third edition, 2003.
|
| [TSS] |
D.P. Friedman and M. Felleisen.
The Seasoned Schemer.
MIT Press, Cambridge, MA, 1996.
|
|