A gentle introduction to functional programming for web programmers using F#


Guest post by Thomas Denny Microsoft Student Partner at the University of Oxford

thomasdenny

About Me

Hi I am currently studying Computer Science at the University of Oxford.

I am the president (2017-18) and was the secretary (2016-17) of the Oxford University Computer Society, and a member of the Oxford University History and Cross Country societies. I also lead a group of Microsoft Student Partners at the university to run talks and hackathons.

Introduction

Over the last decade functional programming rose in prominence such that now nearly all high-level programming languages support it in some way, and I firmly believe that learning functional programming techniques have the potential to affect every facet of modern software engineering practices. Whilst there is value in exploring the functional programming features of your language of choice, it is often worth exploring them in a language that was designed from the ground up to be functional. This way you can see interesting or useful ideas applied in a succinct manner; the same ideas may be harder to apply in other languages, but could reduce code complexity later on all the same.

A typical web programmer will almost certainly be familiar with ECMAScript 6, the most recent version of the JavaScript standard. In this article I will use TypeScript, which is a superset of ES6 with additional syntax. In order to explore functional programming we will use F#, a language by Microsoft that works on top of the .Net stack.

Functions as values

The core tenet of functional programming is that functions can be treated as values, and passed around just like any other value. As a simple example, here is a TypeScript function that takes a function value as an argument, calls it, and returns its result:

function call<T>(f: () => T): T {
    return f();
}

In the above we use TypeScript's [generic functions][generics] to define a generic function that will work with functions that return any type. Now suppose we define a simple function and use the call function:

function n(): number {
    return 42;
}

console.log(call(n));

This example will then print out the number 42. We don't necessarily need to name a function to create a function value, and we can instead just create a closure value instead:

console.log(call( () => 42 ));

Now these examples aren't very interesting, but they demonstrate the basics. A very common use-case for functional programming is working with lists of data. Suppose that we have a list of names, and we want to filter out any names longer than a certain length, then tag each name with its length, and then concatenate all of these strings together. A typical function written in an imperative style for this might look like:

function filterNames(names: string[], maxLength: number): string {
    let output = null;
    for (let name of names) {
        if (name.length <= maxLength) {
            let nameWithLength = name + " (" + name.length.toString() + ")";
            if (output == null) {
                output = nameWithLength;
            }
            else {
                output = output + ", " + nameWithLength;
            }
        }
    }
    return output;
}

Arrays in ES6 support three important functions: filter, map, and reduce. Each of these takes a function, applies that function to each element in the array, and then returns a new array. This therefore allows to rewrite the above function as:

function filterNamesFunctional(names: string[], maxLength: number): string {
    return names.filter(n => n.length <= maxLength)
                .map(n => `${n} (${n.length})`)
                .reduce((output, n) => output + ", " + n);
}
  • filter applies the function predicate to each element in the array and returns a new array only containing the elements for which the predicate was true

  • map applies a function to each element in the array and returns a new array containing the result of that function applied to each element
  • reduce (in this context) will return the first element of the array if there is only one, but otherwise it will repeatedly apply to the function to an accumulator (which starts as the first element), and the next element in the array, so the result is f(f(...f(array[0], array[1]), ...), array[array.length - 1])

Mapping, filtering, and reducing in F

We'll start out by copying the last TypeScript function and implementing it in F#. Once you've got F# installed on your computer, create a new file func.fsx; fsx files are F# scripts and you can run them by running fsharpi <filename> at a command line.

let names = [
    "thomas"
    "james"
    "steve"
]
printfn "%A" names

If you run this you'll then see ["thomas"; "james"; "steve"] printed to the console. In F# we can declare lists either with each entry separated by a newline or each entry separated by a semicolon; a newline is sufficient for lines of code. The printfn function is similar to the printf function in C and it supports the same formatting specifiers. Note that the way that we call the function is different to ES6: each argument is just separated by a space.

Now let's implement the function from ES6:

let filterNamesFunctional names maxLength =
    names
    |> Seq.filter (fun name -> (String.length name) <= maxLength)
    |> Seq.map (fun name -> sprintf "%s (%d)" name (String.length name))
    |> Seq.reduce (fun output name -> output + "," + name)

There's lots to learn from this:

  • We declare functions in F# in the same we declare values (they're the same thing!) using let name = value. Importantly, however, when we declare a value like this we cannot change the value associated with it later

  • The parameter names of the function filterNamesFunctional are names and maxLength, just like before. Like TypeScript, F# is also statically typed (types are checked at compile time), but it also supports type inference, which means that in most cases the compiler can work out the type without the programmer need to declare it
  • fun arg0 arg1 ... -> result in F# is equivalent to (arg0, arg1, ...) => result in TypeScript
  • We have to use String.length to get the length of a string
  • sprintf works like printf, but returns a string
  • When an argument to a function needs to call a function we have to surround it in parentheses to ensure that it is not ambiguous
  • Rather than using map, filter, and reduce methods we now use functions that are part of the Seq module, which can manipulate sequences. We then 'pipe' the result of each stage to the next using the 'piping operator' |> (there are several other piping operators; this is the forward piping operator)

Then, to print the result, we call the function and output the result with printfn as before:

printfn "%A" (filterNamesFunctional names 5)

Composing functions

Once you're used to the idea of functions as values, a natural extension is to compose them together to create new functions. Suppose we we want to double a number and square it. Two such approaches could be:

let double x = x * 2.0
let square x = x ** 2.0 // ** is the exponent operator in F#
let squareDouble x = square (double x)
let squareDouble2 x = x |> double |> square

However, both of these approaches still need to mention the x in both the declaration and definition of the function. Instead, we can use the function composition operator >>:

let squareDouble = double >> square

The value double >> square is itself a function! It takes a value, passes it to the double function, passes the result of that to the square function, and then returns the result. It's important to note that you can only compose functions that take one argument.

Currying

Internally in F# all functions only take one argument, which might seem a bit weird as we've clearly seen functions filterNamesFunctional already that take more than one argument. In reality, whenever you have a function that appears to take more than one argument, the function is actually written so that it returns a function that takes the next set of arguments. For example, the function let add x y = x + y is actually implemented as:

let add x = (fun y -> x + y)

This is incredibly powerful, because it enables you to curry, or partially apply, a function so that it can be reused in different contexts. For example, we can create a function that always adds 42 to its input:

let add42 = add 42

Options

In TypeScript, as is the case in many programming languages, many values are actually optional and either represent something or nothing. Typically this is represented with a pointer either pointing to some value, or null. This is incredibly fragile, and can easily break! For example, the following TypeScript code will crash when it is run:

function printLength(s: string) {
    console.log(s.length);
}
printLength(null);

I get the error TypeError: Cannot read property 'length' of null when I try to run this. F# helps to prevent this by introducing an Option type and forcing you to unwrap it whenever you wish to use it:

let printLength s =
    match s with
    | None -> printfn "0"
    | Some(s) -> printfn "%d" (String.length s)

This is also the first time we've seen a match expression. This is similar to a switch statement in TypeScript or many other programming languages, but it supports pattern matching. There are, however, several more compact ways to write this function; you can see examples in the F# guide.

Parallel mapping

All of the functions we've looked at so far are side-effect free, i.e. they don't mutate anything outside of their scope. If we have several independent function calls to side-effect free functions then we can be sure that if we execute them in parallel they will not adversely affect one another. For example, F# makes it possibly to map over a list in parallel by automatically running the mapping function in separate threads (where possible):

open Microsoft.FSharp.Collections

let pmap f ls =
    ls
    |> PSeq.map f
    |> PSeq.toList // We have to convert back to a list

This behaves, externally, just like List.map but has the advantage that it can potentially run much faster, depending on the number of cores, the number of items in the last, and how quickly f executes on each item.

It is possible to define a function similar to PSeq in other programming languages, however it may be easier to introduce bugs if the function passed to it is not side-effect free; F# helps prevent that.

Units of measure

Whilst none of the features described so far are unique to F# (and are in fact present in most functional programming languages), there are certainly features that are, and units of measure is one of them. Typically when programming we might represent a physical quantity, such as a distance, with a value such as a double. This is fine if you can always be certain of the unit that you are dealing with, but often you may not be. Units of measure add type safety to these values, without sacrificing the ability to use functions already defined for numeric types. For example:

[<Measure>] type mile
[<Measure>] type mile
[<Measure>] type hour

let mph = 60f<mile> / 2.0f<hour>

Extra reading

Comments (0)

Skip to main content