**Homework 9: Higher-Order Functions** **Reed CSCI 121 Fall 2022**   *complete the exercises by 9am on 11/8* Here we exercise the *higher order* function aspects of Python. The Python language allows you to treat functions as data. You can pass function objects as parameters to functions and you can return functions from functions. Functions can also be described tersely, and *anonymously*, with the `lambda` construct. We focus briefly on the trickiest of these features: functions that return functions. Consider the kind of Python code suggested below ~~~ none def functionMaker(...): def some_function(...): ... # code of some_function ... return ... return some_function ~~~ The function `functionMaker` is handed some information. Using that information it describes `some_function` and returns that function object to the code that called `functionMaker`. Here is a more realized version of that code: ~~~ python def makeAdder(amount): def add(x): return x + amount return add ~~~ The function `makeAdder` returns back a function that adds a specific `amount` to its parameter `x`. So `makeAdder(1)` returns an incrementing function, and `makeAdder(10)` returns an "adds 10" function. We could also write the `makeAdder` function like below: ~~~ python def makeAdder(amount): add = (lambda x: x+amount) return add ~~~ or simply ~~~ python def makeAdder(amount): return (lambda x: x+amount) ~~~ This somehow makes it clear what's happening: `makeAdder` gives back a function object that uses any specific `amount` you feed `makeAdder` as input. The code below builds three different function objects, one for each amount: ~~~ none >>> adds10 = makeAdder(10) >>> adds1 = makeAdder(1) >>> adds37 = makeAdder(37) >>> adds10(8) 18 >>> adds1(8) 9 >>> adds37(8) 45 ~~~ You can write `makeAdder` more tersely as: ~~~ python makeAdder = (lambda amount: (lambda x: x + amount)) ~~~ This is just a nested pair of `lambda` expressions. (#) Problems (##) `[HW9 P1]` max of two Write a function `def max_of_two(f,g,x)` that takes three parameters. The first two are functions and the third is a number. The function `max_of_two` should call each of the two functions `f` and `g` on the number `x` that was given. It should return the maximum of those two results. ~~~ none >>> max_of_two(lambda a: a+10, lambda b: b*b, 2) 12 >>> max_of_two(lambda a: a+10, lambda b: b*b, 3) 13 >>> max_of_two(lambda a: a+10, lambda b: b*b, 4) 16 >>> max_of_two(lambda a: a+10, lambda b: b*b, 5) 25 ~~~ The function `max_of_two` is a higher-order function because it takes two function parameters. (##) `[HW9 P2]` average Write a function `def average(f,start,end)` that takes three parameters. The first is an integer function and the next two are integers. It should return the average value of function `f` when evaluated on numbers in the range from `start` up to `end`. That is, it should compute the sum `f(start) + f(start+1) + ... + f(end)` divided by the number of summands in that sum. ~~~ none >>> average(lambda b: b*b, 1, 5) 11.0 >>> average(lambda b: b*b, 2, 5) 13.5 >>> average(lambda b: b*b, -1, 1) 0.666666666666666 >>> average(lambda a: a+10, 1,5) 13.0 ~~~ The function `average` is a higher-order function because it takes a function as a parameter. You can assume that `start` is not larger than `end` and so there is at least one value in the range. (##) `[HW9 P3]` brick maker Define a function `brick_maker` that takes a single parameter--- a symbol represented as a string---and returns a function that's specialized for making a rectangular "brick" , that is, a string made up of repeats of that character, of a given width and height. ~~~ none >>> at_brick = brick_maker('@') >>> dot_brick = brick_maker('.') >>> print(at_brick(3,2)) @@@ @@@ >>> print(dot_brick(5,1)) ..... >>> print(dot_brick(3,4)) ... ... ... ... >>> at_brick(2,2) '@@\n@@\n' ~~~ Note that the function given back by `brick_maker` is *a function that* **returns** *a string*. Neither `at_brick` nor `dot_brick` print. In our first few tests, above, we explicitly `print` the string returned by `at_brick` and `dot_brick`. In the last test, we examine the string returned by `at_brick`. Bricks have positive widths and positive heights. You need not worry about handling brick parameters that are not positive. The function `brick_maker` is higher order. When given a symbol it gives back a function, one that happens to be a brick creation function for that symbol. (##) `[HW9 P4]` max of functions Write a function `def max_of_funcs(f,g)` which takes two function parameters, `f` and `g`. It should return function `h`, with the property that when given a value `x` the result of `h(x)` should be the maximum of `f(x)` and `g(x)`. Note that this function is very different from the function `max_of_two`, but the two are closely related. ~~~ none >>> f = max_of_funcs(lambda a: a+10, lambda b: b*b) >>> f(2) 12 >>> f(3) 13 >>> f(4) 16 >>> f(5) 25 ~~~ The `max_of_funcs` is a higher-order function because it takes two function parameters and returns a function. (##) `[HW9 P5]` conditional print Write a function `conditional_print`. It is a higher-order function that takes a parameter and gives back a *printing procedure*, one that acts like the built-in `print` procedure by outputting information to the terminal. This printing function will behave a little differently though: the function that `conditional_print` gives back will be a *conditionally printing procedure*. Let's pin down what we mean by this. The `print` procedure takes a parameter and always prints it, like so: ~~~ none >>> print(6) 6 >>> print(3) 3 >>> print(7) 7 >>> print(37) 37 >>> print(36) 36 ~~~ A "conditionally printing procedure" only prints inputs of a particular type (while doing nothing on other inputs). For example, you could create a function that prints its integer input when it is a multiple of 3, and does nothing otherwise. Suppose you did this, defined a function called `print_when_mult_of_three`, and used it instead of `print`. Then we'd expect to see this kind of interaction: ~~~ none >>> print_when_mult_of_three(6) 6 >>> print_when_mult_of_three(3) 3 >>> print_when_mult_of_three(7) >>> print_when_mult_of_three(37) >>> print_when_mult_of_three(36) 36 ~~~ You'll notice what happened there. When the function was fed multiples of three (like 6, 3, and 36) it printed those. For the other numbers, it didn't do anything. It printed only when that condition was met. So, back to the matter at hand: we want you to write a higher-order function called `conditional_print` that constructs conditionally printing procedures. It should take a single input, which we will call a "test". A test is a function that only returns `True` or `False`. When given a test, `conditional_print` should return a conditionally printing procedure that only prints when the test returns `True`. It can then be used like below ~~~ none >>> mult_of_three = (lambda n: n % 3 == 0) >>> pr_mo3 = conditional_print(mult_of_three) >>> pr_mo3(6) 6 >>> pr_mo3(7) >>> pr_pos = conditional_print(lambda x: x > 0) >>> pr_pos(7) 7 >>> pr_pos(-7) >>> pr_pos(0) >>> pr_pos(1) 1 ~~~ The `conditional_print` function is higher-order because it takes a function as a parameter. It also returns back a function, a procedure that conditionally prints.