Kai Ito
Power of Active Patterns

Power of Active Patterns

I’ve been working on a dotnet core global tool. One of the things that such a tool has, is many command line options. I found Argu to parse these command line options. However I ran into the problem that I was left with a very large amount of if/else statements. Perhaps I don’t know what the “proper” way is of handling this, but this was my naive approach.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
23: 
24: 
25: 
26: 
27: 
28: 
29: 
30: 
31: 
32: 
33: 
34: 
35: 
36: 
37: 
let hasInfo = arguments.Contains Info
let hasPostalCode = arguments.Contains PostalCode
let hasUpdate = arguments.Contains Update
let hasList = arguments.Contains List
let hasClearDatabase = arguments.Contains ClearDatabase

if hasInfo
then printfn "info"
elif hasPostalCode && hasUpdate
then printfn "%s" <| parser.PrintUsage()
elif hasPostalCode && not hasUpdate
then getPostalCodeInformation <| arguments.GetResult PostalCode
elif hasList
then
    match arguments.GetResult List with
    | None -> printfn "%s" <| parser.PrintUsage()
    | Some list ->
        match list with
        | Supported -> ConsolePrinter.printCountries DataDownload.supportedCountries
        | Available ->
            Database.ensureDatabase()
            match Query.getAvailableCountries () with
            | Ok countries ->
                match countries with
            | None -> printfn "No countries have been updated yet"
            | Some c -> ConsolePrinter.printCountries c
            | Error e ->
                ConsolePrinter.printErrorMessage e ErrorLog.writeException
elif hasClearDatabase
then
    Database.clearDatabase()
elif not hasPostalCode && hasUpdate
then
    match arguments.GetResult Update with
    | None -> printfn "%s" <| parser.PrintUsage()
    | Some countryCode ->
        updateCountry countryCode

The entire time I was thinking that there must be a better way. After mulling over it awhile, I suddenly remembered Active Patterns. What’s interesting to note, is that I had never used Active Patterns before, but I still somehow knew that this was finally a perfect use case for these. This allowed me to decouple the command line option, from its functionality into easily maintained blocks.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
let (|ShowInformation|HasPostalCode|UpdateCountry|ListAvailable|ListSupported|              HasClearDatabase|HelpRequested|)
    (input: ParseResults<Arguments>) =
    if input.Contains Info then ShowInformation
    elif input.Contains PostalCode then HasPostalCode
    elif input.Contains Update then UpdateCountry
    elif input.Contains List
    then
        let listRequest = input.GetResult List
        let isAvailableRequest = listRequest |> Option.exists (fun l -> l = Available)
        let isSupportedRequest = listRequest |> Option.exists (fun l -> l = Supported)
    if not isAvailableRequest && not isSupportedRequest
    then HelpRequested
    elif isAvailableRequest
    then ListAvailable
    else ListSupported
    elif input.Contains ClearDatabase then HasClearDatabase
    else HelpRequested

Here we have the definition of the Active Pattern. Each command line option is is own possible pattern, then we have a much more manageable and readable block that returns the selected options. Now, we only need to define what should happen with these options.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
match arguments with
| ShowInformation -> printfn "info"
| HasPostalCode -> getPostalCodeInformation <| arguments.GetResult PostalCode
| UpdateCountry ->
    match arguments.GetResult Update with
    | None -> printfn "%s" <| parser.PrintUsage()
    | Some countryCode ->
        updateCountry countryCode
| ListAvailable ->
    Database.ensureDatabase()
    match Query.getAvailableCountries () with
    | Ok countries ->
        match countries with
        | None -> printfn "No countries have been updated yet"
        | Some c -> ConsolePrinter.printCountries c
    | Error e ->
        ConsolePrinter.printErrorMessage e ErrorLog.writeException
| ListSupported -> ConsolePrinter.printCountries DataDownload.supportedCountries
| HasClearDatabase -> Database.clearDatabase()
| _ -> printfn "%s" <| parser.PrintUsage()

Much better. This is a lot more clear to read and understand which command line options map to what functionality.

val hasInfo : 'a
val hasPostalCode : 'a
val hasUpdate : 'a
val hasList : 'a
val hasClearDatabase : 'a
val input : 'a
active recognizer ShowInformation: 'a -> Microsoft.FSharp.Core.Choice<'b,'c,'d,'e,'f,'g,'h>
active recognizer HasPostalCode: 'a -> Microsoft.FSharp.Core.Choice<'b,'c,'d,'e,'f,'g,'h>
active recognizer UpdateCountry: 'a -> Microsoft.FSharp.Core.Choice<'b,'c,'d,'e,'f,'g,'h>
val None : 'a
active recognizer ListAvailable: 'a -> Microsoft.FSharp.Core.Choice<'b,'c,'d,'e,'f,'g,'h>
active recognizer ListSupported: 'a -> Microsoft.FSharp.Core.Choice<'b,'c,'d,'e,'f,'g,'h>
active recognizer HasClearDatabase: 'a -> Microsoft.FSharp.Core.Choice<'b,'c,'d,'e,'f,'g,'h>