In this post, which is the second part of the series Functional error handling in F# and C#, we will examine an application for parsing command line arguments with error handling.
The functional approach uses Applicative Functors. Applicative Functors and the basics of functional error handling, only with immutable objects and without the use of exceptions, are explained int the first post of this series: Error handling with Applicative Functors in F# and C#.
A complete solution with all the code samples from this series can be found here.
As a starting point I took this F# console application template and reimplemented it with railway oriented error handling in F# and C#. The original version writes error messages directly to the console, which is totally ok in many situations. However, the railway oriented approach offers a bit more flexibility and safety because it will let the caller decide on how to handle the failure cases.
Parsing command line arguments in F#
The simple parser will return a dictionary containing key value pairs of commands and optional values, or a list of errors if the provided arguments cannot be parsed or don't comply to the given specification.
First we will define the models for specifying the arguments information and error cases:
type ArgInfo = { command:string; description: string option; required: bool}
type ClaError =
| InvalidArgument of string
| DuplicateCommand of string
| UnknownCommand of string
| RequiredCommandMissing of string
| ValueMissing of string
| CannotParseValue of string
Then we need a function to parse a sequence of strings to key value pairs, in this case just tuples of type (string * string option)
. The option
in the second value indicates that there can be commands without a value. Commands must be prefixed with either '-
', '--
' or '/
'.
let parse args =
let (| Command | Value |) arg =
let m = Regex.Match(arg, @"^(?:-{1,2}|\/)(?<command>[a-zA-Z0-9]+.*)$", RegexOptions.IgnoreCase);
match m.Success with
| true -> Command (m.Groups.["command"].Value.ToLower())
| _ -> Value (arg.ToLower())
let valueAndTail args =
match args with
| head::tail ->
match head with
| Command _ -> None, args
| Value v -> Some v, tail
| [] -> None, []
let rec parseRec result remaining =
match result, remaining with
| Bad _, _ -> result
| _, [] -> result
| Ok (parsed,_), head::tail ->
match head with
| Value v -> v |> InvalidArgument |> fail
| Command cmd ->
let value, tail2 = valueAndTail tail
parseRec (ok (parsed @ [(cmd, value)])) tail2
parseRec (ok []) args
If the arguments can't be parsed correctly, the function will return a failure that specifies the first bad argument.
Here are some examples called from a C# test project (with the help of some extension methods for C# compatibility and of FSharp.Extras):
// success
new[] { "-x1", "1", "--x2", "2", "/x3", "-x4", "4" }.Parse().Match(
ifSuccess: (v, _) => Check.That(v).ContainsExactly(
Tuple.Create("x1", "1".Some()),
Tuple.Create("x2", "2".Some()),
Tuple.Create("x3", FSharpOption<string>.None),
Tuple.Create("x4", "4".Some())),
ifFailure: _ => Assert.Fail());
/// failure
new[] { "-x1", "1", "--x2", "2", "x3", "-x4", "4" }.Parse().Match(
ifSuccess: (v, _) => Assert.Fail("succeeded but should fail"),
ifFailure: errs => Check.That(errs).ContainsExactly(ClaError.NewInvalidArgument("x3")));
Next there are three little checks that will ensure
- that there are no duplicates of commands
- that all required commands are in the argument list
- and that there aren't any commands in the argument list that are not in the definition list
let inList xs x = xs |> List.contains x
let checkArgsContainNoDuplicates definedCmds args =
args
|> List.filter (fst >> inList definedCmds)
|> List.groupBy fst
|> List.filter (snd >> List.length >> fun x -> x > 1)
|> List.map fst
|> List.distinct
|> List.map DuplicateCommand
let checkAllRequiredArgsExist requiredCmds args =
requiredCmds
|> List.filter (not << inList (args |> List.map fst))
|> List.map RequiredCommandMissing
let checkAllArgsAreDefined validCmds args =
args
|> List.map fst
|> List.distinct
|> List.filter (not << inList validCmds)
|> List.map UnknownCommand
For the sake of DRY and SoC these function can be combined with a function that transforms the error messages into a Result
.
let validate x validator =
let errs = validator x
match errs with
| [] -> ok x
| _ -> Bad errs
Combining everything
let parseArgs args argInfos =
let definedCommands = argInfos |> List.map (fun x -> x.command)
let required = argInfos |> List.filter (fun x -> x.required) |> List.map (fun x -> x.command)
let createDict _ _ x = dict x
parse args
>>= fun parsedArgs ->
createDict
<!> validate parsedArgs (checkArgsContainNoDuplicates definedCommands)
<*> validate parsedArgs (checkAllRequiredArgsExist required)
<*> validate parsedArgs (checkAllArgsAreDefined definedCommands)
First we transform the argument information into a list of all defined commands, required and not required, and into a list of only all required commands.
Then we define the function createDict
that takes three arguments that each represents one of the checks. createdict
transforms only the last argument into a dictionary. When the function will be lifted into the Result
type, the 3 arguments are needed to apply the results of the 3 checks. The 1st and the 2nd argument will be ignored if all the checks pass. But when lifted, if at least one of the checks fails, the error messages of all failed checks will be propagated.
Next, the arguments are parsed with parse args
. This returns a Result<(string * string option) list, ClaError>
. Then createDict
is combined with the result of parse args
with >>=
(infix bind
) because if the parsing fails we cannot validate. bind
is used because it bypasses all subsequent operations in case of a failure.
The validations are then composed with the applicative style so that error messages will be accumulated.
Dependent and independent checks
The general rule here is that dependent values are combined with the monadic style using bind
, and independent values are combined with the applicative style using e.g. apply
and lift
. This is described in more detail here by Scott Wlaschin.
The three validation functions are all independent. The outcome of any of them doesn't influence the outcome of the others. Therefore we can combine them with apply
.
They do depend however on the outcome of the parser function. So they have to be combined with that using bind
.
Example
Here is a test that shows the behavior:
var defs = new[]
{
new ArgInfo("x", FSharpOption<string>.None, true),
new ArgInfo("y", FSharpOption<string>.None, false),
new ArgInfo("z", FSharpOption<string>.None, false),
};
new[] { "-y", "lizard", "-a", "Anton", "-z", "Zealot", "-z", "Zoo" }
.ParseArgs(defs)
.Match(
ifSuccess: (dictionary, _) => Assert.Fail("should fail but was success."),
ifFailure: errs => Check.That(errs).ContainsExactly(
ClaError.NewDuplicateCommand("z"),
ClaError.NewRequiredCommandMissing("x"),
ClaError.NewUnknownCommand("a")));
Conclusion
We have seen an example of functional error handling in practice. The functional error handling approach leads us to defining small function that each do only one thing. In the parseArgs
functions all the small checks are composed together using the railway oriented style.
The details of error handling like e.g. accumulating messages or bypassing subsequent operations are abstracted away.
There is no way to access an invalid value. Therefore no exceptions can be thrown that break the flow of the program. Also, there is no need to wrap the calling code inside try catch
statements.
The railway oriented style enforces us to always handle both the success and the failure cases.
I disclaim that the design can still be improved. One issue e.g. is that the list of argument information can contain invalid state like duplicate definitions of commands. So a next step could be to make this invalid state unrepresentable by design. This parser is very simplistic and I focused on readability. Other than making functions tail-recursive e.g., performance was not a primary concern.
The code from this post can be found here.
In the next post of the series Functional error handling - parsing command line arguments in C# we will see how the same functional error handling behavior can be implemented in C#.