Thank you to our sponsors who keep this newsletter free to the reader:
Today's issue is sponsored by Localizely, the translation management platform that helps you translate texts in your app for targeting multilingual markets.
And by Hasura, an open-source engine that gives instant GraphQL and REST APIs on new or existing SQL Server, enabling teams to ship apps faster.
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.
Let's dive in.
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);
}
}
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;
}
}
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:
- Functional Programming in C#, by Enrico Buonanno
- Functional Programming With C# Using Railway-Oriented Programming
- How Function Composition Can Make Your Code Better
- Make Your ASP.NET Core API Controllers Incredibly Simple With Functional Programming
Thank you for reading, and have a wonderful Saturday.