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.