Skip to main content

Chapter 7 Functional Programming

This chapter introduces ideas of functional programming, which is a way to think about programming using functions. Typically this requires a language that falls into the category of a functional computer language, one of which is Julia. In short, a language that has this feature has a function (or procedure) as a fundamental element of the language and one of which can be passed as arguments.

Section 7.1 Functional vs. Non-functional forms

A common operation is to start with some array and create a new array based on the first one where there is some relation between the two. This is often called a mapping. If we start with the following array:
arr=collect(1:5)
and we want a new array that is the square of each element. Using a statement-based method, the following will create this using a for loop:
newarr=zeros(Int,5)  # this an array of zeros of length 5
for i=1:5
  newarr[i]=arr[i]^2
end
newarr
which returns the array [1,4,9,16,25].
There is a much simpler way to do this using a method called map which takes in a function and an array and produces another array in which a function acts on each element of the array. In this case, the function is the square.
f(x)=x^2
arr=[1,2,3,4,5]
map(f,arr)
This can be written even more simply, by using an anonymous function for the first element or:
map(x->x^2,arr)
The key to this example is the ability to pass the function as an argument to the map command. Once you get a feeling for functional programming, it is often an easier way to write code.
 1 
An extreme example of this is to avoid using any for loops after understanding maps and other related functions. Although, often this can happen, it doesn’t 1) make the underlying code any faster or 2) any easier to understand, so I don’t generally prescribe to this philosophy.

Subsection 7.1.1 Anonymous Functions

The example above included in the first argument x->x^2, which is an example of a anonymous function. In short, it is anonymous because we didn’t assign it to a name and instead just included the function as an argument to the map command and this is precisely when anonymous functions are used.
Recall that we saw a couple of examples of this in Chapter 6. One example was filter(n -> n%2 == 0, A) which returns all elements of A where the element is an even number. The first argument of filter is a function and if the function is simple, passing in as an anonymous function is typical. The other example we used was in the sort command and can pass a function which is applied before sorting using the by command.
Any time that a function is used in the form ->, it is anonymous. For example, if we want to sum the contents of the array A=[1,2,3,4,5], one way is to
reduce((x,y)-> x+y, A)
which results in 15, the sum of the elements of the array. We’ll see the reduce function in the next section. Concentrate right now on the (x,y)->x+y, an anonymous functions can take on more than one variable.
Check Your Understanding 7.1.
  1. Write a statement-based loop to take the array [1,2,3,4,5] and output an array that is the reciprocal of each number.
  2. Write a functional-based code using the map command to do the same.

Subsection 7.1.2 Mapping over two arrays

The map function can work over multiple arrays as long at the first argument, which is the function can take as inputs multiple values. A simple example is to multiply element by element over vectors like
map(*,[1,2,3],[10,20,30])
and the result is [10, 40, 90].
An astute reader will remember that we could have also done this with broadcasting as in [1,2,3] .* [10, 20, 30], but the point of this is show an example with mapping over two arrays. Another example of this using an anonymous functions is
map((x,y)->x^2+y,[1,2,3],[10,20,30])
which squares each element in the first array, then adds to a second array resulting in [11, 24, 39] and again this could have been done with broadcasting.

Section 7.2 Reducing an array

The map command in general takes in an array and returns an array. Another common task with arrays is to reduce the entire array to a single value. If arr=[1,2,3,4,5], then we can sum the values with the reduce command as in:
reduce((x,y)->x+y,arr)
which returns 15. This could be written simpler with reduce(+, arr).
To multiply all numbers, we can type:
reduce((x,y)->x*y,arr)
and the result is 120. This could be written simpler with reduce(*, arr). In both of these cases, there is a little bit hidden, so let’s look at these in more detail.
  1. First, the first argument of the reduce command is a 2-argument (or binary) function.
  2. Secondly, the operation is done on all numbers on the array, but needs to start with some value (because it is a binary function). In the sum case, the initial value is 0 (by default) and in the multiply case it is 1.
These are perhaps obvious examples and each of these have the built-in functions sum and prod that find the sum and product of an array.
Let’s look at an example that returns the number of array elements that are greater than zero:
numPos(arr) = reduce((n,val) -> val > 0 ? n+1 : n, arr, init=0)
and then if it is tested on an array of both positive and negative numbers:
numPos([-3,5,8,-2,11])
this returns 3. How this works is as follows. There are two values associated within reduce from the function. The first is n and the second is val.
  1. The variable n is initially 0 (this is the init=0 part of the function call)
  2. On the first step, val takes on the first value in the array (or -3). It is checked if it is positive and if so, return n+1 or n. Since n=0 and -3 is not positive, then the function returns 0.
  3. On the second step, val is 5 and this time the function returns n+1 or 1
  4. val is 8 and the function returns n+1 or 2
  5. val is -2 and the function returns n or 2
  6. val is 11 and the function returns n+1 or 3.
  7. Since the array has been passed through, the result is the last value or 3.

Check Your Understanding 7.2.

  • Write a reduce function that will count the number of times the string "hi" appears in an array. Test it on ["hi","bye","hi","hello"] and other arrays of strings.
  • What does reduce(*, ["J","u","l","i","a"],init="") do?

Section 7.3 The mapreduce function

The mapreduce function is perhaps more helpful than reduce. For example, if we want to sum the squares of each number in an array, then mapreduce can do this easily.
mapreduce(x->x^2,+,[1,2,3],init=0)
is a short-hand way to do \(1^2+2^2+3^2=14\text{.}\) For mapreduce, the first argument is a function of one variable (unary) that is applied to every element of array. The second argument is a binary element that is used for the reduce part. Note: Julia also has a version of the sum function that can apply a function. For example, sum(x->x^2,[1,2,3]) returns the sum of 14.
Another example that we will see later is how to write a polynomial using its coefficients. For example, if coeffs = [-2,4,5,7], then we want to create a way to produce the polynomial "-2 x^0 + 4 x^1 + 5 x^2 + 7 x^3". This is a clear candidate for either a reduce or mapreduce because we have an array and reduce a single thing (in this case a string). We will use mapreduce in this case because there is a transformation of each element which produces the polynomial term and then the reduce is a concatenation.
Although it seems like we should use the array itself, we are going to use the array (technically the range), 1:length(coeffs), so we can produce the powers in each of the term. Therefore the mapping function will be
i -> "$(coeffs[i])x^($i-1)"
which take a number i and looks up the right coeffs and raise to the i-1 power, since we need to shift the power by one. Next, we need a function that will concatenate the terms. This will be
(str, term) -> str * " + $term "
where str is the current version of the polynomial string and term will be the current term (which is created with the mapping function). Putting this altogether
mapreduce(i -> "$(coeffs[i]) x^$(i-1)", (str, term) -> str * " + $term " , 1:length(coeffs) )
and this will return the string "-2 x^0 + 4 x^1 + 5 x^2 + 7 x^3 ". We will use this in Section 12.3 when we crate a Polynomial type and this function will show the results in a more natural way.

Check Your Understanding 7.3.

In calculus, an important infinite series is
\begin{equation*} 1 + \frac{1}{4} + \frac{1}{9} + \frac{1}{16} + \cdots \end{equation*}
and although we can’t sum an infinite number of terms, a finite version of this is still useful.
Use mapreduce to sum the first 20 terms.
Hint.
Use arr=1:20 for the array and the mapping function is the reciprocal.

Section 7.4 Mapping a Function over an 2D array

Above, we saw the map function which applies a function over each element of the array. We will see in Chapter 19 that the mapslices functions is quite helpful.
Consider the array A=[i+j for i=1:10,j=1:3] which returns the array
10×3 Matrix{Int64}:
  2   3   4
  3   4   5
  4   5   6
  5   6   7
  6   7   8
  7   8   9
  8   9  10
  9  10  11
 10  11  12
 11  12  13
If we want to sum down the columns of the array, we can enter
mapslices(sum,A,dims=[1])
which returns the array[65 75 85], which is a 1D array with the column sums. The mapslices function needs three arguments, a function that can take an array, a 2D array and the keyword dims which says how to apply the sum. Note: we can also replace the command sum with + as well.
If instead we wanted to sum along rows, then
mapslices(sum,A,dims=[2])
which returns
10×1 Matrix{Int64}:
  9
 12
 15
 18
 21
 24
 27
 30
 33
 36
Although note that this is a 2D array. The function mapslices returns an array of one fewer dimensions as the original.

Check Your Understanding 7.4.

Using the matrix A=[i+j for i=1:10,j=1:3],
  • evaluate mapslices(prod,A,dims=[1]). What does that function do?
  • evaluate mapslices(prod,A,dims=[2]). What does that function do?
  • use the mapslices function to find the maximum element in each row.
  • use the mapslices function to find the maximum element in each column.

Section 7.5 Do-Begin-End blocks

All of the functions that are applied in the examples in this chapter are relatively simple in that they can be written as a single line and we do so in anonymous form. Consider instead a more complicated example that would be very difficult and not readable as a single line.
The input will be an array. For each number i in the array if it is a multiple of 3, return the number, if i mod 3 is 1, then return the square. If i mod 3 is 2, then return twice the number.
This can be written as
map(1:10) do i
	begin
		if i % 3 == 0
			x = i
		elseif i % 3 == 1
			x = i^2
		else
			x = 2i
		end
		x
	end
end
and you may notice that this looks much different that the map function above. The structure is basically
map(values) do var
	begin
		# some steps/expression on variable var
		#
		# last line is the output for the given value of values.
	end
end
This works with other functions as well. The function that counts the number of positive numbers in a list can be written as
numPos(values::Vector{Int}) = reduce(values, init = 0) do n, val
    begin
        pos = n
        if val > 0
            pos += 1
        end
        pos
    end
end
and then can be run with numPos([4,-8,22, 100, -2, 0, 4]) and return 4. This is clearly longer than the previous version of this, but perhaps much more readable.
NOTE: ADD AN EXERCISE HERE