Five years of decline
(the command-line parsing library)
It’s been a little over five years since I first published decline
, a functional and UNIXy command-line parser for Scala. (If you’re not familiar with decline
, the documentation might help.)
For me, five years is enough time to start forgetting things… so before that happens, this page collects some of the early history of decline
and the motivations and ideas behind it. It may be interesting to others thinking about API design, or anyone who’s just curious about the library.
Why decline?
Imagine it: you’re me, in the mid-2010s, writing Scala. The teams I worked on tended to write in the popular “mostly functional” style, which stresses things like composition and immutability. For new team members, who often came in with a Java/OOP-ish background, this style was often fairly new. (As it was for me just a couple years earlier!)
One common way to learn a language is to write a little command-line application; and if you do, one of the first libraries you may reach for is a command-line parser. There were a couple popular Scala libraries for this already, but they were designed around mutation and made composition rather awkward. I don’t mean to slight these libraries; they do the job effectively, and they’re still very popular! But it sure doesn’t look that great for functional programming if it can’t handle the first simple problem you encounter.
If you want to figure out how to solve a problem with functional programming, Haskell is one obvious place to look. Haskellers in particular tend to reach for optparse-applicative
; it has a clean, compositional API and generates very good help output out of the box. Unfortunately, porting Haskell code directly tends to make for very and obscure awkward Scala… a particularly bad idea if you’re hoping to make something approachable. So, decline
’s job: pull the key ideas from optparse-applicative
over to Scala, but in a way that feels as idiomatic as possible and plays nicely with the rest of the language.
Small and boring
If you’re working on a fun and exciting library like Spark or Cats — the sort of thing that someone might write a book about, or add as a skill to their CV — you can afford a bit of a learning curve if it pays off in the long run.
decline
is not that kind of library. Relatively few people are especially fired up about command-line parsing. (This is true for most domains, I think… a bit of a trap for a library author, because almost any domain can be interesting once you get really deep into it, so it’s easy to complicate things in ways that won’t benefit most users.) So: one priority for decline
was to keep the API as simple as possible. This has had a major impact on every part of the design:
- Any set of command line options – ranging from single flags or positional arguments to dozens of nested subcommands – has the same top-level type,
Opts
. This means any waydecline
allows you to combine differentOpts
instances ought to work for all possible combinations of options. (Want to give a subcommand a default value, or make a flag and pair of positional arguments mutually exclusive? Probably not! Butdecline
handles these cases anyways.) This makes for a very flexible and general core; it also makes certain types of features harder to implement. decline
generally only implements a CLI-facing feature if it’s found in the POSIX or GNU standards (like short and long flags) or is very common in the wild (like subcommands). Aside from generally keeping things well-scoped, this was also meant to nudge developers towards building CLIs with a more standard shape that was easier to document… and thus hopefully for the end user to learn.- It has relatively few helper methods: anything that’s not extremely commonly-needed has been left out. As a result, many users building complex CLIs end up implementing some helpers of their own to make certain patterns easier. However, everyone seems to want a slightly different set… if all the helpers anyone had suggested were added to
decline
, the public API would be several times larger.
This tradeoff is probably not the right one for all libraries, or all authors, and I suspect some users have found this annoying… but on balance, I think it’s been pretty fruitful for decline
.
One way decline
manages to add value without making the interface more complicated is by improving the quality of the error messages or the help. (Probably half the code in decline
is dedicated to this, and there’s still some room to do better.) These aren’t especially glamorous features, but I think they do a lot to improve the experience for the end users of the CLI, as well as the developers who care about them.
On Alternative
Type classes are a common tool for writing generic code in Scala, but they also work a bit like design patterns are intended to. If you know some type implements a particular type class, that often gives you a pretty good hint about how to work with it; and when you’re designing a type it’s often helpful to think a bit about the type classes it might implement to guide your work.
decline
is designed around the Alternative
type class. This is the key idea it takes from optparse-applicative
. It’s not especially common in Scala, though; decline
is often the first place people encounter Alternative
in the wild. Since it’s unfamiliar, this adds some learning curve, but it’s also critical for features like the autogenerated help output or powerful validation.
I’d love for Alternative
to be better known: it is an extraordinarily useful type class. However, decline
’s docs barely mention it! It’s possible to be effective with decline
without understanding Alternative
, so forefronting it might be unnecessarily daunting or confusing. Personally, I find a type class easier to get an intuition for when I’ve already seen the pattern a few times; I hope that having some experience with decline
makes it easier for people to figure out Alternative
later on, if they decide to take that plunge.
Releasing, and what comes after
decline
existed for nearly a year before I really told anyone about it. I have never been that interested in the glamorous life of an open-source maintainer — for me, the responsible-maintenancey aspects of the job are not the fun parts, important as they are — but also wasn’t keen to just throw the codebase over the wall and walk away. I did eventually submit decline
to the Typelevel incubator, and it’s mostly spread by word of mouth since then.
I have not regretted this so far! In part, the narrow scope of the library has helped keep things manageable… and while it does require saying “no” a lot, most users have been pretty good sports about it. decline
has also attracted a wonderful set of contributors who’ve added features or helped the library keep up with change in the world around it; my sincere thanks to everyone who’s chosen to spend their time on this!
decline
also owes a huge debt to Typelevel. Aside from the cats
library, which is great and decline
uses to full advantage — community moderation is a hard and often thankless job, and Typelevel has built what is perhaps the healthiest community in the Scala ecosystem. decline
would likely not exist without it.