At 67 Bricks, we are big proponents of functional programming. We believe that projects which use it are easier to write, understand and maintain. Scala is one of the most common languages we make use of and we see more object oriented languages like C# are often written with a functional perspective.
This isn’t to say that functional languages are inherently better than any other paradigms. Like any language, it’s perfectly possible to use it poorly and produce something unmaintainable and unreadable.
Immutable First
At first glance immutable programming would be a challenging limitation. Not being able to update the value of a variable is restrictive but that very restriction is what makes functional code easier to understand. Personally I found this to be one of the hardest concepts to wrap my head around when I first started functional programming. How can I write code if I can’t change the value of variables?
In practice, reading code was made much easier as once a value was assigned, it never changed, no matter how long the function was or what was done with the value. No more trying to keep track in my head how a long method changes a specific variable and all the ways it may not thanks to various control flows.
For example, when we pass a list into a method, we know that the reference to the list won’t be able to change, but the values within the list could. We would hope that the function was named something sensible, perhaps with some documentation that makes it clear what it will do to our list but we can never be too sure. The only way to know exactly what is happening is to dive into that method and check for ourselves which then adds to the cognitive load of understanding the code. With a more functional programming language, we know that our list cannot be changed because it is an immutable data structure with an immutable reference to it.
High Level Constructs
Functional code can often be more readable than object oriented code thanks to the various higher level functions and constructs like pattern matching. Naturally readability of code is very dependent on the programmer; it’s easy to make unreadable code in any language, but in the right hands these higher level constructs make the intended logic easier to understand and change. For example, here are 2 examples of code using regular expressions in Scala and C#:
def getFriendlyTime(string time): String = {
val timestampRegex = "([0-9]{2}):([0-9]{2}):([0-9]{2}).([0-9]{3})".r
time match {
case timestampRegex(hour, minutes, _, _) => s"It's $minutes minutes after $hour"
case _ => "We don't know"
}
}
public static String GetFriendlyTime(String time) {
var timestampRegex = new Regex("([0-9]{2}):([0-9]{2}):([0-9]{2}).([0-9]{3})");
var match = timestampRegex.Match(time);
if (match == null) {
return "We don't know";
} else {
var hours = match.Groups[2].Value;
var minutes = match.Groups[1].Value;
return $"It's {minutes} minutes after {hour}";
}
}
I would argue that the pattern matching of Scala really helps to create clear, concise code. As time goes on features from functional languages like pattern matching keep appearing in less functional ones like C#. A more Java based example would be the streams library, inspired by approaches found in functional programming.
This isn’t always the case, there are some situations where say a simple for loop is easier to understand than a complex fold operation. Luckily, Scala is a hybrid language, providing access to object oriented and procedural styles of programming that can be used when the situation calls for it. This flexibility helps programmers pick the style that best suits the problem at hand to make understandable and maintainable codebases.
Pure Functions and Composition
Pure functions are easier to test than impure functions. If it’s simply a case of calling a method with some arguments and always getting back the same answer, tests are going to be reliable and repeatable. If a method is part of a class which maintains lots of complex internal state, then testing is going to be unavoidably more complex and fragile. Worse is when the class requires lots of dependencies to be passed into it, these may need to be mocked or stubbed which can then lead to even more fragile tests.
Scala and other functional languages encourage developers to write small pure functions and then compose them together in larger more complex functions. This helps to minimise the amount of internal state we make use of and makes automated testing that much easier. With side effect causing code pushed to the edges (such as database access, client code to call other services etc.), it’s much easier to change how they are implemented (say if we wanted to change the type of database or change how a service call is made) making evolution of the code easier.
For example, take some possible Play controller code:
def updateUsername(id: int, name: string) = action {
val someUser = userRepo.get(id)
someUser match {
case Some(user) =>
user.updateName(name) match {
case Some(updatedUser) =>
userRepo.save(updatedUser)
bus.emit(updatedUser.events)
Ok()
None => BadRequest()
case None => NotFound()
}
def updateEmail(id: int, email: string) = action {
val someUser = userRepo.get(id)
someUser match {
case Some(user) =>
user.updateEmail(email) match {
case Some(updatedUser) =>
userRepo.save(updatedUser)
bus.emit(updatedUser.events)
Ok()
case None => BadRequest()
case None => NotFound()
}
Using composition, the common elements can be extracted and we can make a more dense method by removing duplication.
def updateUsername(id: int, name: string) = action {
updateEntityAndSave(userRepo)(id)(_.updateName(name))
}
def updateEmail(id: int, email: string) = action {
updateEntityAndSave(userRepo)(id)(_.updateEmail(email))
}
}
private def updateEntityAndSave(repo: Repo[T])(id: int)(f: T => Option[T]): Result = {
repo.get(id) match {
case Some(entity) =>
f(entity) match {
case Some(updatedEntity) =>
repo.save(updatedEntity)
bus.emit(updatedEntity.events)
Ok()
None => BadRequest()
case None => NotFound()
}
None Not Null
Dubbed the billion-dollar mistake by Tony Hoare, nulls can be a source of great nuisance for programmers. Null Pointer Exceptions are all too often encountered and confusion reigns over whether null is a valid value or not. In some situations a null value may never happen and need not be worried about while in others it’s a perfectly valid value that has to be considered. In many OOP languages, there is no way of knowing if an object we get back from a method can be null or not without either jumping into said method or relying on documentation that often drifts or does not exist.
Functional languages avoid these problems simply by not having null (often Unit is used to represent a method that doesn’t return anything). In situations where we may want it, Scala provides the helpful Option type. This makes it explicit to the reader that you call this method, you will get back Some value or None. Even C# is now introducing features like Nullable Reference Types to help reduce the possible harm of null.
Scala also goes a step further, providing an Either
construct which can contain a success (Right
) of failure (Left
) value. These can be chained together to using a railway styles of programming. This chaining approach can lead us to have an easily readable description of what’s happening and push all our error handling to the end, rather than sprinkle it among the code.
Using Option can also improve readability when interacting with Java code. For example, a method returning null can be pumped through an Option and combined with a match statement to lead to a neater result.
Option(someNullReturningMethod()) match {
case Some(result) =>
// do something
case None =>
// method returned null. Bad method!
}
Conclusions
There are many reasons to prefer functional approaches to programming. Even in less functionally orientated languages I have found myself reaching for functional constructs to arrange my code. I think it’s something all software developers should try learning and applying in their code.
Naturally, not every developer enjoys FP, it is not a silver bullet that will overcome all our challenges. Even within 67 Bricks we have differing opinions on which parts of functional programming are useful and which are not (scala implicits can be quite divisive). It’s ultimately just another tool in our expansive toolkit that helps us to craft correct, readable, flexible and functioning software.