Lab 1: Introductory concepts and functional programming January 19, 2015 The purpose of this lab is to start using Python while trying out some basic functional patterns. There is also a glance at constraint networks, and the mathematical view of functions (as opposed to general procedures). Finally, some basic scoping concepts are explored and preparations are made for the next lab. Often there are several ways of solving the given tasks, some more Pythonic than others. The purpose here is also to get (re)acquainted with basic recursion and usage of higher-order functions. 1 Recursion and functional programming 1.1 1 2 3 4 5 6 1 2 3 4 5 6 7 Tail recursive functions Below we are given a fairly simple example of a recursive sum function: def sum(term, lower, successor, upper): if lower > upper: return 0 else: return term(lower) + \ sum(term, successor(lower), successor, upper) Note that transformations are applied to every number, and that the way of getting from a number to its successor might not just be about adding one. Now, we can easily transform the function above into a recursive procedure with an iterative model by following the pattern def sum_iter(term, lower, successor, upper): def iter(lower, result): if ???: ??? else: ??? return iter(???, ???) When executing the first sum-function with input n a number of jumps and ADD-operations (O(n) of them) would be saved on the stack. For a large enough input, this would cause a problem. In a language that utilised tail-call optimisation 1 , the latter version would run with O(1) stack depth. We will look at how this is implemented in later labs. In this task you shall 1 Unlike Python’s CPython implementation. 1 a) Implement sum iter as a tail-recursive procedure, following the template given. b) Explain what makes this a tail recursive function. [task based on SICP 1.30] 1.2 Generalising sum a) In a similar fashion, define product and product iter which compute products. b) Define factorial using one of the functions above. c) Create a program that calculates an approximation of π by using one of the product functions, and the approximation π 2 4 4 6 6 7 = ∗ ∗ ∗ ∗ ∗ ∗ ... 4 3 3 5 5 7 8 [task based on SICP 1.31] 1.3 General accumulator sum and product follow a similar pattern, with only some slight differences. a) Create functions accumulate and accumulate-iter that generalise this pattern: def accumulate(combiner, null, term, lower, succ, upper): pass 1 2 3 def accumulate_iter(combiner, null, term, lower, succ, upper): pass 4 5 b) Define product and sum as calls to accumulate and accumulate iter. c) When defining sum and product above, we can use the same combiner regardless of if we use accumulate or accumulate iter, and still get the same result. This does not hold in general. Find an example function where it doesn’t hold, and explain what (mathematical) property the combiner must have, in order for it to work. [task based on SICP 1.32] 1.4 folds, left and right A fold is a function that takes a function f , some recursive data structure and recursively applies f to the parts of the structure to build up a result. a) Create a purely functional, recursive left fold foldl that works on indexable sequences. See eg Wikipedia for an illustration. foldl(f, 0, [1,2,3]) == f(f(f(0,1) , 2), 3) b) Create a purely functional, recursive right fold foldr. foldr(f, 0, [1,2,3]) == f(1, f(2, f(3, 0))).2 2 Cf Pythonś functools.reduce. 2 c) Define the following functions as calls to foldr and foldl. def my_map(f, seq): """Return [f(seq[0]), f(seq[1]), ...]. Uses foldr or foldl.""" pass 1 2 3 4 def reverse_r(seq): """Return seq reversed. Uses foldr.""" pass 5 6 7 8 def reverse_l(seq): """Return seq reversed. Uses foldl.""" pass 9 10 11 1.5 repeat and thinking about types a) Define a function repeat that takes a function f , an integer n ≥ 0 and returns the function f n . For example, repeat(f, 2)(x) = f 2 (x) = f (f (x)). Note that n = 0 is a valid input. >>> sq = lambda x: x*x >>> sq_twice = repeat(sq, 2) >>> sq_twice(5) 625 b) Of course, Python isn’t statically typed, and we might get runtime errors if we feed a bad function f to repeat. Consider f : X → Y as a mathematical function. What properties would we like to have, if f is to work as above (in terms of domain and image)? If you were to write a type signature (such as add integers :Int×Int→ Int or f : X → Y above) for repeat, what would it be? c) Define a function compose that takes one-parameter functions f , g and returns f ◦ g, with the natural interpretation (f ◦ g)(x) = f (g(x)). d) Use your higher-order function accumulate and compose to create a repeated application that works as repeat. [task based on SICP 1.42] 1.6 smooth The idea of smoothing a function is an important concept in signal processing. If f is a function and dx is some small number, then the smoothed version of f is the function whose value at a point x is the average of f (x − dx), f (x), and f (x + dx). a) Write a procedure smooth that takes f as input, and returns the smoothed version of f with dx=0.01. b) Write a procedure n fold smooth that takes f and n ≥ 0 as inputs and, using your repeat function, returns the n-fold smoothed version of f . >>> sq = lambda x: x*x >>> smooth(sq)(4) 16.000066666666665 3 >>> five__smoothed_square = n_fold_smooth(sq, 5) >>> five_smoothed_square(4) 16.000333333333334 >>> regular_sq = n_fold_smooth(sq, 0) >>> regular_sq(5) 25 [task based on SICP 1.44] 4 2 Other basic concepts 2.1 1 2 Evaluation models Consider the following code: def f(): f() 3 4 5 def test(x,y): return 0 if x == 0 else y What would happen if we evaluated the call test(0, f()) in a language with normal order evaluation or a general lazy language (such as Haskell)? Why? In a language with applicative-order evaluation? 2.2 1 2 3 4 5 6 7 8 9 10 Scopes, closures and the environment model Python has a very simple LEGB model for scopes - look for bindings in the local-, enclosing-, global- and builtin environments. A similar ordered structure holds for ECMAScript, which we will work on in the following labs. For the time being, we shall explore the concepts of closures and chains of environments, and leave destructive updates to later.3 x = 10 def f(): print("In f: ", x) def g(x): print("In g: ", x) f() def keep_val(value): def f(): print("--- x={0}, value={1}\n".format(x, value)) return f a) We execute the following sequence. Explain the output (whenever it is printed). What is printed, and why? print_mess = keep_val("Stored") value = "New and updated." print_mess() g(5000) x = 0 g(5000) 1 2 3 4 5 6 b) Draw an environment diagram showing what happens after the definitions and each the and function calls. c) If Python didn’t have lexical scoping, but rather had dynamic scoping (like some other scripting languages), what would the output look like then? 3 Since we won’t have use for Python-keywords such as nonlocal and global in the later labs, we won’t dwell on that now. 5 2.3 1 2 3 4 5 6 7 Objects with state and environment diagrams Moving away from the purely functional way of looking at the world, we now consider objects with mutable state. Here we use explicit closures to create a minimal illustrative ”bank account” object of our own (without using Python’s ”Class”). The code (including the exception class) is available in account.py. def make_account(balance): def withdraw(amount): nonlocal balance if balance >= amount: balance = balance - amount else: raise AccountError("Account balance too low") 8 def deposit(amount): nonlocal balance balance = balance + amount 9 10 11 12 def get_value(): return balance 13 14 15 public_methods = {’withdraw’ : withdraw, ’deposit’ : deposit, ’get_value’ : get_value} return public_methods 16 17 18 a) We want to extend the code with the possibility to have a bank account which takes interest rate r ≥ 0 and handles interest accrued. This implies handling not only state in terms of balance, but also time. For each of the operations withdraw and deposits, the bank account object should also take a time t (expressed as seconds since the creation of the object; t0 = 0). At withdrawal/deposit number n we call the timestamp tn . This should satisfy tn ≥ tn−1 . The interest (cf simple interest) accrued since last access is then calculated as (tn − tn−1 ) ∗ r ∗ bn−1 ≥ 0, where b is the balance. Modify the account to handle the interest. b) Draw an environment diagram showing what happens in your code when you execute the following: >>> >>> >>> >>> a1 = make_account(10, 0.1) a2 = make_account(10, 0.01) a1[’deposit’](100, 10) # deposit 100 at t=10 a2[’withdraw’](10, 10) Some languages requires one to be explicit about scopes in some cases. As a Python aside, consider what happens if you comment out the nonlocal line from deposit. [task based on SICP 3.11] 3 Event-driven programming In this part we examine event driven programming, by looking at and extending a simple constraint system. 6 The idea in event driven programming is to generate a system of callbacks that a set of components together, possibly allowing for cascades of function calls. Such a cascade might be trigger by for instance user input (in a GUI), or changes in one part of a system that must propagate in order for it to be in a consistent state. The task you are given is to extend the constraint system given in constraints.py with a new constraint. Here is an example system which forces the connectors C and F to show the same temperature; C in Celsius and F in Farenheit. >>> from temperature import * >>> c = Connector("Celsius") >>> f = Connector("Farenheit") >>> temperature_converter(c,f) Connected Celsius with Farenheit >>> c.show_updates = True >>> f.show_updates = True >>> c.set_value(100, "user") # we set C to 100. Farenheit should update. SET: Celsius = 100 SET: Farenheit = 212.0 >>> f.set_value(123, "user") # is F=123 consistent with C being 100? Traceback (most recent call last): File "constraints.py", line 141, in <module> f.set_value(123, "user") File "constraints.py", line 34, in set_value raise ConnectorError(self.name, self.value, new_val) __main__.ConnectorError: Farenheit: 212.0 is not 123 >>> c.forget("user") FORGET: Celsius FORGET: Farenheit 3.0.1 Tasks a) Add a new constraint class squarer to the constraint system. This should link two connectors v1 and v2 in such a way that the value of v2 is the square of the value of v1. b) Explain what sorts of mathematical problems might arise from such a constraint, why (what properties of f (x) = x2 cause this) and what sort of functions you would ideally want (from an unrestricted domain). [task based on SICP chapter 3.3.5] 7