I was recently a guest on the .NET Rocks podcast and one of the things that came up was this notion in F# of “if it compiles – it works”. Given the constraints of time on the show (and technical difficulties with sound on my end) I wanted to give a fuller take on why I think this is so, as I can imagine to people not used to F# (or Functional Programming) it may sound like Snake Oil.
In F# classes (and records) are non-nullable by default. This means that if you stick with the happy path you cannot get a null reference exception (in your own code). If you really need to represent a state of Something or Nothing then the Option type is a built in type that allows you to access the values safely via pattern matching or the Option module. Don Syme posted a very entertaining post about this close to a year ago (I wonder if Don should call it his billion dollar saving?).
Of course you can still have nullable classes in F#- but you have to explicitly state this with the AllowNullLiteral attribute – a clear indication that something may go wrong later.
But even Bret “The Hitman” Hart knows this is silly!!
And for the record – “?.” in C# 6 is a lame attempt to solve null in C#.
In F# values are immutable by default. This means that state is consistent throughout the application. Imagine passing a list into a void method which add a value to the end. In F# (using the build in List type – which is of course immutable) the append would only affect the local copy of the list in the method – it would have no effect. To capture the change in F# you would have to change the void method (or unit in F#) to return the new list.
Immutability is often discussed as a great way to avoid data races in multithreaded code, but even in more trivial cases it is incredibly useful to know that the state of an object cannot change midway through a method. In C# you may not think twice about a series of void return methods which mutate properties/fields of a class, but this means the state of a class is constantly in flux which may make it hard to reason about it’s state at any given time. This often leads to bugs (especially if you can set references to NULL!).
F# is not a “pure” functional language – hence mutation is allowed, but only if you specify it. This means that by looking through your code for the mutable keyword or the <- operator you can identify potentially problematic areas of your code without even running it.
Return values must be explicitly ignored
Because by default a function has no side-effect, to ignore its result would have been effectively a no-op. The complier therefore forces you to explicitly ignore the return value and this removes an entire class of errors. If you are ignoring lots of results, it’s probably a code smell that you are mutating state in functions – the clue that you are doing something fishy is explicit in the code.
One of the things I talked about on the show was Functional SOLID. SOLID is a system which “intend(s) to make it more likely that a programmer will create a system that is easy to maintain and extend over time” (Wikipedia). For an OO programmer it is important to have rules about what to do to create clean extensible software because the defaults lead you to the opposite.
Breaking down how SOLID might apply to a functional language:
Single Responsibility – a function (by default) has no side-effect. Therefore values in => value out. A function therefore by default can only have a single responsibility.
Open-Close – a function can be extended (but not modified) by using higher order functions.
Liskov Substitution (design by contract) - a function signature could be said to be a stable contract and using the default single responsibility of a function, you are more likely (but not guaranteed) to get the same behaviour, provided the contract is clearly defined. Use higher order functions in place of classes/interfaces.
Interface Segregation – what could be a smaller interface than a function? Use higher order functions in place of interfaces.
Dependency Inversion – what easier way to invert a dependancy than higher order functions?
When you read the above list, it boils down to 2 things – use functions and higher order functions. These are the defaults of functional programming (as opposed to rules imposed by programmers), hence there is no need for SOLID for functional languages.
I made up the term “Functional SOLID” simply to delete it. Consider it my Randall Stevens.
Boolean traps exist in all kinds of code – a simple true/false which should be one of the simplest things to reason about but often isn’t. But things can get even more complex with numbers – what if the int that your function takes is supposed to represent quantity of apples and not seconds since the epoch.
Units of measure help make sure your float of Kg doesn’t end up being used as a float of metres.
But taking things at a more basic level, F# doesn’t even allow implicit conversion between number types – a relatively experienced C# programmer may guess correctly what type x is – and the pitfalls of the automatic conversion:
var x = 1.3 * 2;
but the F# programmer has the compiler watch his/her back:
let x = 1.3 * 2; error FS0001: The type 'int' does not match the type 'float'
This may seem like paranoia, but as there is no clear default, the compiler default is to make you choose. Which seems fair enough to me.
It’s easy to become institutionalised in a world of OO, where null is the norm, mutation is everywhere, and large unwieldy classes reign supreme. It doesn’t have to be that way.
Hopefully I’ve helped illustrate some of the ways that F# (and other functional languages) can help reduce common errors in code. As you can see from every example, it’s the default behaviour that gets you to the happy place. You can deviate should you want/need to, but that’s an explicit choice to opt-out of safety, whereas the default for OO languages is to have no safety by default.