ca9163d9
ca9163d9

Reputation: 29159

F# optional pattern matching

Right now I have the following pattern match

let argList = args |> List.ofSeq
match argList with
| "aaa" :: [] -> run "aaa"
| "bbb" :: DateTimeExact "yyyyMMdd" date :: [] -> run "bbb" date
....

It's used to parse command line parameters like

exec aaa

exec bbb 20141101

Now I want to be able to add option -o (optional). Such as exec bbb 20141101 -o. How to modify the pattern to do it? Even better, -o should be able to be place in any position.

Upvotes: 2

Views: 239

Answers (1)

Jono Job
Jono Job

Reputation: 3038

If you're looking for an approach that can be easily extended, I find that using the F# type system can really help when interpreting command line arguments.

Ultimately, you want to end up with the details of what was provided in the argList. So first, define the type(s) that you want. You're then parsing the list of strings into those type(s). In your example, it appears that you can either have "aaa", or "bbb" that is immediately followed by a date. That feels like a Union type:

type Cmd = 
    | Aaa 
    | Bbb of DateTime

Also, there may be an extra flag passed "-o" that can appear at any point (except between bbb and its date). So that feels like a Record type:

type Args = { Cmd: Cmd; OSet: bool; }

So now we know that we want to turn the argList collection into an instance of Args.

You're wanting to go through each of the items in the argList and handle them. One way to do this is with recursion. You want to match on each item (or pair of items, or triple, etc) before continuing to check the rest of the list. At each match, you update* the Args to include the new details.

(* Because Args is immutable, you don't actually update it. A new instance is created.)

let rec parseArgs' argList (a: Args) =
    match argList with
    | [] -> 
        a
    | "aaa"::xs -> 
        parseArgs' xs { a with Cmd = Aaa }
    | "bbb":: DateTimeExact "yyyyMMdd" d ::xs -> 
        parseArgs' xs { a with Cmd = Bbb(d) }
    | "-o"::xs -> 
        parseArgs' xs { a with OSet = true }
    | x::_ -> 
        failwith "Invalid argument %s" x

When calling parseArgs', you need to provide an initial version of Args - this is your "default" value.

let parseArgs argList = parseArgs' argList { Cmd = Aaa; OSet = false }

And then you can use it:

parseArgs ["aaa"] // valid
parseArgs ["bbb"; "20141127"] // valid
parseArgs ["bbb"; "20141127";"-o"] // valid
parseArgs ["-o";"bbb"; "20141127"] // valid
parseArgs ["ccc"] // exception

Your Args instance now has all the details in a strongly typed form.

let { Cmd = c; OSet = o } = parseArgs ["aaa"]

match c with
| Aaa -> run "aaa" o
| Bbb d -> run "bbb" d o

As you want more and different options, you just add them to your Types, then update your match statement to handle whatever the input version is.

There are of course many different ways to handle this. You might was error messages rather than exceptions. You might want the Cmd value to be an Option rather than defaulting to "Aaa". Etc, etc.


Edit in response to comment:

Adding an additional -p someValue is straightforward.

First, update Args to hold the new data. In your example the value of "someValue" is the important bit, but it may or may not be provided. And it's defined by "-p" so that we know it when we see it. For simplicity I'm going to pretend someValue is a string. So Args becomes:

type Args = { Cmd: Cmd; OSet: bool; PValue: string option }

Once you add the new field in Args, your "default" should complain because the new field is not set. I'm going to say that by default, -p is not set (that's why I've used a string option). So update to:

let parseArgs argList = parseArgs' argList { Cmd = Aaa; OSet = false; PValue = None }

Then you just need to identify and capture the value when it's provided. This is the pattern matching part:

let rec parseArgs' argList (a: Args) =
    match argList with
    ... snip ...
    | "-p"::p::xs ->
        parseArgs' xs { a with PValue = Some p}
    ... snip ...

Then just use the PValue value when you have your Args item.

Note: I'm not doing much validation here - I'm just assuming that whatever comes after "-p" is the value I want. You could add validation during the pattern matching using a "when" guard, or validate the Args value after it has been created. How far you go is up to you. I usually think about my audience (just me vs. work colleagues vs. whole internet).

Whether it's a good idea to allow the "bbb" and the date to be specified separately is up to you as the designer, but if the date should only be specified with the "bbb" command and is mandatory for it, then it probably makes sense to keep them together.

If the Cmd and the date were not "tightly linked" you could pattern match for the date separately from the cmd. If so, you might move the date from being held on the Cmd union, to being a field in its own right in Args.

If date was optional, you might use a -d [date] option instead. This would keep the same pattern as the other optional arguments.

An important thing to aim for is to try to make your interface as intuitive as possible for people to use. This is mostly about being "predictable". It doesn't necessarily mean catering for as many input styles as possible.

Upvotes: 5

Related Questions