First published on MSDN on Jul 12, 2017
Guest post by Thomas Denny Microsoft Student Partner at the University of Oxford
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.
IntroductionOver 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 valuesThe 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 isf(f(...f(array[0], array[1]), ...), array[array.length - 1])
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
arenames
andmaxLength
, 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 likeprintf
, 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
, andreduce
methods we now use functions that are part of theSeq
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.
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
.
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.
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
Updated Mar 21, 2019
Version 2.0Lee_Stott
Microsoft
Joined September 25, 2018
Educator Developer Blog
Follow this blog board to get notified when there's new activity