Prefer declarative code over imperative code - building a command line parser in 5 lines of code

Prefer declarative code over imperative code - building a command line parser in 5 lines of code

Author: Kasper B. Graversen
[Introduction] [All categories] [All articles] [Edit article ]
Design KBGit Declarative Programming Imperative Programming Coding Guideline Code Readability

Declarative code has many advantages over imperative code. The code is simpler code due to a good separations of concerns. The "what" is cleanly separated from the "how". Further, the declarations may find other purposes such as automatic consistent documentation.

Please show your support by sharing and voting:

Reddit this Tweet this Googleplus this Facebook this LinkedIn this Feedly this Ycombinator this

Table of Content

One of my pet projects is to implement a working Git clone in just 500 lines of code (See KBGit on Github for more details). For that I need a command line parser. Given the fairly limited line budget, I need something short and sweet... let's build a command line parser in vey few lines of code!

Requirements for our command line parser

Our requirements are straight forward.

  1. We need to parse a set of pre-defined sentence such as git log.
  2. A sentence may leave room for additional information such as a commit message like git commit -m "user input here".
  3. After successfully parsing a sentence, we need to invoke specific parts of the git-implementation. E.g. if the user types "git log" we shall invoke the log() method.
  4. If a sentence cannot be matched, print a help-message detailing parseable sentences.

The imperative approach

Initially, I thought the smallest implementation was an imperative approach. E.g.

    if (args.length == 1 && args[0] == "log")
        return git.Log();
    if (args.length == 3 && args[0] == "commit" && args[1] == "-m")
        return git.Commit(args[2]);
    if (...)
    else
    {
        Console.WriteLine(@"Cannot parse input");
        Help
        git log                 for logging
        git commit -m <message> for committing
        ...");
    }

I bet you have seen plenty of code like this. Often when I encounter a wall of code like this, I cannot help but play out in my head, times toll on the code. Pressure to deliver, or perhaps lack of knowledge of a better way. And perhaps it all started out as a single if-else..then.. slowly over time.. turning into a monstrosity.

Aesthetics aside, there are a few downsides to this approach:

The declarative approach

A declarative approach operates on a more formal grammar and has a general matching algorithm applied to all grammar lines in search of a match. It turns out we can write a declarative parser in only 5 lines of code! In addition to the parser is a line of code declaring the grammar for each sentence to match.

Let's first have a look at the parser:

// declarative parser
var matches = Config
    .Where(x => x.grammar.Length == cmdParams.Length)
    .SingleOrDefault(x => x.grammar.Zip(cmdParams, (gramar, arg) => gramar.StartsWith("<") || gramar == arg).All(m => m));

if (matches.grammar == null)
    return $"KBGit Help\r\n----------\r\ngit {string.Join("\r\ngit ", Config.Select(x => $"{string.Join(" ", x.grammar),-34} - {x.explanation}."))}";

// using the parser
var valueFromInvokingTheGitFunction = matches.actionOnMatch(git, cmdParams);

So the basic idea is

The only thing left to explain, is the grammar lines. Below are two examples. Each grammar line consists of three parts. A readable explanation, the sentence to parse and finally, the code to invoke on a match. We take advantage of the named tuple feature of C# here:

(string explanation, string[] grammar, Func<KBGit, string[], string> actionOnMatch)[] Config =
{
    ("Show the commit log", new[] { "log"}, (git, args) => git.Log()),
    ("Make a commit", new[] { "commit", "-m", "<message>"}, (git, args) => { git.Commit(args[2], "author", DateTime.Now); }),
}   

If you don't think about it, you almost don't see it. The grammar is quite readable. The grammar simply is "commit", "-m", "<message>" !

Conclusion

The declarative implementation is a bit more advanced than the imperative implementation, but has a number of advantages.

  1. The grammar is very readble and is not concerned with how a matching strategy is implemented.
  2. Since the declaration of the grammar is separate from the actual matching, we can improve the parser over time without needing to change our grammar specification (the Config variable above).
  3. The parser operates on the grammar and the grammar is (coincidentally) readily printable as a help text. By printing the grammar as the documentation, our documentation is never out of sync.
  4. Lastly, and perhaps only important to the KBGit implementation, it is less lines of code!

I hope you feel inspired to do more declarative programming and less imperative programming in the future. :-)

More articles on this topic

Please show your support by sharing and voting:

Reddit this Tweet this Googleplus this Facebook this LinkedIn this Feedly this Ycombinator this



Congratulations! You've come all the way to the bottom of the article! Please help me make this site better for everyone by commenting below. Or how about making editorial changes? Feel free to fix spelling mistakes, weird sentences, or correct what is plain wrong. All the material is on GitHub so don't be shy. Just go to Github, press the edit button and fire away.






Read the Introduction or browse the rest of the site