Adapting Match in C#: A Functional Programming Pattern
Origins
Pattern matching, commonly known as Match
in functional programming, originates from functional programming languages like ML, Haskell, and F#. The concept is rooted in mathematical pattern matching and algebraic data types (ADTs). In these languages, pattern matching serves as a more powerful and expressive alternative to traditional imperative conditional statements.
The pattern matching concept was first introduced in the ISWIM programming language by Peter J. Landin in 1966 and was later popularized by ML and its descendants. While C# has gradually incorporated more functional programming features, including some pattern matching capabilities since C# 7.0, implementing a full Match
operation gives us additional power and expressiveness that's closer to pure functional programming languages.
public class Match<T>
{
private readonly T _value;
private Match(T value)
{
_value = value;
}
public static Match<T> Create(T value) => new Match<T>(value);
public TResult With<TResult>(
Func<T, bool> predicate,
Func<T, TResult> result,
Func<T, TResult> otherwise)
{
return predicate(_value) ? result(_value) : otherwise(_value);
}
public TResult With<TResult>(
Dictionary<Func<T, bool>, Func<T, TResult>> matches,
Func<T, TResult> otherwise)
{
foreach (var match in matches)
{
if (match.Key(_value))
{
return match.Value(_value);
}
}
return otherwise(_value);
}
}
This implementation provides two main matching approaches:
A simple predicate-based match with a single condition
A dictionary-based match that can handle multiple conditions
Basic Usage Example
Let's look at how to use this Match implementation in practice:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
public class Program
{
public static void Main()
{
var person = new Person { Name = "John", Age = 25 };
// Simple match
var result = Match<Person>
.Create(person)
.With(
p => p.Age >= 18,
p => $"{p.Name} is an adult",
p => $"{p.Name} is a minor"
);
// Multiple conditions match
var detailedResult = Match<Person>
.Create(person)
.With(
new Dictionary<Func<Person, bool>, Func<Person, string>>
{
{ p => p.Age < 13, p => $"{p.Name} is a child" },
{ p => p.Age < 20, p => $"{p.Name} is a teenager" },
{ p => p.Age < 30, p => $"{p.Name} is a young adult" }
},
p => $"{p.Name} is an adult"
);
}
}
This pattern brings several benefits:
Declarative Code: The matching syntax makes the code's intent clearer
Type Safety: The pattern ensures type consistency across all match cases
Exhaustiveness: The otherwise case ensures all possibilities are handled
Composability: Matches can be easily combined and nested
Advanced Usage with Option Type
The Match pattern becomes even more powerful when combined with the Option/Maybe type:
public class Option<T>
{
private readonly T _value;
private readonly bool _hasValue;
private Option(T value, bool hasValue)
{
_value = value;
_hasValue = hasValue;
}
public static Option<T> Some(T value) => new Option<T>(value, true);
public static Option<T> None() => new Option<T>(default, false);
public TResult Match<TResult>(
Func<T, TResult> some,
Func<TResult> none)
{
return _hasValue ? some(_value) : none();
}
}
// Usage example
var optionalPerson = Option<Person>.Some(new Person { Name = "John", Age = 25 });
var greeting = optionalPerson.Match(
some: person => $"Hello, {person.Name}!",
none: () => "Hello, guest!"
);
Conclusion
Implementing Match in C# brings several functional programming benefits to your codebase:
Better Error Handling: Combined with Option types, Match provides a robust way to handle null cases and errors
More Readable Code: Pattern matching makes complex conditional logic more readable and maintainable
Type Safety: The pattern enforces type safety at compile time
Functional Style: It enables a more functional programming style in C#, leading to more declarative and less imperative code
While C# continues to evolve and incorporate more functional programming features natively, implementing custom Match operations can provide additional power and flexibility in your code. This pattern is particularly useful when working with domain models, handling complex business logic, or dealing with data transformations.
The key is to use Match where it makes sense - typically in scenarios where you have complex branching logic or when working with Optional types. Like any pattern, it's important to balance its use with readability and team familiarity with functional programming concepts.
Thank you for reading this article. See you next time.
Subscribe to my newsletter
Read articles from Rick directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Rick
Rick
15+ years of experience having fun building apps with .NET I began my professional career in 2006, using Microsoft technologies where C# and Windows Forms and WPF were the first technologies I started working with during that time. I had the opportunity to actively participate in the Windows ecosystem as an MVP and Windows 8/Windows Phone application developer from 2013-2018. Throughout my career, I have used Azure as my default cloud platform and have primarily worked with technologies like ASP.NET Core for multiple companies globally across the US, UK, Korea, Japan, and Latin America. I have extensive experience with frameworks such as: ASP.NET Core Microsoft Orleans WPF UWP React with TypeScript Reactive Extensions Blazor I am an entrepreneur, speaker, and love traveling the world. I created this blog to share my experience with new generations and to publish all the technical resources that I had been writing privately, now made public as a contribution to enrich the ecosystem in which I have developed my career.