How To Apply Functional Programming In C#

Milan Jovanović - Sep 17 '23 - - Dev Community

Although C# is an object-oriented programming language, it received many new functional features in recent versions.

To mention just a few of these features:

  • Pattern matching
  • Switch expressions
  • Records

You're probably already doing functional programming without even knowing it.

Do you use LINQ? If you do, then you're doing functional programming. Because LINQ is a functional .NET library.

In today's issue, I will show you how to refactor some imperative code with functional programming.

Benefits Of Functional Programming

Before we take a look at some code, let's see what are the benefits of using functional programming :

  • Emphasis on immutability
  • Emphasis on function purity
  • Code is easier to reason about
  • Less prone to bugs and errors
  • Ability to compose functions and create higher-order functions
  • Easier to test and debug

From my experience, I think functional programming is more enjoyable once you get used to it. Starting out, it feels strange because your old object-oriented programming habits will kick in. But after a while, functional programming feels easier to work with than imperative code.

Starting With Imperative Code

Imperative programming is the most basic programming approach. We describe a step-by-step process to execute a program. It's easier for beginners to reason with imperative code by following along with the steps in the process.

Here's an example of an EmailValidator class written with imperative code:

public class EmailValidator
{
    private const int MaxLength = 255;

    public (bool IsValid, string? Error) Validate(string email)
    {
        if (string.IsNullOrEmpty(email))
        {
            return (false, "Email is empty");
        }

        if (email.Length > MaxLength)
        {
            return (false, "Email is too long");
        }

        if (email.Split('@').Length != 2)
        {
            return (false, "Email format is invalid");
        }

        if (Uri.CheckHostName(email.Split('@')[1]) == UriHostNameType.Unknown)
        {
            return (false, "Email domain is invalid");
        }

        return (true, null);
    }
}
Enter fullscreen mode Exit fullscreen mode

You can clearly see the distinct steps:

  • Check if email is null or empty
  • Check if email is not too long
  • Check if email format is valid
  • Check if email domain is valid

Let's see how we can refactor this using functional programming.

Applying Functional Programming

The basic building block in functional programming is - a function. And programs are written by composing function calls. There are a few other things you need to keep in mind, like keeping your functions pure. A function is pure if it always returns the same output for the same input.

We can capture each step from the imperative version of EmailValidator with a Func delegate. To also capture the respective error message together with the validation check, we can use a tuple. And since we know all of our validation steps, we can create an array of Func delegates to store all of the individual functions.

public class EmailValidator
{
    const int MaxLength = 255;

    static readonly Func<string, (bool IsValid, string? Error)>[] _validations =
    {
        email => (!string.IsNullOrEmpty(email), "Email is empty"),
        email => (email.Length <= MaxLength, "Email is too long"),
        email => (email.Split('@').Length == 2, "Email format is invalid"),
        email => (
            Uri.CheckHostName(email.Split('@')[1]) != UriHostNameType.Unknown,
            "Email domain is invalid")
    };

    static readonly (bool IsValid, string? Error) _successResult = (true, null);

    public (bool IsValid, string? Error) Validate(string email)
    {
        var validationResult = _validations
            .Select(func => func(email))
            .FirstOrDefault(func => !func.IsValid);

        return validationResult is { IsValid: false, Error: { Length: >0 } } ?
            validationResult : _successResult;
    }
}
Enter fullscreen mode Exit fullscreen mode

Notice that this allows us to do all sorts of interesting things with the _validations array. How hard would it be to modify this function to return all of the errors instead of just the first one?

If you're thinking we can use LINQ's Select method somehow, you're thinking in the right direction.

Further Reading

We only scratched the surface of what functional programming is, and what you can do with it. If you want to learn more, here are some learning materials:

Thank you for reading, and have a wonderful Saturday.


P.S. Whenever you're ready, there are 2 ways I can help you:

  1. Pragmatic Clean Architecture: This comprehensive course will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture. Join 950+ students here.

  2. Patreon Community: Think like a senior software engineer with access to the source code I use in my YouTube videos and exclusive discounts for my courses. Join 820+ engineers here.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .