CSE 399-004: Python Programming Lecture 10: Functional programming, Memoization March 26, 2007 http://www.seas.upenn.edu/~cse39904/
Announcements Should have received email about meeting times Length: ~15 minutes (try to show up on time!) Purpose: Give a high level overview of what you're doing Basic data structures and classes How do they interact What would you like my opinion on? Again, this is fairly informal Come prepared to talk and explain But no need to have anything like slides prepared 2
Functional programming
A quick survey Who has programmed in: Scheme Lisp Standard ML OCaml Haskell Some other functional language? 4
A quick survey Who has programmed in: Scheme Lisp What makes these languages "functional?" Standard ML OCaml Haskell Some other functional language? 4
A quick survey Who has programmed in: Scheme Lisp Standard ML OCaml What makes these languages "functional?" Better question: What's a dysfunctional language? Haskell Some other functional language? 4
What is functional programming? Depends on who you ask Two important features (in my mind): Minimal (or no!) use of mutating assignment Higher-order functions: Functions that take other functions as arguments Popular functional languages tend to also be strongly typed: types are checked at compile time and there are no implicit coercions 5
Why care about functional prog.? Standard answer: Reasoning about mutation is hard If we don't mutate anything, then everything acts as it would in pure mathematics A similar reason underlies why global state is bad Another answer: It's "more elegant" if functions are "firstclass", meaning that we can treat them like any other piece of data 6
Is Python a functional language? Depends on your coding style Mutating state seems to be a very common thing in Python programming: You want to modify structures Object-oriented programming, in practice, seems to also favor mutation Even if you think it's a functional language, "real" functional languages tend to be a bit more expressive 7
>>> q = [2, 3] >>> p = [1, q, 4] >>> print p [1, [2, 3], 4] >>> q[0] = 42 >>> print q [42, 3] >>> print p [1, [42, 3], 4] These first two assignments are fine, as far as functional programming is concerned. q 2 3 8
>>> q = [2, 3] >>> p = [1, q, 4] >>> print p [1, [2, 3], 4] >>> q[0] = 42 >>> print q [42, 3] >>> print p [1, [42, 3], 4] These first two assignments are fine, as far as functional programming is concerned. Makes me cringe to say that, because of iteration constructs q 2 3 8
>>> q = [2, 3] >>> p = [1, q, 4] >>> print p [1, [2, 3], 4] >>> q[0] = 42 >>> print q [42, 3] >>> print p [1, [42, 3], 4] p 1 4 q 2 3 9
>>> q = [2, 3] >>> p = [1, q, 4] >>> print p [1, [2, 3], 4] >>> q[0] = 42 >>> print q [42, 3] >>> print p [1, [42, 3], 4] p 1 4 q 42 3 10
>>> q = [2, 3] >>> p = [1, q, 4] >>> print p [1, [2, 3], 4] >>> q[0] = 42 >>> print q [42, 3] >>> print p [1, [42, 3], 4] This is the kind of assignment statement that functional programming avoids. The difference? Making a variable simply point to a (new) data structure versus modifying a data structure in place. 11
>>> q = [2, 3] >>> p = [1, q, 4] >>> print p [1, [2, 3], 4] >>> q[0] = 42 >>> print q [42, 3] >>> print p [1, [42, 3], 4] >>> p = [3, 4] This assignment is fine, as far as functional programming is concerned. The difference? Making a variable simply point to a (new) data structure versus modifying a data structure in place. 12
Nested functions >>> def foo(x, y):... def bar():... return y.append(x+2)... return bar... >>> zs = [2, 3] >>> f = foo(2, zs) >>> f() >>> zs [2, 3, 4] >>> zs.append(5) >>> zs [2, 3, 4, 5] >>> f() >>> zs [2, 3, 4, 5, 4] What happened here? bar doesn't define y, x x, y are not global So x, y are free in bar They must refer to the nearest enclosing scope, i.e., foo 13
Nested functions >>> def foo(x, y):... def bar():... return y.append(x+2)... return bar... >>> zs = [2, 3] >>> f = foo(2, zs) >>> f() >>> zs [2, 3, 4] >>> zs.append(5) >>> zs [2, 3, 4, 5] >>> f() >>> zs [2, 3, 4, 5, 4] When bar is returned from foo, it's packed up in an environment defining x, y Not a namespace, but the idea is similar When bar is executed, execute it in that environment 14
Nested functions >>> def foo(x, y):... def bar():... return y.append(x+2)... return bar... >>> zs = [2, 3] >>> f = foo(2, zs) >>> f() >>> zs [2, 3, 4] >>> zs.append(5) >>> zs [2, 3, 4, 5] >>> f() >>> zs [2, 3, 4, 5, 4] In the context of functional programming, it's important that we be able to construct new functions at run-time 15
Passing functions to other functions >>> def gt(x, y):... if x > y:... return -1... elif x == y:... return 0... else:... return 1... >>> print sorted([10, 20, 4, 50]) [4, 10, 20, 50] >>> print sorted([10, 20, 4, 50], cmp = gt) [50, 20, 10, 4] 16
Passing functions to other functions >>> def snd(x):... return x[1]... >>> a = [(20, 5), (10, 10), (30, 40), (70, 2)] >>> print sorted(a) [(10, 10), (20, 5), (30, 40), (70, 2)] >>> print sorted(a, key = snd) [(70, 2), (20, 5), (10, 10), (30, 40)] 17
Lambda forms >>> a = [(20, 5), (10, 10), (30, 40), (70, 2)] >>> print sorted(a) [(10, 10), (20, 5), (30, 40), (70, 2)] >>> print sorted(a, key = lambda x: x[1]) [(70, 2), (20, 5), (10, 10), (30, 40)] 18
Lambda forms In Python, we can define nameless functions using the lambda keyword lambda arg1, arg2,... : expr The "expr" there is key: Only expressions are allowed, i.e., those constructs which evaluate to a value For example, no assignments or blocks Anonymous lambdas in "real" functional languages are, under the covers, how all functions are defined 19
Lambda forms lambda arg1, arg2,... : expr The "expr" there is key: Only expressions are allowed, i.e., those constructs which evaluate to a value Another point about functional programming Everything has a value! We don't evaluate something for its side-effects What's the value of an if statement or a while loop in Python? 20
Common higher-order functions map(f, a) filter(p, a) [ f(x) for x in a ] [ x for x in a if p(x) ] >>> reduce(lambda x, y : x ** y, [4, 3, 2, 1]) 4096 >>> reduce(lambda x, y : x + y, [4, 3, 2, 1]) 10 21
Common higher-order functions map(f, a) filter(p, a) [ f(x) for x in a ] [ x for x in a if p(x) ] >>> reduce(lambda x, y : x ** y, [4, 3, 2, 1]) 4096 >>> reduce(lambda x, y : x + y, [4, 3, 2, 1]) 10 "Real" functional languages call this "fold". 21
Partial applications >>> def f(x, y):... return x + y... >>> f(2) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: f() takes exactly 2 arguments (1 given) Python doesn't let you partially apply functions, another common feature in "real" functional languages. 22
Perhaps I'm too hard on Python Just because there are a number of things which "real" functional languages "do better", that doesn't mean it's pointless to avoid functional programming in Python The benefits are all there in Python You just may not have as expressive constructs as you might like to make it feel entirely natural 23
Memoization
Recursion versus iteration def fib_iter(n) : a, b = 0, 1 for i in range(0, n - 1): a, b = b, a + b return a Fast, but a bit hard to make sense of. Very close to mathematical definition, but very slow. def fib_rec(n) : if n <= 1: return n else: return fib(n - 1) + fib(n - 2) 25
Recursion versus iteration def fib_iter(n) : a, b = 0, 1 for i in range(0, n - 1): a, b = b, a + b return a Functional programming prefers recursion. (What's that i in the for loop doing?) def fib_rec(n) : if n <= 1: return n else: return fib(n - 1) + fib(n - 2) 25
fib(5) fib(4) fib(3) fib(3) fib(2) fib(1) fib(2) fib(1) fib(1) fib(0) fib(1) fib(0) fib(2) fib(1) fib(0) 26
fib(5) fib(4) fib(3) fib(3) fib(2) fib(1) fib(2) fib(1) fib(1) fib(0) fib(1) fib(0) fib(2) fib(1) fib(0) 26
fib(5) fib(4) fib(3) fib(3) fib(2) fib(1) fib(2) fib(1) fib(1) fib(0) fib(1) fib(0) fib(2) fib(1) fib(0) 26
Memoized version of fib def fib_mem(n, ans = {}) : if n in ans: return ans[n] if n <= 1: ans[n] = n else: ans[n] = fib_mem(n - 1) + fib_mem(n - 2) return ans[n] 27
Memoized version of fib def fib_mem(n, ans = {}) : if n in ans: return ans[n] if n <= 1: ans[n] = n else: ans[n] = fib_mem(n - 1) + fib_mem(n - 2) return ans[n] We use ans here to keep track of the results of previous calls to fib_mem. Users should never supply a value for this argument. 27
Memoized version of fib def fib_mem(n, ans = {}) : if n in ans: return ans[n] if n <= 1: ans[n] = n else: ans[n] = fib_mem(n - 1) + fib_mem(n - 2) return ans[n] Recall that the value of this default argument is initialized once, when the function is defined. All the modifications in the function update the value. 28
Memoized version of fib def fib_mem(n, ans = {}) : if n in ans: return ans[n] if n <= 1: ans[n] = n else: ans[n] = fib_mem(n - 1) + fib_mem(n - 2) return ans[n] Downside: We've traded off memory usage for speed. (Also: is this in the spirit of functional programming?) 29