Programming Languages: Chapter 5: Functional Programming in Scheme



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 [COPL9] pp. 662 and 672)


Functions vs. procedures

  • a function returns a value (e.g., int f(int x))
  • a procedure does not return a value and is typically evaluated for side-effect (e.g., void print(int x))


Parameters vs. arguments

  • formal parameters (also known as bound variables or simply parameters)
  • actual parameters (often called arguments)
  • // defining function f
    // a and b are the formal parameters
    int f (int a, int b) {
       return (a+b); 
    }
    
    // invoking f
    // x and y are the actual parameters (arguments)
    f(x,y);
    


Hallmarks of functional languages

  • ``a functional programming language gives a simple 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 identifier can have a value of any type bound to it
  • 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'' [EOPL2] 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"
    • Racket editor matches parentheses, whew!


Scheme

  • racket is the command-line interface to 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 pow 
      (lambda (x n)
        (cond
          ((zero? n) 1)
          (else (* x (pow x (- n 1)))))))
    


Lists in LISP

  • lists are heterogeneous in LISP (contrast with ML and Haskell in which they are homogeneous)
  • heterogeneous list are more flexible; can represent a homogeneous list, but cannot do the reverse
  • are lists heterogeneous or homogeneous in C++? Java?
  • contain symbols and other similarly restricted lists
  • `an S-expression is either an atom or a (possible empty) list of S-expressions' [TLS] p. 92
  • S-list (defined in grammar adapted from [EOPL2] p. 5):
               <s-list> ::= ({<symbol-expression>}*)
    <symbol-expression> ::= <atom> | <s-list>
    
  • examples:
    (a b c)
    (a b c d)
    (a 1 b 2 3 c)
    (an (((s-list)) (with () lots) ((of) nesting)))
    
  • quote: protects expressions from evaluation
    • use ' if you want the code to be treated as data and not a program
    • omit ' if you want the code to be treated as a program and not data
    • symbols do not evaluate to themselves (unless preceded with a quote)
    • numbers and nil and () need not be quoted
  • more examples:
    '(a b c d)
    '(1 2 3 4)
    
  • a list is a cons cell
  • a cons cell is a pair of
    • a pointer to the head of the list as an atom or a list (known as the car)
    • a pointer to the tail of the list as a list (known as the cdr)
  • cons: constructs (and allocates) new memory; Scheme analog of malloc(16) in C (i.e., allocates memory for two pointers)
  • running time of cons: O(1)
  • always follow the Laws of car, cdr, and cons [TLS]
  • comparison to linked lists in C++
    • more or less one in the same
    • both dynamic - grow and shrink arbitrarily
    • can anyone write a function to reverse a linked list in 4 lines of C++ code?


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?

  • 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 [EOPL2] p. 12 (top)


Simple functions

  • grammar for a list of numbers: <list-of-numbers> ::= () | (<number> . <list-of-numbers>)
  • (define list-of-numbers?
      (lambda (lst)
        (or (null? lst)
            (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 first argument, not last
    • 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])


More car and cdr

  • where do car and cdr they derive their names from? the IBM 704 computer (see [COPL9] p. 671)
  • 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

  • grammar: <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
    • grammar: () | (<key> <bin-search-tree> <bin-search-tree>)
    • context?
    • more examples of context:
      • concept of setness
      • concept of order (e.g., a sorted list)


atoms, lat's, and S-lists

  • atom? predicate ([TLS] p. xii)
    (define atom?
       (lambda (x)
          (and (not (pair? x)) (not (null? x)))))
    
  • grammar for a list of atoms: <list-of-atoms> ::= () | (<atom> . <list-of-atoms>)
  • 
    (define list-of-atoms?
      (lambda (lst)
        (or (null? lst)
            (and (atom? (car lst)) (list-of-atoms? (cdr lst))))))
    
  • use list? predicate to determine a S-list
  • contrast with list function
    > (list 'a 'b 'c)
    (a b c)
    


lat's vs. S-lists

(define remove_first 
  (lambda (a lat)
    (cond
      ((null? lat) '())
      ((eqv? a (car lat)) (cdr lat))
      (else (cons (car lat) (remove_first a (cdr lat)))))))

(define remove_all 
  (lambda (a lat)
    (cond
      ((null? lat) '())
      ((eqv? a (car lat)) (remove_all (cdr lat)))
      (else (cons (car lat) (remove_all a (cdr lat)))))))

(define remove_all*
  (lambda (a l)
    (cond
      ((null? l) '())
      ((atom? (car l))
       (cond
         ((eqv? a (car l)) (remove_all* a (cdr l)))
         (else (cons (car l) (remove_all* a (cdr l))))))
      (else (cons (remove_all* a (car l)) (remove_all* a (cdr l)))))))
  • theme: follow The First Commandment [TLS]
  • two problems with remove_all*:
    1. computing (car l) more than once for the same value of l (hint: follow The Fifteenth Commandment [TSS]); note: (cdr l) is only being computed once for the same value of l
    2. passing a to every invocation of remove_all* 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 coined by 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

;; [TSPL3] 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)))))))

((lambda (sum) ((sum sum) '(1 2 3)))
   (lambda (s)
     (lambda (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 remove_all*:
saving results of common subexpressions to avoid re-computation

Now, we can fix the first problem with remove_all* by following The Fifteenth Commandment [TSS]).
(define remove_all*
  (lambda (a l)
    (cond
      ((null? l) '())
      (else (let ((head (car l)))
              (cond
                ((atom? head)
                 (cond
                   ((eqv? a head) (remove_all* a (cdr l)))                     
                   (else (cons head (remove_all* a (cdr l))))))
                (else (cons (remove_all* a head)
                            (remove_all* a (cdr l))))))))))


Factoring the parameter a from remove_all*

Now, we can fix the second problem with remove_all* by following the The Twelfth Commandment [TSS].
(define remove_all*
  (lambda (a l)
    (letrec ((remove_all_helper*
              (lambda (l)
                (cond
                  ((null? l) '())
                  (else (let ((head (car l)))
                          (cond
                            ((atom? head)
                             (cond
                               ((eqv? a head)
                                  (remove_all_helper* (cdr l)))                     
                               (else
                                  (cons head
                                     (remove_all_helper* (cdr l))))))
                            (else
                               (cons
                                  (remove_all_helper* head)
                                  (remove_all_helper* (cdr l)))))))))))
      (remove_all_helper* 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

  • don't make me include multiple header files, change, compile, change, re-compile, ad infinitum, ad nauseam
  • 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
    • atoms
    • lists (lst)
      • lat's
      • lon's
    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
    remove_first a lat
    ;; remove all occurrences of a from lat
    remove_all a lat
    ;; remove all from l
    ;; has 2 problems
    remove_all* 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 in let in remove_all* 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 remove_all* (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 Concurrent Haskell, pH (a parallel Haskell from MIT), and Erlang.


References

    [COFP] S. Thompson. Haskell: The Craft of Functional Programming. Addison-Wesley, Harlow, England, Second edition, 1999.
    [COPL9] R.W. Sebesta. Concepts of Programming Languages. Addison-Wesley, Boston, MA, Ninth edition, 2010.
    [EOPL2] D.P. Friedman, M. Wand, and C.T. Haynes. Essentials of Programming Languages. MIT Press, Cambridge, MA, Second edition, 2001.
    [TSPL3] 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.

Return Home