Seek Product Blog

Product blog Updates & Insights from the Product & Technology teams at SEEK

Functional Programming 101

02 Apr 2014

In the fast paced and ever changing world of software development, functional programming is not only a challenge to the current way of thinking but also a great opportunity to improve the way we develop software. The underlying principles of functional programming involve using functions to act as the building blocks of a program, rather than objects and procedures. Here at SEEK we take our coding very seriously and want to reduce the complexity behind the scenes while still making use of a powerful paradigm to tackle complex real world programming tasks.

Inspired by lambda calculus (and that’s about as mathy as we’ll get), functional programming is built on the idea that pure functions should only return a result based on the values they’re passed, with no additional reading and writing from files or databases. A pure function is effectively an equation, if you pass in a particular value you will always be given the same result regardless of external factors, or how many times you call the function. The same value in, gets the same result back, every time. Although that seems common sense, in this realm of functional programming there are a multitude of languages you can use, and choosing the right one to deliver your functionality is half the battle. A fully functional programming language will allow the declaration and use of functions anywhere you can declare or use a value, whereas an object-oriented programming language will require it’s functions (called methods) and data to be contained within a class (or type).

You may be familiar with procedural (or imperative) programming if you’ve come across C or Pascal, or object-oriented and imperative programming if you’ve seen Java or C#. Functional (or declarative) programming is another programming paradigm, harking back to the late 50s with Lisp, which has been experiencing something of a resurgence over the last few years. Interestingly, some modern languages are blending these paradigms, for example: Scala could be considered an even mix of functional and object-oriented programming; and C#, while fully object oriented, supports some functional features (with LINQ being the most prominent application).  The development team at SEEK are effectively a .NET shop, so using F# as our functional language and C# as our functional-friendly object-oriented language let’s have a look at a few examples.

// An F# function:
let add x y = x + y
// A C# method:
int Add(int x, int y)
{
    return x + y;
}

Both of these functions do the same thing, although, you can see F# is a little more succinct than C#. This brevity is typical for many functional languages. 

Treating functions like a value

A good functional programming language should be able to pass around functions like values and by doing so enable more flexible code.

// F#
let apply fn x y = fn x y
let x = apply add 1 2
// C#
int Apply(Func<int, int, int> fn, int x, int y)
{
    return fn(x, y);
}
void Main()
{
    int x = Apply(Add, 1, 2);
}


Again, both of these do the same thing, with the F# ‘apply’ function and the C# ‘Apply’ method both taking a parameter ‘fn’ that is treated as a function. In our example we use the previous ‘add’ function and apply it to the two numbers passed in.

Immutability

Immutability, another important aspect of functional programming, goes hand in hand with pure functions and is simply the notion that once assigned a value, a variable cannot change. 

// F#
let x = 1                             // automatically immutable
let x = 2                             // this won’t even build
// C#
readonly int x = 1;                   // need to specify this is immutable
void Main() { x = 2; }                // this won’t build


Recursion

Writing code using values that don’t change makes for safer programming, and makes it easier to take advantage of multiple CPUs. Now-a-days, with Moore’s Law failing us, computers come with more CPU’s rather than faster ones which means you need to write code explicitly to use those extra CPU’s. In the typical Java and C# world of changing variables it can become a nightmare to manage, in comparison to the functional programming developer who can spend more time on the solution rather than handling errors.

Recursion is simply the act of a function calling itself, and with all of its complex glory, it can be the only answer for complex problems. Remember Fibonacci from school maths or perhaps Agile estimations? Basically, a Fibonacci number is worked out by adding the two previous Fibonacci numbers before it, eg. 1, 2, 3, 5, 8, 13, 21. Let’s look at a few functions that determine a particular Fibonacci number.

// F#
let fib n =
    let rec loop i a b =                    // inner recursive function!
        if i = 0 then b                     // at zero return our ‘b’ variable
        else loop (i-1) b (a+b)             // recursive call to loop
    loop n 1 1                              // start with the ‘seed’ numbers
// C#
int Fib(int n)
{
    Func<int, int, int, int> loop = null;
    loop = (i, a, b) =>                     // inner recursive function
    {
        if (i == 0) return b;               // return ‘b’ at zero
        else return loop(i - 1, b, a + b);  // recursive call
    };
    return loop(n, 1, 1);                   // start with the ‘seed’ numbers
}

In both cases the code is calling itself in an attempt to work out each number until we get to the one we want, finally returning the fifth one. If we work through this, the F# calls would look something like this:

fib 5
  loop i=5 a=1 b=1               ? our first number is second seed number of 1
   loop i=(5 - 1) a=1 b=(1 + 1)   ? add two seed numbers and second number is 2
    loop i=(4 - 1) a=2 b=(1 + 2)   ? add the two previous and third number is 3
     loop i=(3 - 1) a=3 b=(2 + 3)   ? fourth number is 5
      loop i=(2 - 1) a=5 b=(3 + 5)   ? fifth number is 8
       loop i=(1 - 1) a=8 b=(5 + 8)   ? finally with i at zero we return number 8

Data Manipulation

Put these together and you find that functional languages lend themselves to data manipulation, a very useful trait for a language to have these days. You may have heard of MapReduce? If not, it’s a way of taking a list of data elements, applying a function to them in turn and aggregating them. For example, if we wanted to sum up the square of a specific set of numbers, we could do something like this:

// F#
let sqr x = x * x               // square the given number
let multipleOf3 x = x % 3 = 0   // true if a multiple of 3 
let sumSqr n =
    [ 1..n ]
    |> Seq.map sqr              // convert each number to its square
    |> Seq.where multipleOf3    // filter to include multiples of 3
    |> Seq.sum                  // total them up
// C#
int Sqr(int x)
{
    return x*x;
}
bool MultipleOf3(int x)
{
    return x%3 == 0;
}
int Sum(int n)
{
    return Enumerable.Range(1, n)
       .Select(Sqr)             // C#'s Select is equivalent to F#'s map
       .Where(MultipleOf3)
       .Sum();
}


Testing, refactoring, and general maintenance are a key consideration in successful software development, with functional programming giving us the tools to tackle these problems in a more productive way. The flexibility of using functions like values, relying on immutability and applying recursion judiciously allows you to achieve safer more reliable code, as well as opening up the potential of multiple CPUs. Throw data manipulation into the mix and a whole class of business problems become easier to solve.Obviously the above is a contrived example, but don’t let this fool you, this process of mapping, filtering and aggregating data is very powerful. 

Jeremy Clough is the Solution Lead for our Search Team at SEEK.