Improving an Old Pattern Continued

The active patterns we’ve seen so far are enough to take some complex code and make it clean and readable. Let’s take the active patterns we know and improve the pattern matching code we created earlier.

Reviewing the Code

We were reviewing a chunk of code to determine whether a client requires an update based on the version numbers. Here is where we left off with the pattern match.

let checkForUpdate (rMaj, rMin, rBld) userVer = 
    match userVer with
    | (uMaj, _, _) 
        when rMaj > uMaj                                     
            -> Major
    | (uMaj, uMin, _) 
        when rMaj = uMaj && rMin > uMin                   
            -> Minor
    | (uMaj, uMin, uBld) 
        when rMaj = uMaj && rMin = uMin && rMin > uBld 
            -> Build
    | _     -> NoUpdate

The match is checking the major version, followed by the minor, and the build. The issue I have with this pattern match is we keep having to compare values we already know. As the match falls through the patterns we end up checking the major version three times. This isn’t very readable.

Adding a Partial Active Pattern

The first thing we can do is add a partial active pattern to match on all the cases that don’t require an update. We can leverage the fact that F# does structural equality by default that we can do this quite easily.

let (|NoUpdatePartial|_|) (relVer, userVer) =
    if relVer <= userVer then Some ()
    else None

With this partial pattern we've now eliminated the need to do multiple comparisons of any individual values.

Combining Active Patterns

We'll take the partial active pattern and use it in a multicase active pattern. Our multicase pattern needs only be concerned with matching the individual version values. This is providing we add the partial active pattern before the individual value patterns. It will look something like this:

let (|Major|Minor|Build|NoUpdate|) (relVer, userVer) =
    match (relVer, userVer) with
    | NoUpdatePartial                        -> NoUpdate
    | (rMaj,_,_),(uMaj,_,_) when rMaj > uMaj -> Major
    | (_,rMin,_),(_,uMin,_) when rMin > uMin -> Minor
    | _                                      -> Build

We now have a complete active pattern we can use when determining what kind of update is required.

let checkForUpdate relVer userVer =
    match relVer, userVer with
    | Major    -> doMajorUpdate ()
    | Minor    -> doMinorUpdate () 
    | Build    -> doBuildUpdate ()
    | NoUpdate -> doNoUpdate ()

Summary

We’ve greatly increased the expressiveness of our pattern matching by using Active Patterns. We’ve used a Partial Active Pattern and a Multi-Case Active Pattern. Active pattern can be combined together to create larger patterns all in the name of readability. All of this helps lead us to correct and expressive solutions.