DEV Community

Cover image for You owe it to your team to let them try F#
Jakob Christensen
Jakob Christensen

Posted on

You owe it to your team to let them try F#

If you are the lead or senior of a team of developers that build applications in C#, this is for you.

Let me just throw it out there for you:

TL;DR

You owe it to your team to let them try F#.

Give them time to write a new project or just a new component in F#. They will thank you later. And so will your management.

This turned out to be a lengthy post, so here is a table of contents.

C# feels good, but...

If you are like me, you have been building applications in C# for years. C# makes you comfortable. C# makes you happy. And your team can build just about anything in C#.

But maybe, just maybe, you have a nagging feeling that some things didn't have to be so hard to understand, or requests for changes did not have to feel so overwhelming.

Maybe that feeling comes after the umpteenth time your team has chased down a null reference error reported by a user.

Maybe that feeling comes when the your team has spent way too much time adding a new feature, because it has some unexpected effect on the application.

Or maybe that feeling comes when there is a bottle neck on your team because the steep learning curve for your code base makes it really hard to bring in new hires.

As you have probably guessed by now, there is an easier way, and that way is F#. F# will give you:

  • Fewer bugs
  • Faster time to market
  • Faster onboarding of team members

I now this because we did it on our team. Below I will walk you through these bullets.

Fewer bugs

Applications without errors make happy users and if the users are happy, management and employees are happy.

After working with F# for about a month, one of my co-workers, who is very experienced in C#, said:

If F# compiles, it just works.

Of course, this is an exaggeration but there is a lot of truth in his statement. F# helps you build robust software with no surprising runtime errors. F# does that in a number of different ways, and I will walk you through two of those here:

  1. F# gives you a way to make invalid state impossible, and
  2. F# makes it easier for you to test your code.

Make invalid state unrepresentable (and no more null-reference errors)

Someone, and I'm not sure who, once coined the expression

Make invalid state unrepresentable.

It means that you should model your code in a way so that parameters and values can never take on a state that is not allowed in your program. One of the most dreaded errors in C# applications is the "object not set to a reference" error, which happens when you reference a null-object. That is, an object is null which is an invalid value for that case. For example, you might have some code that retrieves an object from storage:

// C#
var user = GetUserByEmail("alice@acme.com");
Console.WriteLine(user.Name);
Enter fullscreen mode Exit fullscreen mode

What you cannot tell from this short example is, what happens if there is no user for that email address? Does GetUserByEmail return null? If it does, your code fails. You could add a check for null but the signature for GetUserByEmail does not give you any information on what to do.

In F# it is to model that in a way so that it is clear what happens if the user does not exist. In particular you could use the Option type and the F# code would look like so:

// F#
match getUserByEmail "bob@acme.com" with
| Some user -> Console.WriteLine(user.Name)
| None -> Console.WriteLine("User not found.")
Enter fullscreen mode Exit fullscreen mode

Option can be either None or Some and the F# compiler will force you to handle both cases. This makes it very clear what getUserByEmail returns when there is no user and you won't accidentally reference a null-object.

You can model your own types in the same way as Option. While we are at it we might as well expand getUserByEmail so that we can identify users by other things than email, for example nick name. For that we can create or own type:

type UserIdentifier =
    | Email of string
    | Nick of string

let getUser identifier =
    match identifier with
    | Email email -> getUserByEmail email
    | Nick nick -> getUserByNick nick
Enter fullscreen mode Exit fullscreen mode

As with Option, the F# compiler will force you to match all cases of UserIdentifier and hence you won't forget any cases and also, the caller of getUser won't be able to pass in any invalid values.

You can do even better. Above, email is represented by a string. But maybe you want to make sure that you only pass a validated email. You could create a new type ValidatedEmail that is guaranteed to only contain validated emails. The UserIdentifier type would then look like so:

type UserIdentifier =
    | Email of ValidatedEmail
    | Nick of string
Enter fullscreen mode Exit fullscreen mode

I will leave the implementation of ValidatedEmail as an exercise for the reader 😏

Easier testing

We have been taught for years to write code that adheres to principles like the Open Closed principle and the principle of Inversion of Control. Personally, dependency injection has been my weapon of choice for so long, that I have lost sight of the why. And testing with mocks and stubs does not exactly lend itself to readable tests.

Functional programming has a different approach, since it is just... functions. Functions are all about inputting data and getting some other data back, which you will input into another function and so on.

You may visualize it like so with arrows:

let result = inputData -> function1 -> function2 -> function3
Enter fullscreen mode Exit fullscreen mode

You can think of it as data flowing through a pipeline of functions. F# actually has a built-in "pipe" operator |> that does exactly that. It takes the output of one function and "pipes" it into the next:

let result 
    = inputData 
    |> function1
    |> function2
    |> function3
Enter fullscreen mode Exit fullscreen mode

Yes, the above it valid F# πŸ˜ƒ

You can even create new functions by composing other functions. That lets you define workflows in your system as a pipeline composed of a series of functions. That may look like so:

let doSomeBusinessLogic =
    validate
    >>= calculateFoo
    >>= calculateBar

let pipeline =
    readInput
    >>= doSomeBusinessLogic
    >>= saveOutput
Enter fullscreen mode Exit fullscreen mode

Here, the >>= is just some special operator that I have defined. It composes two functions, hence doSomeBusinessLogic and pipeline are two functions composed from other functions.

You may want to read on one way to do that in one of my other entries here:

The beauty here is that it is so easy to test.

You can choose to write tests for validate, calculateFoo, and calculateBar separately, or you can choose to test the entire doSomeBusinessLogic function as a whole. You can even go all in and just test pipeline. It is all up to you.

Also, note that this model isolates your business logic from communication with external resources like web services or databases. The boundaries towards externals are at the top and at the bottom of your pipeline.

Faster time to market

My second point is that F# will help your team get products faster out the door. Besides the better testing part that I went through above, I'd like to mention 3 ways that F# helps in this:

  • Immutability
  • Less code
  • Better domain modelling

Let us go through these bullets:

Immutability

Values in F# are immutable by default, that is you cannot change a value once it has been initialised. For seasoned C# developers like you and I that may sound horrifying. How can you get stuff done if you cannot change anything? But if you think about it for at bit, immutability takes a massive burden off your shoulders because you know that you won't risk a massive rippling side effect in your application if you change a variable.

That makes your code much more trustworthy and you can fearlessly add new features and fix bugs.

Which gives you faster time to market...

Less code

In F#, every line has a purpose. It is very concise and succinct. Constructs like the pipe operator |> and type inference saves you a lot of typing.

Which gives you faster time to market...

Less code also makes your code easier to understand which I will get back to.

Better domain modelling

As C# developers, we have always been told to use class hierarchies to model our domain. But if you think about it, domains really don't behave as mutable objects with some internal state and public functions that operate on that state, are they? The usual OO example model of Animal and Monkey really show that this kind of modelling does not work 🐡

No, if you want to model a domain with software, it is usually about modelling information (data) passing through a workflow (functions).

Which is exactly what F# is very good at.

Which gives you faster time to market...

Faster onboarding of new employees

You finally made it to my third and last argument for switching to F#.

I believe that F# is easier to learn than C#. This is a bold statement, I know. The thing is, C# is actually really hard to learn. You need to learn about classes, public/protected/private/static, inheritance, constructors, interfaces, and more. When that is done, you need to learn OOD, which is a never ending story.

Of course, you can do all of this in F# as well, because it is built on .NET after all. But you don't have to. You can build every application in the world just by knowing data and functions.

I also claim that an F# codebase is easier to get to know for new hires. The reason is that it has fewer abstractions. You don't have to hunt down interface implementations that have been dependency injected via a .config file in a completely different assembly.

Granted, F# does has functional constructs, that are hard to understand, for example the >>= operator, I showed you above. But you don't have to use stuff like that. Actually, the F# community encourages you to use it sparingly and stick with functions, data and the |> thing.

F# is all about keeping it simple. That is good news for juniors and new hires.

Top comments (0)