2018.04.24

# Literal Expressions

In Python, we can write expressions, called literals, for:

• ints

42
• strings

'hi'
• lists

[1 , 2 , 3]
• tuples

(42 , 'hi')
• dicts

{'key_1':'val_1' , 'key_2':'val_2')
• etc.

# Lambda Expressions

We can also write literal expressions for functions:

double  =  lambda x : 2 * x

This is the function that doubles its argument (here called x)

$x ⟼ 2 * x$

It is equivalent to:

def double (x) :
return 2 * x

The terminology “lambda” is historical: it comes from the original model of universal computation, called “λ-calculus”, which was invented in the 1930s, before computers even existed.

The general form of a lambda expression is:

lambda <parameter_variables> : <expression_using_those_parameter_variables>

# Function Composition

One of the most fundamental operations on functions is function composition: using the output from one function as the input for another.

double  =  lambda x : 2 * x  #  signature: function (int -> int)
succ  =  lambda x : x + 1    #  signature: function (int -> int)
two_x_plus_one  =  lambda x : succ (double (x))
four_x  =  lambda x : double (double (x))

We can write the function composition operation itself as a higher-order function:

def compose (f , g) :
# signature: any a , b , c . function (a -> b) , function (b -> c) -> function (a -> c)
return lambda x : g (f (x))

Letting us write:

two_x_plus_one  =  compose (double , succ)
four_x  =  compose (double , double)

Now we can compose functions without using def or lambda.

# The Identity Function

• The operation of integer addition has a neutral element:

0 + 42 == 42 == 42 + 0
• The operation of integer multiplication has a neutral element:

1 * 42 == 42 == 42 * 1
• The operation of list concatenation has a neutral element:

[] + [1 , 2 , 3] == [1 , 2 , 3] == [1 , 2 , 3] + []
• Likewise, the operation of function composition has a neutral element, called the identity function:

def identity (x) :
# signature: any a . a -> a
return x

This function is pretty boring, but very useful for building higher-order functions.

# Compound Compositions

Using induction, identity and compose we can define more complex patterns of function composition:

def iterate (n , f) :
# signature: any a . int , function (a -> a) -> function (a -> a)
# precondition: n >= 0
# composes f with itself n times
if n == 0 :
return identity
elif n > 0 :
return compose (f , iterate (n - 1 , f))

$(n , f) \quad ⟼ \quad \mathrm{lambda} ~ x ~ : ~ \overbrace{\underbrace{f (f (⋯ (f } _ {(n-1) ~ \mathrm{times}}(f} ^ {n ~ \mathrm{times}} (x)))⋯))$

eight_times_x  =  iterate (3 , double)
def compose_all (fs) :
# signature: list (function) -> function
# precondition: the output type of each function must equal the input type of the next one.
if fs == [] :
return identity
else :
return compose (fs [0] , compose_all (fs [1 : ]))
four_x_plus_three  =  compose_all ([double , succ , double , succ])

# Partial Application of Functions

We can turn a function that expects two arguments into a function that expects the first argument and returns a function expecting the next one:

# signature: function (int , int -> int)
mult  =  lambda x , y : x * y

# signature: function (int -> function (int -> int))
mult_c  =  lambda x : lambda y : mult (x , y)

triple  =  mult_c (3)  #  =  lambda y : mult (3 , y)

Instead of doing this by hand, we can write a higher-order function to do this for any two-argument function. This is called function currying:

# signature: any a , b , c . function (function (a , b -> c) -> function (a -> function (b -> c)))
curry  =  lambda f : lambda x : lambda y : f (x , y)

mult_c  =  curry (mult)

Aside: using induction, we can curry functions with any number of arguments (although this is a bit tricky).

def curry_n (n , f) :
def curry_n_h (n , f , args) :
return  f (* args)  if  n == 0  else  lambda x : curry_n_h (n - 1 , f , args + [x])
return curry_n_h (n , f , [])

# Curried Higher-Order Functions

We can curry higher-order functions like map and filter:

# signature : any a , b . function (function (a -> b) -> function (iterator (a) -> iterator (b)))
map_c  =  curry (map)
# signature : any a . function (function (a -> bool) -> function (iterator (a) -> iterator (a)))
filter_c  =  curry (filter)

This makes them even more useful because then we can partially apply them to easily form new functions:

double_all  =  map_c (double)
list (double_all ([1 , 2 , 3 , 4 , 5]))
just_evens  =  filter_c (lambda n : n % 2 == 0)
list (just_evens ([1 , 2 , 3 , 4 , 5]))

With higher-order functions we can write useful bits of programs with no loops and no recursion.

The ability to manipulate functions just like any other kind of data greatly expands our power of expression.

# Example: Rat Diets

Recall the homework problem of finding the average weight of rats on a given diet:

data = \
[   #    name      ,    diet        , weight
('Whiskers'    , 'rat chow'     , 300.0) ,
('Mr. Squeeky' , 'swiss cheese' , 450.0) ,
('Pinky'       , 'rat chow'     , 320.0) ,
('Fluffball'    , 'swiss cheese' , 500.0)
]

Here is a high-level strategy:

• Identify the table rows matching the chosen diet. (filtering)
• Extract the weights from just those rows. (mapping)
• Take the average of just those weights. (arithmetic)
def avg_weight (diet , table) :
matching_weights = compose_all ([filter_c (lambda row : row [1] == diet) , map_c (lambda row : row [2]) , list]) (table)
return  sum (matching_weights) / len (matching_weights)  if  matching_weights != []  else  0.0

Higher-order functions let us work at higher levels of conceptual abstraction so that we can write complex programs concisely, with minimal bureaucratic overhead.

# The Accumulator Pattern

In the accumulator patten for processing a list:

• we initialize an accumulator,

• then we iterate over the elements of a list using a loop,

• in the loop body, we update the accumulator based on its current value and the current list element

• then we return or further process the accumulator after the loop

acc = init               # initialize the accumulator
for x in xs :            # iterate over the list
acc = f (acc , x)    # update the accumulator

# Reducing Lists

We can turn the accumulator pattern into a higher-order function for processing lists:

def reduce (f , init , xs) :
# signature: any a , b . function (b , a -> b) , b , list (a) -> b
acc = init
for x in xs :
acc = f (acc , x)
return acc

Conceptually, the reduce function is doing this:

As with map and filter, reduce becomes even more useful when we curry it:

reduce_c  =  curry_n (3 , reduce)

# Examples: List Reduction

Now we can do list reductions with no loops and no recursion, often as one-liners:

# the sum of the numbers in a list (or 0 if empty):
sum  =  reduce_c (lambda acc , x : acc + x) (0)
sum ([1 , 2 , 3 , 4 , 5])
# the longest string in a list (or '' if empty):
longest_string  =  reduce_c (lambda acc , x : acc if len (acc) >= len (x) else x) ('')
longest_string (['was' , 'it' , 'the' , 'best' , 'of' , 'times' , 'or' , 'not' , '?'])
# the largest number in a non-empty list:
max  =  lambda xs : reduce_c (lambda acc , x : acc if acc >= x else x) (xs [0]) (xs [1 : ])
max ([1 , 2 , 3 , 4 , 3 , 2 , 1])

This style of programming takes a little getting used to, but is very powerful.

Functions are the foundations of programming. I encourage you to be curious about how you can use them to express your ideas as programs.