Vaughan

A case for pattern matching

.NET Core

Over the years developers using more functional languages like F# have boasted about how much easier their code is to read and how imperative C# can be. NO LONGER! Well maybe not much longer.

We are starting to move in the right direction with pattern matching as a good replacement for switch statements. switch statements are very useful but the general syntax has been around for more than 50 years. Starting from C# 7.0, we could start using pattern matching as an alternative. It seems an active feature because each release has made it more powerful.

I want to show an example how the combination of pattern matching and value tuples can create very readable code.

Imagine your application has to calculate ticket prices for a zoo based on different rules for the day of the week and the age group of the person. This below is I think a reasonable way to implement it before pattern matching.

public enum Age { Child, Adult, Senior }

private Age GetAgeGroup(int age)
{
    if (age < 13) return Age.Child;
    if (age < 60) return Age.Adult;
    return Age.Senior;
}

/*
* Base price   - R100
* Children     - Half price
* Seniors      - Half price on Wednesdays
* Adults       - 25% off Weekends
*/
public decimal GetTicketPrice(int age, DateTime date)
{
    DayOfWeek dayOfWeek = date.DayOfWeek;
    Age ageGroup = GetAgeGroup(age);

    if(ageGroup == Age.Child)
    {
        return 50;
    }

    if(ageGroup == Age.Adult && 
       (dayOfWeek == DayOfWeek.Saturday ||
        dayOfWeek == DayOfWeek.Sunday))
    {
        return 75;
    }

    if (ageGroup == Age.Senior &&
        dayOfWeek == DayOfWeek.Wednesday)
    {
        return 75;
    }

    return 100;
}

Its not terrible but the GetTicketPrice has to walk through all the scenarios in quite a few lines. As the rules become more complex it may become easier to have subtle bugs. Below is the same method but its doing a few things at once. Firstly its creating a value tuple of (Age, DayOfWeek) and then its using pattern matching to match it to the correct option. The discard (_) option at the end is equivalent to the switch default case.

/*
* Base price   - R100
* Children     - Half price
* Seniors      - Half price on Wednesdays
* Adults       - 25% off Weekends
*/
public decimal GetTicketPrice(int age, DateTime date)
{
    return (GetAgeGroup(age), date.DayOfWeek) switch
    {
        ( Age.Child, DayOfWeek.Monday) => 50,
        ( Age.Child, DayOfWeek.Tuesday) => 50,
        ( Age.Child, DayOfWeek.Wednesday) => 50,
        ( Age.Child, DayOfWeek.Thursday) => 50,
        ( Age.Child, DayOfWeek.Friday) => 50,
        ( Age.Child, DayOfWeek.Saturday) => 50,
        ( Age.Child, DayOfWeek.Sunday) => 50,
        ( Age.Adult, DayOfWeek.Saturday) => 75,
        ( Age.Adult, DayOfWeek.Sunday) => 75,
        ( Age.Senior, DayOfWeek.Wednesday) => 50,
        _ => 100
    };
}

One problem with the above statement is that even though children are 50 for every day of the week, I added a pattern for each day. You don't actually need to do it. By using the discard character again, it tells the pattern that any value will match.


/*
* Base price   - R100
* Children     - Half price
* Seniors      - Half price on Wednesdays
* Adults       - 25% off Weekends
*/
public decimal GetTicketPrice(int age, DateTime date)
{
    return (GetAgeGroup(age), date.DayOfWeek) switch
    {
        ( Age.Child, _) => 50,
        ( Age.Adult, DayOfWeek.Saturday) => 75,
        ( Age.Adult, DayOfWeek.Sunday) => 75,
        ( Age.Senior, DayOfWeek.Wednesday) => 50,
        _ => 100
    };
}

You have to admit that this is much more readable compared to the original version. Whats great is that pattern matching is still getting new features at each new major C# release with Type, Relational and Logic patterns coming in .NET 5.

Related Posts

BMC logoBuy me a coffee