Functional error handling – parsing command line arguments in C#

This is the third part of the series Functional error handling in F# and C#. In this post we will see how the command line argument parser with functional error handling, that was shown here using F#, can be implemented in C#.

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.

Parsing command line arguments in C#

For the C# version of the argument parser we will use the same models in order to avoid duplication:

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

The approach to parsing the arguments is slightly different to the F# implementation. But it is also a functional approach without mutating any state. It makes use of a fold operation which is much faster for long lists of arguments than the recursive implementation. The reason is probably that the C# compiler cannot optimize tail recursive code.

private static FSharpOption<string> TryParseCommand(this string cmd)
{
    var m = Regex.Match(cmd, @"^(?:-{1,2}|\/)(?<command>[a-zA-Z0-9]+.*)$", RegexOptions.IgnoreCase);
    return m.Success
        ? m.Groups["command"].Value.ToLower().Some()
        : FSharpOption<string>.None;
}

internal static Result<List<Tuple<string, FSharpOption<string>>>, ClaError> Parse(this IEnumerable<string> args)
{
    Func<Result<List<Tuple<string, FSharpOption<string>>>, ClaError>, Tuple<FSharpOption<string>, string>, Result<List<Tuple<string, FSharpOption<string>>>, ClaError>> folder =
        (acc, t) => 
            from accValue in acc
            from accValueNew in t.Item1.Select(cmd =>
                    t.Item2.TryParseCommand().HasValue()
                        ? Tuple.Create(cmd, FSharpOption<string>.None)
                        : Tuple.Create(cmd, t.Item2.ToLower().Some()))
                .ToResult(ClaError.NewInvalidArgument(t.Item2))
                .Select(xs => accValue.Concat(new[] { xs }).ToList())
            select accValueNew;

    // first arg has to be a command
    var emptyOrError = args.Take(1).All(x => x.TryParseCommand().HasValue())
        ? Result<ClaError>.Succeed(new List<Tuple<string, FSharpOption<string>>>())
        : Result<List<Tuple<string, FSharpOption<string>>>>.FailWith(ClaError.NewInvalidArgument(args.First()));

    return args
        .Zip(args.Skip(1).Concat(args.Take(1)), (fst, snd) => Tuple.Create(fst.TryParseCommand(), snd))
        .Where(pair => pair.Item1.HasValue() || !pair.Item2.TryParseCommand().HasValue())
        .Aggregate(emptyOrError, folder);
}

Validation

Here are three little checks implemented in C# 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
private static readonly Func<IEnumerable<string>, Func<IEnumerable<Tuple<string, FSharpOption<string>>>, IEnumerable<ClaError>>> CheckArgsContainNoDuplicates =
    definedCommands => args =>
        args
        .Where(x => definedCommands.Contains(x.Item1))
        .GroupBy(x => x.Item1)
        .Where(x => x.Count() > 1)
        .Select(x => x.Key)
        .Distinct()
        .Select(ClaError.NewDuplicateCommand);

private static readonly Func<IEnumerable<string>, Func<IEnumerable<Tuple<string, FSharpOption<string>>>, IEnumerable<ClaError>>> CheckAllArgsAreDefined =
    definedCommands => args =>
        args
        .Select(x => x.Item1)
        .Distinct()
        .Where(x => !definedCommands.Contains(x))
        .Select(ClaError.NewUnknownCommand);

private static readonly Func<IEnumerable<string>, Func<IEnumerable<Tuple<string, FSharpOption<string>>>, IEnumerable<ClaError>>> CheckAllRequiredArgsExist =
    requiredCommands => args =>
        requiredCommands
        .Where(required => !args.Select(p => p.Item1).Contains(required))
        .Select(ClaError.NewRequiredCommandMissing);

They are defined as private properties of type Func in curried form to support nice composability. We could define them as “normal” functions if we want more idiomatic code. But this is off course a matter of taste.

Combining everything

Here is how everything will be composed together. The inner LINQ query expression make use of the applicative style to accumulate the error messages.

public static Result<Dictionary<string, FSharpOption<string>>, ClaError> ParseArgs(this IEnumerable<string> commandLineArgs, IEnumerable<ArgInfo> defs)
{
    var definedCmds = defs.Select(x => x.command);
    var requiredCmds = defs.Where(x => x.required).Select(x => x.command);

    return 
        from parsedArgs in commandLineArgs.Parse()
        from dict in 
            from check1 in parsedArgs.Validate(CheckArgsContainNoDuplicates(definedCmds))
            join check2 in parsedArgs.Validate(CheckAllRequiredArgsExist(requiredCmds)) on 1 equals 1
            join check3 in parsedArgs.Validate(CheckAllArgsAreDefined(definedCmds)) on 1 equals 1
            select check3.ToDictionary(x => x.Item1, x => x.Item2)
        select dict;
}

Example

The behavior is exactly the same as with the F# version:

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

Doing functional error handling with railway oriented programming is possible in C#. Because of missing language features like pattern matching, advanced type inference, or tail-recursion support it might not feel very comfortable right away. However, it has a lot of benefits over the imperative way of doing this. And a strange programming construct may just be a friendly construct you haven’t yet met.

The code from this post can be found here.

In the next post Functional vs. imperative error handling we will compare the functional to the imperative approach to error handling.