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.
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 way
declineallows you to combine different
Optsinstances 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! But
declinehandles these cases anyways.) This makes for a very flexible and general core; it also makes certain types of features harder to implement.
declinegenerally 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 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.
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.