<jupyter_start><jupyter_text>Functional programming  Functional Programming Understanding to think in a *functional programming* style is almost asimportant as object orientation for building DRY, clear scientific software,and is just as conceptually difficult.Programs are composed of functions: they take data in (which we call*parameters* or *arguments*) and send data out (through `return` statements.)A conceptual trick which is often used by computer scientists to teach the coreidea of functional programming is this: to write a program,in theory, you only ever need functions with **one** argument, even when you think you need two or more. Why?Let's define a program to add two numbers:<jupyter_code>def add(a, b):
    return a+b

add(5,6)<jupyter_output><empty_output><jupyter_text>How could we do this, in a fictional version of Python which only defined functions of one argument?In order to understand this, we'll have to understand several of the conceptsof functional programming. Let's start with a program which just adds five tosomething:<jupyter_code>def add_five(a):
    return a+5

add_five(6)<jupyter_output><empty_output><jupyter_text>OK, we could define lots of these, one for each number we want to add. But thatwould be infinitely repetitive. So, let's try to metaprogram that: we want afunction which returns these add_N() functions.Let's start with the easy case: a function which returns a function which adds 5 to something:<jupyter_code>def generate_five_adder():
    def _add_five(a):
        return a+5
    return _add_five

coolfunction = generate_five_adder()
coolfunction(7)<jupyter_output><empty_output><jupyter_text>OK, so what happened there? Well, we defined a function **inside** the other function. We can always do that:<jupyter_code>def thirty_function():
    def times_three(a):
        return a*3
    def add_seven(a):
        return a+7
    return times_three(add_seven(3))

thirty_function()<jupyter_output><empty_output><jupyter_text>When we do this, the functions enclosed inside the outer function are **local** functions, and can't be seen outside:<jupyter_code>add_seven<jupyter_output><empty_output><jupyter_text>There's not really much of a difference between functions and other variablesin python. A function is just a variable which can have () put after it to callthe code!<jupyter_code>print(thirty_function)
x=[thirty_function, add_five, add]
for fun in x:
    print(fun)<jupyter_output><function thirty_function at 0x109aabc80>
<function add_five at 0x109aab9d8>
<function add at 0x109aab6a8><jupyter_text>And we know that one of the things we can do with a variable is `return` it. So we can return a function, and then call it outside:<jupyter_code>def deferred_greeting():
    def greet():
        print("Hello")
    return greet

friendlyfunction=deferred_greeting()
# Do something else
print("Just passing the time...")
# OK, Go!
friendlyfunction()<jupyter_output>Hello<jupyter_text>So now, to finish this, we just need to return a function to add an arbitrary amount:<jupyter_code>def define_adder(increment):
    def adder(a):
        return a + increment
    return adder

add_3=define_adder(3)
add_3(9)<jupyter_output><empty_output><jupyter_text>We can make this even prettier: let's make another variable pointing to our define_adder() function:<jupyter_code>add = define_adder<jupyter_output><empty_output><jupyter_text>And now we can do the real magic:<jupyter_code>add(8)(5)<jupyter_output><empty_output><jupyter_text>Closures You may have noticed something a bit weird:In the definition of `define_adder`, `increment` is a local variable. It should have gone out of scope and died at the end of the definition. How can the amount the returned adder function is adding still be kept?This is called a **closure**. In Python, whenever a function definition references a variable in the surrounding scope, it is preserved within the function definition.You can close over global module variables as well:<jupyter_code>name = "James"

def greet():
    print("Hello, ", name)

greet()<jupyter_output>Hello,  James<jupyter_text>And note that the closure stores a reference to the variable in the surrounding scope: ("Late Binding")<jupyter_code>name="Matt"

greet()<jupyter_output>Hello,  Matt<jupyter_text>Map and Reduce We often want to apply a function to each variable in an array, to return a new array. We can do this with a list comprehension:<jupyter_code>numbers=range(10)

[add_five(i) for i in numbers]<jupyter_output><empty_output><jupyter_text>But this is sufficiently common that there's a quick built-in:<jupyter_code>list(map(add_five, numbers))<jupyter_output><empty_output><jupyter_text>This **map** operation is really important conceptually when understandingefficient parallel programming: different computers can apply the *mapped*function to their input at the same time. We call this Single Program, MultipleData. (SPMD) **map** is half of the **map-reduce** functional programmingparadigm which is key to the efficient operation of much of today's "datascience" explosion. Let's continue our functional programming mind-stretch by looking at **reduce** operations.We very often want to loop with some kind of accumulator, such as when finding a mean, or finding a maximum:<jupyter_code>def summer(data):
    sum = 0.0
    
    for x in data:
        sum+=x
        
    return sum
summer(range(10))
import sys

def my_max(data):
    # Start with the smallest possible number
    highest = sys.float_info.min
    
    for x in data:
        if x>highest:
            highest=x
            
    return highest
my_max([2,5,10,-11,-5])<jupyter_output><empty_output><jupyter_text>These operations, where we have some variable which is building up a result,and the result is updated with some operation, can be gathered together as afunctional program, taking in the operation to be used to combine results as anargument:<jupyter_code>def accumulate(initial, operation, data):
    accumulator=initial
    for x in data:
        accumulator=operation(accumulator, x)
    return accumulator

def my_sum(data):
    def _add(a,b):
        return a+b
    return accumulate(0, _add, data)
my_sum(range(5))
def bigger(a,b):
    if b>a:
        return b
    return a

def my_max(data):
    return accumulate(sys.float_info.min, bigger, data)

my_max([2,5,10,-11,-5])<jupyter_output><empty_output><jupyter_text>Now, because these operations, _bigger, and _add, are such that e.g. (a+b)+c = a+(b+c) , i.e. they are **associative**, we could apply our accumulationto the left half and the right half of the array, each on a different computer, and then combine the two halves:1+2+3+4=(1+2)+(3+4)Indeed, with a bigger array, we can divide-and-conquer more times:1+2+3+4+5+6+7+8=((1+2)+(3+4))+((5+6)+(7+8))So with enough parallel computers, we could do this operation on eight numbersin three steps: first, we use four computers to do one  each of the pairwiseadds.Then, we use two computers to add the four totals.Then, we use one of the computers to do the final add of the two last numbers.You might be able to do the maths to see that with an N element list, thenumber of such steps is proportional to the logarithm of N.We say that with enough computers, reduction operations are O(ln N)This course isn't an introduction to algorithms, but we'll talk more about thisO() notation when we think about programming for performance.Anyway, this accumulate-under-an-operation process, is so fundamental tocomputing that it's usually in standard libraries for languages which allowfunctional programming:<jupyter_code>from functools import reduce

def my_max(data):
    return reduce(bigger, data, sys.float_info.min)

my_max([2,5,10,-11,-5])<jupyter_output><empty_output><jupyter_text>Lambda Functions When doing functional programming, we often want to be able to define a function on the fly:<jupyter_code>def most_Cs_in_any_sequence(sequences):
    
    def count_Cs(sequence):
        return sequence.count('C')
    
    counts=map(count_Cs, sequences)
    return max(counts)

def most_Gs_in_any_sequence(sequences):
    return max(map(lambda sequence: sequence.count('G'),sequences))

data=[
    "CGTA",
    "CGGGTAAACG",
    "GATTACA"
]

most_Gs_in_any_sequence(data)<jupyter_output><empty_output><jupyter_text>The syntax here is that these two definitions are identical:<jupyter_code>func_name=lambda a,b,c : a+b+c

def func_name(a,b,c):
    return a+b+c<jupyter_output><empty_output><jupyter_text>lambda defines an "anonymous" function.<jupyter_code>def most_of_given_base_in_any_sequence(sequences, base):
    return max(map(lambda sequence: sequence.count(base), sequences))

most_of_given_base_in_any_sequence(data,'A')<jupyter_output><empty_output><jupyter_text>The above fragment defined a lambda function as a **closure** over `base`. If you understood that, you've got it!  To double all elements in an array:<jupyter_code>data=range(10)
list(map(lambda x: 2*x, data))
[2*x for x in data]
def my_max(data): return reduce(lambda a,b: a if a>b else b, data,
        sys.float_info.min)

my_max([2,5,10,-11,-5])<jupyter_output><empty_output><jupyter_text>Using functional programming for numerical methods Probably the most common use in research computing for functional programmingis the application of a numerical method to a function. For example:<jupyter_code>% matplotlib inline<jupyter_output><empty_output><jupyter_text><jupyter_code>from scipy.optimize import newton
from numpy import linspace, zeros
from matplotlib import pyplot as plt

solve_me=lambda x: x**2-x

print(newton(solve_me, 2), newton(solve_me,0.2))

xs=linspace(-1,2,50)
solved=[xs,list(map(solve_me,xs)),xs,zeros(len(xs))]
plt.plot(*solved)<jupyter_output>1.0 -3.4419051426429775e-21<jupyter_text>Sometimes such tools return another function:<jupyter_code>def derivative_simple(func, eps, at):
    return (func(at+eps)-func(at))/eps
def derivative(func, eps):
    
    def _func_derived(x):
        return (func(x+eps)-func(x))/eps
    
    return _func_derived

straight = derivative(solve_me, 0.01)
straight(3)
derived=(xs,list(map(solve_me,xs)),xs,list(map(derivative(solve_me,0.01),xs)))
plt.plot(*derived)
print(newton(derivative(solve_me,0.01),0))<jupyter_output>0.49500000000000044<jupyter_text>Of course, coding your own numerical methods is bad:<jupyter_code>import scipy.misc

def derivative(func):
    def _func_derived(x):
        return scipy.misc.derivative(solve_me,x)
    return _func_derived

newton(derivative(solve_me),0)<jupyter_output><empty_output>
StarcoderdataJupyterStructuredCleanDedup