United Kingdom: +44 (0)208 088 8978

Using Active Patterns to help model business cases

In my previous post on refactoring and FsCheck, I left the code in a "broken" state. We refactored code from a set of if / then expressions into a pattern match, but accidentally changed the behaviour in the process by changing the order in which the checks were evaluated.

// Original code
let rateCustomer customer =
    if customer.Balance < 0 then Bad
    elif customer.Purchases = 0 then Ok
    else Good

// Refactored code with a bug
let newRateCustomer customer =
    match customer with
    | { Purchases = 0 } -> Ok
    | _ when customer.Balance < 0 -> Bad
    | _ -> Good

The good thing here is that we used FSCheck to prove that the two functions no longer had the same behaviour. However, it's even better if we can use the type system to fix this issue "at source" (literally!): in this case, Active Patterns provide a nice stepping stone to allow us to fix this code by making the ordering of the pattern irrelevant.

Moving from implicit to explicit business rules

In reality, the logic implemented in the first code sample could be more explicit:

  • If the customer has a negative balance, regardless of the number of purchases, rate them Bad.
  • If the customer has a positive balance and has made no purchases, rate them Ok.
  • If the customer has a positive balance and made some purchases, rate them Good.

We can illustrate this in a "truth table":

Balance Purchases Rating
Negative None Bad
Negative Some Bad
Positive None Ok
Positive Some Good

Notice that in the table above, for completeness I've actually "denormalised" the Negative Balance case into both explicit cases (None and Some purchases).

Moving to this type of approach - where we explicitly map out all cases - means that ordering doesn't matter any more.

Using Active Patterns to quickly model truth tables

In F#, we can use Active Patterns in conjunction with pattern matching to help build up this truth table in code:

let (|NoPurchases|SomePurchases|) customer =
    match customer with
    | { Purchases = 0 } -> NoPurchases
    | _ -> SomePurchases
let (|NegativeBalance|NonNegativeBalance|) customer =
    match customer with
    | _ when x.Balance < 0 -> NegativeBalance
    | _ -> NonNegativeBalance

Armed with these two patterns, we can combine them together in a pattern match, without the need to explicitly create a new type or to map Customer into another shape:

let superRateCustomer customer =
    match customer with
    | NonNegativeBalance & SomePurchases -> Good
    | NonNegativeBalance & NoPurchases -> Ok
    | NegativeBalance & NoPurchases -> Bad
    | NegativeBalance & SomePurchases -> Bad

Notice that I've deliberately re-ordered the pattern matches compared to the original code sample - but it no longer matters, since every case is much more explicit about what is being matched on. Running FSCheck now will also prove that the logic is consistent with the original function:

Check.Quick (areBothFunctionsTheSame rateCustomer superRateCustomer)
Ok, passed 100 tests.

Note that we could simplify the new pattern matching (if we really wanted to) by either combining the two "Bad" cases without sacrificing the explicit nature of the checks using the | (or) operator:

let superRateCustomer customer =
    match customer with
    | NonNegativeBalance & SomePurchases -> Good
    | NonNegativeBalance & NoPurchases -> Ok
    | NegativeBalance & (NoPurchases | SomePurchases) -> Bad

Alternatively, we could entirely replace the condition with _ (wildcard).

Summary

Truth tables are a useful way to model all combinations of different dimensions for a given business rules, and pattern matching combined with active patterns in F# provide an excellent way to represent these directly in code.