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.
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.
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.
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.