Ifp.PatternMatching is a library that brings functional pattern matching to C#.
This library is made by Bob Nystrom and was originally published 2009 in this article. The code was pasted to bitbucket bitbucket.org/munificent/pattern_matching. Based on that work a portable class library was created, tests were added, nuget packages created and finally brought to github.
The library can be used to build business rules that inspect a type hierarchy and apply rules on types that meet some criteria:
//sub-type matching with conditions on the sub-type
var specialFrontendOffers = Pattern.Match<ShoppingCart, decimal>(shoppingCart).
Case<AppShoppingCart>(appShopingCart => appShopingCart.IsFirstRunExperience, 0.05m). //5% off for the first time order with the app
Case<WebShoppingCart>(webShopingCart => webShopingCart.PromoCode == "WebSpecial", 0.04m). //4% off for the newsletter promotion code (only supported by the web interface)
Default(0.0m).
Result;
//matching on a condition and nested pattern matching
var cartDiscounts = Pattern.Match<ShoppingCart, decimal>(shoppingCart).
Case(cart => cart.OrderValue > 100, cart => Pattern.Match<Customer, decimal>(cart.Customer). // if the order value is bigger than 100 the discount depends on the customer status
Case<ClubMember>(0.1m). // Club member always get 10%
Case<StandardCustomer>(standardCustomer => !standardCustomer.HasOutstandingDebts, 0.05m). // standardCustomers get 5% if there are no outstanding debts
Default(0.0m).
Result).
Case(cart => cart.OrderValue > 50, 0.02m). // between 50 and 100, the discount is 2% without further conditions
Default(0.0m).
Result;
//after the first match all the other matches are ignored
var shipping = Pattern.Match<Address, decimal>(shoppingCart.ShippingAddress).
Case(address => address.ShippingDistance > 1000, 7m).
Case(address => address.ShippingDistance > 500, 5m).
Case(address => address.ShippingDistance > 50, 3m).
Default(2m);
var overallPrice = shoppingCart.OrderValue +
(shoppingCart.OrderValue * specialFrontendOffers) +
(shoppingCart.OrderValue * cartDiscounts) +
shipping;
//Start check out process
Pattern.Match(shoppingCart.PayMethod).
Case<CreditCard>(creditCard => CheckoutPerCreditcard(shoppingCart, overallPrice)).
Case<AdvancePayment>(advancePayment => CheckoutPerAdvancePayment(shoppingCart, overallPrice)).
Default(() => { throw new NotSupportedException(); });The Pattern.Match can be used either as expression:
// map US grades to German grades (schulnote)
var grade = "A";
var schulnote = Pattern.Match<string, int>(grade).
Case("A", 1).
Case("B", 2).
Case("C", 3).
Case("D", 4).
Case("E", 5).
Result;or as a statement:
// let an animal make a noise
Animal animal = new Chicken(Gender.Male);
Pattern.Match(animal).
Case<Dog>(d => d.Bark()).
Case<Chicken>(c => c.Gender == Gender.Male, c => c.Cockadoodledoo());If used as an expression the pattern matching looks like this:
//start the pattern-match by specifying the source and target type and passing an object of the source-type.
var objOfTargetType=Pattern.Match<TSourceType, TTargetType>(objOfSourceType).
Case(...). //specify cases (see below)
Case(...).
Default(...). //specify a default value
Result; //ask for the result. Throws exception if there is no matchCase consist of three parts Case<Type parameter>(Predicate, Return value);
- Optional Type parameter. The type of value to match.
TCasemust be a sub-type of theTSourceType - Optional Predicate The predicate to evaluate to test the match. The predicate can be either
- A concrete value or
- A predicate function of type
Func<TCase, bool>orFunc<bool>
- The Return value. Can be
- Either a concrete value of type
TTargetTypeor - A function that produces a
TTargetType. This can either be aFunc<TCase, TTargetType>or aFunc<TTargetType>.
- Either a concrete value of type
Result is optional because the ReturnMatcher can implicit be converted to the TargetType:
int number = Pattern.Match<string, int>("III").
Case("I", 1).
Case("II", 2).
Case("III", 3). // 'Case' returns a ReturnMatcher that is implicit converted to an int.
Case("IV", 4).
Case("V", 5);Match any given animal to one special ability by applying this rules:
- If the
animalis of typeDogand is asearch and rescue dogthen the special ability is scenting. - If the
animalis of typechickenand is male then the special ability is crowing. - Otherwise it doesn't have a special ability.
var specialAbility = Pattern.Match<Animal, SpecialAbility>(animal).
Case<Dog>(d => d.IsSearchAndRescueDog, SpecialAbility.Scenting).
Case<Chicken>(c => c.Gender == Gender.Male, SpecialAbility.Crow).
Default(SpecialAbility.None).
Result;By using C#6 features this example can be used like this:
using static PatternMatching.Pattern;
namespace AnimalRules
{
public class AnimalFacts
{
public SpecialAbility GetSpecialAbilityOf(Animal animal) =>
Match<Animal, SpecialAbility>(animal).
Case<Dog>(d => d.IsSearchAndRescueDog, SpecialAbility.Scenting).
Case<Chicken>(c => c.Gender == Gender.Male, SpecialAbility.Crow).
Default(SpecialAbility.None).
Result;
}
}Calculate the discount of a shopping cart by applying this rules:
- If the customer is
ClubMemberthe discount is 5% of the carts order value. - If the customer is
FirstTimeCustomerthe discount is 4% of the carts order value - If the customer is
StandardCustomerthe discount is 2% of the carts order value - Otherwise there is no discount.
var discount = Pattern.Match<ShoppingCart, decimal>(shoppingCart).
Case(cart => cart.Customer is ClubMember, cart => cart.OrderValue * 0.05m).
Case(cart => cart.Customer is FirstTimeCustomer, cart => cart.OrderValue * 0.04m).
Case(cart => cart.Customer is StandardCustomer, cart => cart.OrderValue * 0.02m).
Default(0.0m).
Result;In the following example the type FirstTimeCustomer isn't in the case list.
Accessing the Result property raises a NoMatchException.
var customer = new FirstTimeCustomer();
var discount = Pattern.Match<Customer, decimal>(customer).
Case<ClubMember>(0.05m).
Case<StandardCustomer>(0.02m).
Result;If used as a statement the pattern matching looks like this:
//start the pattern-match by passing an object of the source-type.
Pattern.Match(objOfSourceType).
Case(...). //specify cases (see below)
Case(...).
Default(...); //specify a default actionCase consist of three parts Case<Type parameter>(Predicate, Action);
- Optional Type parameter. The type of value to match.
TCasemust be a sub-type of theTSourceType - Optional Predicate The predicate to evaluate to test the match. The predicate can be either
- A concrete value or
- A predicate function of type
Func<TCase, bool>orFunc<bool>
- The Action. This can either be an
Action<TCase>or anAction.
Let the animal make a noise.
var animal = new Dog(Gender.Male);
Pattern.Match<Animal>(animal).
Case<Chicken>(c => c.Gender == Gender.Male, c => c.Cockadoodledoo()).
Case<Dog>(d => d.Bark());The type parameter when calling the Match method is usually not needed:
Animal animal = new Dog(Gender.Male);
Pattern.Match(animal). //Type 'Animal' correctly inferred
Case<Chicken>(c => c.Gender == Gender.Male, c => c.Cockadoodledoo()).
Case<Dog>(d => d.Bark());The library supports the extraction of properties during a match to allow decomposition:
Pattern.Match(animal).
Case<Dog, Furs>(fur => WashMe(fur)). // Dog has a property of type Furs that is extracted from the dog instance.
Case<Chicken, Featherings>(feathering => MakeUnableToFly(feathering));To support decomposition the source object needs to implement the IMatchable interface:
public class Dog : Animal, IMatchable<Furs> //Implement IMatchable
{
public Furs Fur { get; } // The property that is enabled for decomposition in pattern matching.
Furs IMatchable<Furs>.GetArg() => this.Fur; //Explicit interface implementation.
}It is possible to support up to four decomposable object properties.
The library can be installed via nuget: https://www.nuget.org/packages/Ifp.PatternMatching/
Install via Package Manager Console
PS> Install-Package Ifp.PatternMatching