7. Lecture 3a - Functional Patterns (R Version)#

Functional programming means thinking in terms of mathematical functions. Strict functional programming languages, such as Haskell, Idris, F#, OCaml, and scala provide abstractions for functions which a very similar to their mathematical counterparts. However, most programming languages do not do this so well. Does this mean that the idea of modelling our abstractions in terms of mathematical functions is inappropriate when using these languages ?

A basic assumption of this course is that, at least for many modern programming languages, this is not the case. The key to making this approach work is to understand the differences between a mathematical function and a function within the language being used, and then find effective ways of working around these differences.

What then are the minimal requirements of a programming language for it to be able to support the use of functional patterns ? The primary requirement is the availablity of Higher Order Programming.

7.1. Higher Order Programming#

Higher Order Programming perhaps sounds more impressive than it actually is. Put simply it means that functions and other “higher order” types can be passed to and returned from functions in the same way as more traditional data e.g. floating point numbers and strings.

7.1.1. Example#

g <- function(f,x)
{
    return(f(x))
}

g(sin,pi/6)
0.5

7.1.2. Example#

f <- function(a)
{
    g <- function(x)
    {
        return(x + 2)
    }
    if(a == 1)
        {
        return(g) 
        }
    return(sin)
}
f(2)(5)
-0.958924274663138
f(1)(5)
7

Notice that in the second example a new function g is defined inside an enclosing function f.

When higher order programming is available most of the main functional design patterns can be implemented in some form.

7.2. Pattern 1 - Composition#

7.2.1. Exercise#

Write a function compose which accepts two functions f and g and returns a function h where $\(h(x) = f(g(x))\)$

Hide code cell source
compose <- function(f,g)
{
    h <- function(x)
    {
        return(f(g(x)))
    }
    return(h)
}

f <- function(x)
{
    return(2*x)
}

g <- function(x)
{
    return(x + 2)
}

h <- compose(f,g)
print(h(3))

h <- compose(g,f)
print(h(3))
Hide code cell output
[1] 10
[1] 8

7.2.2. Exercise#

What limitations, if any, are there with your implementation of the compose function ?

7.2.3. A more versatile compose#

compose <- function(...) Reduce(function(f,g) function(.) f(g(.)), list(...), function(.) .)
h <- compose(f,g,sin,g,f)
h(3)
5.97871649324676

7.3. Pattern 2 - Currying#

Curry - a popular dish (top). Haskell B Curry - a computer scientist (bottom).

In mathematics a function \(f\) can be completely specified in terms of its graph \(\Gamma_{f}\). The graph is a (possibly infinite) set of ordered pairs, for example

\(\Gamma_{f} = \left\{ (1,1),(2,4),(3,9) \right\}\)

Conceptually the graph shows how to map the first element of a given pair to the second. This is emphasised through the notation \(f(1) = 1\) and \(f(3) = 9\) and so on.

7.3.1. Exercise#

Give a small example of a graph of function of the form \(f(x,y)\).

Write a function in R that implements it.

How many “arguments” does the mathematical function “take” ?

How many “arguments” does your R function “take” ?

So, mathematical functions map single entities to single entities. To make python functions behave in this way they can be curried. However, in R, the libraries that set out to implement a generic pattern for currying actually implement a related but different pattern called partial application (covered later in this section). To help out, here is some home made curry !!

curry <- function(f)
{
    args <- list()
    nargs <- length(formals(args(f)))
    curried <- function(...)
    {
        largs <- c(args,list(...))
        if(length(largs) == nargs)
        {
            return(do.call(f,largs)) 
        }
        args <<- largs
        return(curried)
    }
    return(curried)
}

7.3.1.1. Example#

h <- function(x,y) 
{
   return(x+y)
}
f <- curry(h)
z = f(2,3)  # function taking two arguments ???? No.
print(z)
[1] 5
g = f(2) # f is a function of one argument that returns a function 
z = g(3) # g is a function (with one argument)
print(z)
[1] 5

The “curry” pattern is slightly odd in that most functional programming languages do not need it since functions are curried by design.

7.3.2. Exercise#

How can currying be put to practical use ?

7.3.3. Solution#

Currying allows a programmer to provide a way of adding extra parameters to a function whilst still allowing it to be Reused in other code. A strategy in the pig game provides an excellent example of a use case for the curry pattern.

7.3.4. Exercise#

Try and use curried strategies in your pig competition code.

7.4. Pattern 3 - Partial Application#

Closely related to Currying, partial application accepts a function, freezes one or more of its arguments , and returns a new function with a reduced number of arguments. The frozen arguments are often referred to as the “bound” arguments or “bound” values.

f <- function(a,b,c) 
{
    return(2^a * 3^b * 5^c)
}
f(2,3,2)
2700
library(purrr)
Attaching package: ‘purrr’


The following object is masked _by_ ‘.GlobalEnv’:

    compose
g <- partial(f,b=3)
g(2,2)
2700

7.4.1. Exercise#

How does partial application differ from Currying ?

What effect does the position of the “bound” arguments have on the way the partially applied function is used ?

7.5. Pattern 4 - Classes and Objects#

7.5.1. Motivating Problem#

Here is a simple function

f <- function(x)
{
    return(2*x)
}

Now imagine you want to keep track of how many times this function gets used in a big program.

7.5.2. Solution#

count <- 0

for(i in 1:10)
{
    f(i)
    count <- count + 1
# ... lots more code
# ...
}

# somewhere else in your program
y <- f(8)
count <- count + 1

# ... lots more code
# ...

# somewhere else in your program
z <- f(3)/2
count <- count + 1

# ... lots more code
# ...

### and finally
cat("f was called ",count," times",'\n')
f was called  12  times 

7.5.3. Exercise#

What are the potential problems with this solution ?

Broadly speaking, how would you score this solution with respect to the 5Rs ?

7.5.4. Exercise#

Design an alternative approach counting the number of times f gets used.

Does your approach have any advantages over the original solution ?

How would you rate your solution with respect to the 5Rs

7.5.5. Exercise#

Consider the following function

agent <- function()
{
    state <- 0.71
    update <- function(x)
    {
        state <<- sin(state)
        return(x*state)
    }
    return(update)
}

and see if you can predict what the following code does.

agent_1 <- agent()
agent_2 <- agent()

print(agent_1(1))
print(agent_2(1))
print(agent_1(1))
print(agent_1(1))
print(agent_2(1))
Hide code cell output
[1] 0.6518338
[1] 0.6518338
[1] 0.6066452
[1] 0.5701145
[1] 0.6066452

The function logf is called a closure. It is a function. However, it is a function with extra information. The extra information is provided by the enclosing scope of the function (f in this case) within which the (nested) function g is defined.

In general, a function that returns other functions is called a class, and the functions returned are called objects. If the objects have access to the data inside the scope of the class then they are closures. Note a closure is not a nested function, it is the object that corresponds to the nested function that is the closure.

A program that makes use of classes and objects employs the programming paradigm known as object based programming.

7.5.5.1. Exercise#

Think of, and then describe, some use cases for object based programming.

7.5.5.2. Example#

logger <- function(f)
{
    count <- 0
    value <- function(x)
    {
        count <<- count + 1
        return(f(x))
    }
    log <- function()
    {
        
        return(count)
    }
    return(list("call"=value,"log"=log))
}

7.5.5.3. Exercise#

How do you think the logger class might be used ?

How does using the logger class rate as a solution to the motivating problem ?

What problem(s) does using the logger class present ?

Can you modify the logger class to overcome this problem(s) ?

7.5.5.4. Solution#

Hide code cell source
logged_f <- logger(f)
Hide code cell source
for(i in 1:10) 
{
    logged_f$call(i)
}

logged_f$log()
Hide code cell output
10