“Well, this is interesting!” I silently uttered to myself as I was digging deeper into a topic that now, after the experience, I think anybody who’s serious about software development should learn. Yet, if it wasn’t for my interview at Mews, which incentivized me and inspired me to learn the topic, I probably wouldn’t learn about it for another year, two years, or maybe ever.
The topic was Functional Programming.
When I asked Mews (where the whole backend code is written in C#) about what I can do to better prepare myself for the possible job position, I wasn’t expecting “learn F#” to be the answer. Even though other resources were suggested as well, my curiosity got the best of me. I started learning F#, and I thoroughly enjoyed it!
C# and Functional Programming
In this article, I aim to share the ideas inspired by functional programming (FP for short) transferred into C#, utilizing the FuncSharp library and aiming to make C# more functional. I hope to inspire you to get into FP, check out FuncSharp, and hopefully get some ideas on how your codebase can be more solid and expressive.
Before I start though, let me bust some myths and misconceptions I’ve heard before that might discourage people from learning about FP.
- Object-Oriented Programming and FP do not go together. – There are ideas in FP that are very awkward in OOP languages (like immutability). But, in this article, you will see that there is some beneficial overlap.
- FP has a huge performance overhead. – While there definitely is a performance overhead it is not actually as big as you might think, and the advantages are well worth it.
- FP is hard. – I agree that the syntax at first glance can seem very foreign, but do not let that intimidate you. You need to take it from the basics. Take your time, and soon enough it will start to make sense. All you need is practice.
- Learning FP is useless because nobody uses it. – This one is somewhat true. However, as I stated earlier, there is still great value in learning FP, so don’t let yourself get discouraged by this. To add to that... who knows what the future might bring.
Imperative vs. Descriptive Code
Being able to tell the difference between imperative and descriptive code is going to aid you greatly in the pursuit of writing a more expressive and less error-prone code.
Here’s what an imperative code looks like (Notice how it resembles step-by-step instructions):
This is the same code, written in a descriptive fashion, using LINQ:
Notice how there is no clutter at all – no empty list initialization and no foreach or if statements. It’s also a lot easier to understand. That’s what descriptive code is all about.
For seasoned C# developers, using LINQ might seem obvious. But wait... there’s more!
Simply put, a statement is anything that does not return a value – like the if or while statement, void functions, etc. The opposite of a statement is an expression, which always has a return value.
Consider the following code:
A construct like this is very common, yet it has a huge problem. The very first line (initializing the variable to null) is nothing but clutter that has no actual impact on the functionality. Not only that, but it also leaves an opportunity to introduce a bug. Someone could come in later, rewrite the function, and mistakenly leave the variable null! Like I did in the example: Observe that when discount is not null, the case is covered, yet if the discount is null, the price variable remains unassigned.
We can’t really work around that in the bare C# language unless we swap the null initialization for an expression and avoid using the if statement. That’s where the FuncSharp comes in:
There is one crucial thing to notice here. I got rid of the first if/else statements, and I used the Match extension method from FuncSharp instead. The method’s purpose is to imitate pattern matching, a common feature of FP. Its arguments are pairs of enum values and functions to be called based on the enum value. If the user’s membership is Premium, the first function is called, and if it’s Basic, the second function is called.
Also, notice how I had to add another else statement after the if (discount != null) line. I cannot be missing a return statement. The compiler does not let the price variable have a null value.
So now there is no unnecessary initialization to null cluttering the code. It’s also more solid since we prevented a null value. We are good to go!
Or are we?
A Billion-Dollar Mistake
As it turns out, the inventor of the null reference, Tony Hoare, calls it a billion-dollar mistake. It’s absolutely true. Read about it here.
Nulls should be generally avoided. They tend to poison the call stack, making them hard to track down.
Take a look again at the very last example. It correctly handles the case where the GetDiscount method returns null. But, if another developer decides to use the method, there is nothing to hint to him that the return value can actually be null. The method signature is not very helpful in this regard:
It promises that when it’s supplied the instance of Product,it will return a Discount, but that’s not true. Of course, the developer could look into the code. But what if the implementation is too convoluted, and he misses it?
IOption<T> to the Rescue!
Option is essentially a wrapper that can either be empty or non-empty. It’s something like the .NET’s Nullable type, only with some really neat features to help avoid statements. Therefore, Option gravitates more to descriptive code. It’s actually built-in type in F# because of how useful it is. It’s also called Maybe in other functional languages.
Let’s fix the dishonest and unpredictive method signature from the last example:
Much better! Now it says, “I may return a Discount”, forcing the developer to handle cases where the discount is missing. It becomes predictable, honest, and easy to understand . Implementing that into Figure C, it looks like this:
Do you find this more expressive? Or do you find it harder to understand? Make sure to let me know in the comments!
I used the Match function again but this time with the Option<Discount> class, passing two functions to it again. First to be called, if the Option is non-empty – the value inside the Option will be passed to the supplied method in that case. The other function is called when the Option is empty. This forces me to handle both cases, making the codebase once again more solid.
I am still not in the clear though. What if the product.PremiumPrice is null? What if the product.Price is null? The beauty of it is that as long as it’s not of type Option<Price>, I can safely assume that the property will be set. This is because as long as the Option is used, there is no need to introduce null values anywhere in the codebase.
Where’s the Catch?
With the last example (Figure G), I achieved a more solid code that is also more readable and expressive. If you don’t agree with me about the latter, I would make the argument that once you get used to this style of programming or learn the basics of FP, this will feel more natural.
It’s not without flaws though, and because I like to see both sides of the coin, here are some disadvantages I found:
- It becomes challenging to keep the length of lines under control.
- With the abundance of anonymous functions, it’s sometimes hard to understand compile errors.
- Stepping through the code gets a bit trickier, as there are often many expressions on a single line.
Wrapping it Up
I am grateful that Mews has incentivized me to be a better developer by learning FP. I am proof that if you actually sit down and begin to dig into FP, it’s not hard at all!
Summing up the article, here are the key points:
- Learning FP matters.
- Functional principles can make your code more solid and more expressive.
- Functional principles can be utilized in an OOP language like C#.
If this topic has caught your interest, make sure to check out these additional resources:
Do you have any thoughts on this article? Make sure to let me know in the comments below!
For more engineering insights shared by Mews tech team: