Introduction to Functional Programming
(Content adapted from Critchlow & Eck)
Functions are fundamental in computer programming, although not everything in programming that goes by the name of "function" is a function according to the mathematical definition.
In computer programming, a function is a routine that is given some data as input and that will calculate and return an answer based on that data. For example, in the Java programming language, a function that calculates the cube of an integer could be written
int cube(int n) {return n * n * n;}
In Java, int is a data type. From the mathematical
point of view, a data type is a set. The data type int
is the set of all integers that can be represented as 32-bit
binary numbers. Mathematically, then, .
(You should get used to the fact that sets and functions can
have names that consist of more than one character, since
it's done all the time in computer programming.)
The first line of the above function definition,
"int cube(int n)
", says that we are defining
a function named cube whose range is int
and whose domain is int. In the usual notation for
functions, we would express this as ,
or possibly as ,
where is the set of all
functions that map the set int to the set int.
The first line of the function, int cube(int n)
, is called
the signature of the function (in some languages, such as C++, it
is called the prototype). The signature specifies the
name, the domain, and the range of the function and so carries
exactly the same information as the notation "".
The "" in "int cube(int n)
" is a name for
an arbitrary element of the data type int. In computer
jargon, is called a parameter of the function.
The rest of the definition of cube tells the computer
to calculate the value of for any
by multiplying . The statement "return n * n * n
"
says that is the value that is computed, or "returned,"
by the function. (The stands for multiplication.)
Java has many data types in addition to int. There is a boolean data type named boolean. The values of type boolean are true and false. Mathematically, boolean is a name for the set . The type double consists of real numbers, which can include a decimal point. Of course, on a computer, it's not possible to represent the entire infinite set of real numbers, so double represents some subset of the mathematical set of real numbers. There is also a data type whose values are strings of characters, such as "Hello world" or "xyz152QQZ". The name for this data type in Java is String. All these types, and many others, can be used in functions. For example, in Java, is the remainder when the integer is divided by the integer . We can define a function to test whether an integer is even as follows:
boolean even(int k) {if ( k % 2 == 1 )return false;elsereturn true;}
You don't need to worry about all the details here, but you should
understand that the signature, boolean even(int k)
,
says that even is a function from the set int
to the set boolean. That is,
. Given
an integer , has the value true
if is an even integer, and it has the value false
if is an odd integer.
A function can have more than one parameter. For example, we might
define a function with signature int index(String str, String sub)
.
If and are strings, then would be the
int that is the value of the function at the ordered pair
. We see that the domain of index is the cross product
, and we can write
or, equivalently, .
Partial and Impure Functions
Not every Java function is actually a function in the mathematical sense. In mathematics, a function must associate a single value in its range to each value in its domain. There are two things that can go wrong: The value of the function might not be defined for every element of the domain, and the function might associate several different values to the same element of the domain. Both of these things can happen with Java functions.
In computer programming, it is very common for a "function" to be undefined for some values of its parameter. In mathematics, a partial function from a set to a set is defined to be a function from a subset of to . A partial function from to can be undefined for some elements of , but when it is defined for some , it associates just one element of to . Many functions in computer programs are actually partial functions. (When dealing with partial functions, an ordinary function, which is defined for every element of its domain, is sometimes referred to as a total function. Note that—with the mind-boggling logic that is typical of mathematicians—a total function is a type of partial function, because a set is a subset of itself.)
It's also very common for a "function" in a computer program
to produce a variety of values for the same value of its parameter.
This most frequently occurs because the function is impure—either
it has a side-effect (such as changing the value of a global
variable) beyond just computing a result, or it relies on a value that
has been changed as a side-effect of some other part of the program.
Another way to say that a function is impure is that it depends on a
hidden state—some extra information affecting the result that
is not provided by the function's direct inputs.
A common example is the method in the java.util.Random class with signature
int nextInt(int N)
, which returns a random integer between
0 and . The value of nextInt(5) could be 0, 1, 2, 3, or 4.
This is not the behavior of a mathematical function! Behind the scenes,
this function is storing a variable called the "seed", which is used to
generate a random value, plus a new value for the seed, each time the
function is called. Each time the seed changes, the next value to be
returned is likely to change.
Even though many functions in computer programs are not really mathematical functions, I will continue to refer to them as functions in this section. Mathematicians will just have to stretch their definitions a bit to accommodate the realities of computer programming.
Pure Functional Programming
Unlike Java, a typical functional programming language such as Scala, Haskell, or ReasonML will actively discourage the use of side-effects in functions.1 The benefit of restricting the programmer to pure functions, that always return the same value for a given argument, is that it becomes possible to reason about the behavior algebraically, freely substituting the returned values in place of function calls without having to worry about whether some "hidden state" might have changed since the last time the function was called.
For example, suppose that we have a function that computes the value of some polynomial, such as . If we know that another function is pure, then we can be sure that is the same as , as well as . All of these are algebraically equivalent, as long as is pure. This allows the programmer to reason more easily about the correctness of their program, and it also enables the computer to choose any equivalent expression for evaluation, ideally choosing the most efficient version.
One of the most common ways that a functional language will encourage
pure functions is to do away with, or at least severely restrict, the
ability to update the value assigned to a variable. In ReasonML, there
is no assignment statement. When a value is bound to a variable with a
let
statement, that variable will then remain bound to that value for
as long as the variable exists. A variable will cease to exist when the
block (such as a function body) containing it is finished:
A variable may be temporarily shadowed by another variable with the same
name. This may look like an assignment of a changed value to a variable,
but each use of the let
statement will create a new named location in
memory; if the shadowing variable goes away, the original one will become
visible again with its correct value:
Again, this behavior permits algebraic reasoning about the program. The above code is equivalent to
where we have uniformly renamed the inner variable x as y to make it clear that they are distinct variables. It is also equivalent to
where we have replaced each use of our identifiers with its value. The output is slightly different in this case, because we no longer have the top-level binding of 42 to x that would have been available if we wrote additional lines at the bottom of the program, but it is equivalent if we just look at the printed results.
First-Class Functions
In most programming languages, functions are not first-class objects. That is, a function cannot be treated as a data value in the same way as a String or an int. However, recent versions of Java have taken a step in this direction. It is possible for a function to be a parameter to another function, as long as it is wrapped in a "function object". For example, consider the function signature
int sumten( Function<Integer, Integer> f )
This is a signature for a function named sumten whose
parameter is a function object. The parameter is specified by the
type "Function<Integer, Integer>
". If S and T are types, then
the type Function<S, T>
represents functions from S to T. Therefore,
the parameter of sumten is essentially a function from int to int.2
The parameter name, , stands for an arbitrary such function. Mathematically,
, and so
.
My idea is that would compute . A more useful function would be able to compute for any integers and . This just means that and should be parameters to the function. The signature for the improved function would look like
int sum( Function<Integer, Integer> f, int a, int b )
The parameters to sum form an ordered triple in which the first coordinate is a function and the second and third coordinates are integers. So, we could write
It's interesting that computer programmers deal routinely with such complex objects.
There are several ways of providing a function object as an
argument in Java. If we want to pass the method m of an object
x, where the signature of m is int m( int i )
, then
we can call our function as sum(x::m, a, b)
. However, a more
general technique is to use an anonymous function, also known
as a lambda.3
Anonymous functions
In Java, an expression such as i -> { return i * i * i; }
creates a function object that takes an int (this will be
determined from the context) and returns another int. This
particular function cubes its input. Thus, if we call our sum function as
follows:
sum(i -> { return i * i * i; }, 3, 5)
the result will be , which is 216.
Many languages now support a similar syntax for creating anonymous
function values, and offer some facility for working with functions
as (mostly) first-class objects. For example, the same function
is expressed in ReasonML as i => { i * i * i }
. Since one of the hallmarks of
the functional languages is their ability to work with function
values, you can imagine that they tend to provide the most
thorough integration of functions with other kinds of values.
Here are complete demos of the sum example, first in Java and then in ReasonML:
import java.util.function.Function;public class SumDemo {private static int sum(Function<Integer, Integer> f, int a, int b) {int total = 0;for (int i = a; i <= b; i++) {total += f.apply(i);}return total;}public static void main(String[] args) {System.out.println(sum(i -> { return i * i * i; }, 3, 5));}}
Note that we define the function sum in ReasonML by binding a function value to the name sum, just as we can bind other types of values to identifiers. The keyword "rec" after the "let" indicates that the right-hand side expression is allowed to make use of the name that is currently being defined, so that we can make our function recursive. Without the "rec", any use of sum in the right-hand expression would refer to an older binding to that name.
As a simpler function definition in ReasonML, not needing the "rec" keyword, here is our cube function again:
This is just binding the anonymous function we have been using above
to the name cube. Note that the function n => { n * n * n }
is
exactly the same as the function i => { i * i * i }
, because the name
of the parameter does not matter outside the function.
An interesting fact about ReasonML is that the operators are also functions,
bound to names made out of operator symbols instead of letters and digits. To
refer to an operator as a function value, just put the operator in parentheses:
(+)
, (*)
, (==)
, …. Therefore, an expression such as a + b * c
can also be written as (+)(a, (*)(b, c))
(note that this takes into account
the usual higher precedence of multiplication over addition).
For example, if we wanted to define an exponentiation operator on int, and
call it ***
, we could define it as follows:4
It is even possible in functional languages for a function to return another function as its value. For example,
Here, x *** n
is our exponentiation operator from above, which computes , so for any
integers and , the value of is
a function that computes . Thus,
would define to be the function that satisfies . This is now ready to be handed to our sum function:
would compute . In fact, monomial can be used to create an unlimited number of new functions from scratch. It is even possible to write monomial(2, 3)(5) to indicate the result of applying the function monomial(2, 3) to the value 5. The value represented by monomial(2, 3)(5) is , or 250. This is real functional programming and might give you some idea of its power.
Exercises
For each of the following Java function signatures, translate the signature into a standard mathematical function specification, such as .
int strlen(String s)
Answer
double pythag(double x, double y)
Answer
int round(double x)
Answer
String sub(String s, int n, int m)
Answer
String unlikely(Function<String, Integer> f)
Answer
int h(Function<Integer, Integer> f, Function<Integer, Integer> g)
Answer
Write a Java function signature for a function that belongs to each of the following sets.
Answer
String foo(String s)
Answer
boolean bar(double x, double y)
Answer
double baz(Function<Integer, Integer> f)
It is possible to define new types in Java. For example, the definition
public class Point {public double x;public double y;}
defines a new type named Point. A value of type Point contains two values of type double. What mathematical operation corresponds to the construction of this data type? Why?
Answer
Each element of Point contains a pair of numbers, so the type corresponds to the cartesian product .
Let cube, sum and monomial be the ReasonML functions described in this section. What is the value of each of the following?
- sum(cube, 2, 4)
Answer
- sum(monomial(5, 2), 1, 3)
Answer
- monomial(cube(2), 7)
Answer
The function such that
- sum(, 1, 5)
Answer
- cube(sum(monomial(2, 3), 1, 2))
Answer
- sum(cube, 2, 4)
Write a ReasonML function named compose that computes the composition of two functions. That is, is , where and are functions of one parameter. Recall that is the function defined by .
Answer
let compose = (f, g) => { x => { f(g(x)) } }
Consider the following ReasonML function:
- What is the value of
exercise(4, 5)
?Answer
9
- What is the value of
exercise(12, 13)
?Answer
25
- Use algebraic substitution to evaluate
exercise(a, b)
in terms of the variablesa
andb
.Answer
First,
c = a - b
, andd = a - c
, sod = b
. Now,e = c + d
, soe = a
. Finally, the result iss(d) - s(e)
, which will beb * b - a * a
.
- The most common exception is for functions that send
output to the console, such as the
print_int
function in ReasonML. Being able to track execution and easily display results is very useful, and printing a line on the console is a fairly benign side-effect—it won't cause the function to return different values for the same arguments. However, printing a result can still interfere with algebraic reasoning about a program, because interchanging such a function call with its value can affect whether and how many times the output is printed.↩ - For our purposes we may ignore the distinction between int and Integer in Java.↩
- The mathematician Alonzo Church introduced in the 1930's the use of the Greek letter lambda () to indicate an otherwise unnamed function defined by a formula. That is, instead of writing "the function where ", he wrote "". When the first functional programming language, LISP (invented in the late 1950's), needed a way to create function values, John McCarthy adopted Church's use of lambda, and the name has stuck.↩
- The code here is based on the solution to an exercise in the Recursion section.]↩