Last active
August 27, 2025 12:57
-
-
Save craigsmitham/a7f8cdbfc267a0a15e74 to your computer and use it in GitHub Desktop.
Monadic Command Line Parser w/Linq
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using System; | |
| /* | |
| This program illustrates the use of a C# monadic command line parser to provide a | |
| simple, declaritive, composable and strongly typed API that makes the resulting code easy | |
| to reason about what argument flags are being parsed, and how they are being used | |
| to execute program logic. This is done with a minimum (arguably no) boilerplate, improving | |
| on existing "Fluent", POCO/Attribute, and impertive approaches common in C# .NET libraries. | |
| The power and simplicity of this API is a result of using the LINQ query expression syntax | |
| to compose a custom monad. LINQ query expression syntax is relly just a monad comprehension in | |
| disguise (http://bit.ly/1KY2wMd), and also very similar to Scala for-comprhension syntax. We are | |
| NOT simply using LINQ exension methods on the command line arguments (we aren't even using | |
| System.Linq), but instead creating custom LINQ extension methods that operate on custom | |
| types that implement our domain logic. A common misconception among .NET developers is that LINQ | |
| and query expressions rely on an IEnumerable interface, or at least the IEnumerable extensions | |
| methods defined in System.Linq. The latter is partially true, but the reality is that any type | |
| that has appropriate .NET query operators (http://bit.ly/1hhPtfY) directly or via extension | |
| method can support LINQ. View/run on .NET Fiddle: https://dotnetfiddle.net/odTRNi | |
| Enjoy! | |
| Areas for futher improvement: | |
| - Allow composition of multiple commands (right now we're just parsing flags for a single command/exe) | |
| - Extend API to support documentation of flags and commands for display of command reference user | |
| - Error handling | |
| - Auto-complete | |
| */ | |
| public static class MonadicCommandLineParser | |
| { | |
| public static void Main(string[] args) => ( | |
| from message in String("message") | |
| from times in Int("times") | |
| from exitWhenDone in Bool("exit") | |
| select () => | |
| { | |
| for (var i = 0; i < times; i++) | |
| Console.WriteLine($"{i + 1}:\t{message}"); | |
| if (!exitWhenDone) Console.ReadLine(); | |
| })(new[] { "-message", "Howdy!", "-times", "10", "-exit", "false" } /* or use args */); | |
| // Linq Support | |
| public static CommandLineParser<TNext> Select<T, TNext>(this CommandLineParser<T> parser, Func<T, TNext> selector) => parser.ContinueWith(r => Success(selector(r))); | |
| public static CommandLineParser<TNext> SelectMany<T, TInner, TNext>(this CommandLineParser<T> parser, Func<T, CommandLineParser<TInner>> selector, | |
| Func<T, TInner, TNext> projector) => parser.ContinueWith(t => selector(t).Select(u => projector(t, u))); | |
| public static CommandLineParser<T> SelectMany<T, TInner>(this CommandLineParser<T> parser, Func<T, CommandLineParser<TInner>> selector, | |
| Func<T, TInner, Action> projector) => parser.ContinueWith(t => selector(t).Select(u => Execute<T>(projector(t, u)))); | |
| public delegate Result<T> CommandLineParser<T>(string[] args); | |
| public static CommandLineParser<T> Success<T>(T value) => args => Success(value, args); | |
| public static T Execute<T>(Action action) | |
| { | |
| action(); | |
| return default(T); | |
| } | |
| public static CommandLineParser<TNext> ContinueWith<T, TNext>(this CommandLineParser<T> initial, Func<T, CommandLineParser<TNext>> following) => | |
| args => initial(args).IfSuccess(result => following(result.Value)(result.Args)); | |
| // Parsers | |
| public static CommandLineParser<string> String(string flag) => args => GetArg(s => Success(s, args), flag)(args); | |
| public static CommandLineParser<bool> Bool(string flag) => args => GetArg(s => | |
| { | |
| bool value; | |
| return bool.TryParse(s, out value) ? Success(value, args) : Failure<bool>(args); | |
| }, flag)(args); | |
| public static CommandLineParser<int> Int(string flag) => args => GetArg(s => | |
| { | |
| int value; | |
| return int.TryParse(s, out value) ? Success(value, args) : Failure<int>(args); | |
| }, flag)(args); | |
| public static CommandLineParser<T> GetArg<T>(Func<string, Result<T>> map, string flag) => args => | |
| { | |
| var flagIndex = Array.IndexOf(args, "-" + flag); | |
| return flagIndex == -1 ? Failure<T>(args) : map(args[flagIndex + 1]); | |
| }; | |
| public class Result<T> | |
| { | |
| public Result(bool success, string[] args, T value = default(T)) | |
| { | |
| Success = success; Value = value; Args = args; | |
| } | |
| public bool Success { get; } | |
| public T Value { get; } | |
| public string[] Args { get; } | |
| } | |
| // Helpers | |
| public static Result<T> Success<T>(T value, string[] args) => new Result<T>(true, args, value); | |
| public static Result<T> Failure<T>(string[] args) => new Result<T>(false, args); | |
| public static Result<TNext> IfSuccess<T, TNext>(this Result<T> result, Func<Result<T>, Result<TNext>> next) => result.Success ? next(result) : Failure<TNext>(result.Args); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment