chef focused on making a small dessert, with code floating in the air behind him

Writing DSLs: The joy of human consumable APIs

A well-designed Domain Specific Language (DSL) can help you be more productive as a developer, thus making you, your team and your clients happier. In this post, I’ll guide you through the design and creation of a simple DSL to create EPUB files. We’ll start with a regular API and refactoring until we get to a DSL solution.

A short into to DSLs

At it’s very core, DSL is a fancy term for a very simple language designed to solve something in particular. It’s domain-specific because it works in a very particular use-case, the most common ones being configuration files and APIs. If you are a Ruby developer then you have most likely used a DSL already. RSpec is one of the most popular:

That code is in a language designed to helps us write tests in a more natural way following the BDD testing methodology.
The result is code that is more understandable to you as a human — programmer or otherwise. Even if you’ve never used Ruby before, or don’t know about RSpec, you get an idea of what it is, it describes the functionality of Something.

The biggest drawback of DSLs is that you need to learn a new language every time — it’s easier to always use the same interface for all libraries. The advantage, though, is that the API is much more friendly and easier to use in the long-run. It’s an investment, the easiest the library, the lesser bugs consumers have, and everyone loves having less bugs. 🙂

So let’s get starting building a DSL. I’ll guide you through the design and creation of a simple DSL to create EPUB files. Starting with a regular API, we’ll refactor until we get to a DSL solution.

The design

EPUB is a format for digital books used by iOS and macOS. It’s basically a bunch of HTML files zipped together, following certain naming rules and ceremony. Without getting too deep into the file format specification, let’s just assume for now that all EPUBs must have a title, a description and at least one chapter. Initially, one could think of an API design as follows:

That looks good, right? If the problem is that simple, then we are done. But what if the generator needs more than just a title and a description. Let’s say we now also need an author and a URL. We could just add more arguments:

You might say “Meh it’s not that bad”. And you would be right! But we are taking an unnecessary risk, four arguments for a method is a red flag — it can get out of hand quite easily.

There are many ways to solve that issue, the most common of which is to “extract it into an object”. Let’s create a Book model. We just add the arguments as attributes, make sure the data is always consistent and just inject that object into our generator. Now our code is not only more solid and easier to maintain, but we have the added benefit of testability.

Now we are done… well, not really. Consider now that our EPUB generation library is a Ruby gem. We’ll force all our users to know all the class names: EPUBGenerator, Chapter and Book.
If the library is this small, it’s not really a big deal. If we know we’ll need to expose the user to more classes, then we might want to consider a better solution. This is where a DSL comes handy.

A DSL gives us yet another layer of abstraction. In this example, with a single class name, the user can easily use the library to create a new EPUB:

The way that looks is arbitraty, that’s just a common format for DSLs. With domain-specific languages it’s easier to start with “how it looks” and then move into the implementation, as the other way around might be harder if you have never made a DSLs before.

Now that’s a good enough solution. The code is simple and easy to read. We are still missing a few things though. What would a chapter definition look like? Easy!

You start to notice a pattern here, if chapters needed some dependency, we just pass a new block:

Good! We now have our general design, let’s make it happen!

The implementation

Ruby’s yield is what makes it so easy to write DSLs. You can think of it as a function which gets called with whatever arguments we give it.

In the code above, pass book, an instance of Book to a block of code. We don’t know what the code-block will do with it, that responsibility is up to the caller. The generate method call looks like this:

We’ve abstracted away the Book class name dependency! We’ve also reduced the ceremony for creating books, it’s much simpler now. Let’s repeat this process of yieding code blocks for the Book model:

Nice! Our generator now looks like this:

We are still lacking functionality, but the important thing is to realize that every time we write b.<something> in the generator, we are actually calling a method on a book instance.

That’s it! The hard part is done! From now on, it’s quite straightforward to implement the missing functionality. For the sake of completeness, let’s make another model, the Chapter:

The generator can now add titles to chapters:

Wrapping up

We’ve built our own DSL. And it wasn’t even hard! If you are curious and want the full source code, you can see a fully working gem on GitHub. The complete DSL looks like this:

BONUS TIP

Yielding blocks is used everywhere in Ruby. It is particularly useful for making sure resources are beeing handled properly, the most common example is file manipulation. In order to write to a file we have to open it for writing, write stuff, and then close it.

If we forget to close the file, we won’t get any errors, but it might lead to unexpected behavior. That’s not good, we want all our users to always close the file after they write to it. We can easily solve this with yield:

Leave a Reply