Lab 1: Introductory concepts and functional programming January 19, 2015

advertisement
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
Download