I Wish We Used Domain Specific Languages (DSLs) More

First, let me define a Domain Specific Language (DSL). A DSL as a small programming language specifically designed to communicate solutions for a particular domain of problems.

To expand on this definition, while the discussion of DSLs can include  internal DSLs, which are implemented directly within a General Programming Language (GPL) using the available language constructs, I’ll primarily be focusing on external DSLs, which require a custom parser and provide the most flexibility in terms of design. That said, many of the points below apply to internal DSLs, too.

Now that I’ve described what a DSL is, let me tell you why I wish we used DSLs more.

DSLs Are Small

Today we have access to many wonderful GPLs. There are so many, in fact, that it’s sometimes hard to get teams to come together and choose the “right” language for a particular job.

Perhaps due to this perceived competition amongst the languages, it sometimes feels like we’re in the middle of a language construct race.

Programmer 1: “Language X can do this in a few keystrokes with one operator.”

Programmer 2: “Whatever, Language Y has the deathstar operator, which blows Language X out of the sky.”

Programmer 3: “Language Y? That’s so YYYY – 1. We should use Language Z. With it’s first major release (9 minor versions from now some unknown time in the future), it will provide all of the features of Language X AND Language Y.”

As GPLs add language constructs to “simplify” the work of programmers, they require more and more language-level expertise. Indeed, syntactically complex GPLs limit the number of individuals who can properly communicate solutions.

In contrast, well-designed DSLs increase the population of individuals who can properly communicate solutions.  This inclusion of potential problem-solvers is one of their biggest strengths.

DSLs Are Specifically Designed to Communicate Solutions

Davin Granroth has told me of a professor during his college days who used to say, “Less isn’t more, just enough is more.” I see DSLs as tools that facilitate achieving this ideal through intentionally crafted parsimony.

In a well-crafted DSL, syntax is no longer a vestige of the GPL. Rather, every component of the grammar reflects careful choices that best facilitate the communication of solutions for its specific domain of problems, meaning the relevant information for a particular solution is brought to the surface. These qualities do lead to code that is easier to write. More importantly, these qualities also lead to code that is easier read, whether this happens hours, weeks, or years after the initial commit.

Additionally, because DSLs are focused on communicating solutions, they provide a great amount of flexibility when it comes to the actual implementation. Did the APIs change as part of the most recent service upgrade? No problem, the solutions communicated in the DSL don’t have to change. Do your programmers want to switch to language NextCoolRage? No problem, the solutions communicated in the DSL don’t have to change.

DSLs Adapt to Evolving Problem Spaces

“Work on this one specific problem, and I won’t change the constraints at all in the future…”, said no Project Manager (PM) ever. The solutions we communicate today may not adequately address problems we face tomorrow. Any tools we use to communicate solutions must provide the ability to easily accommodate change.

Because of their small size and specific focus on a particular domain of problems, DSLs can be created/adapted relatively quickly. Small languages can be implemented using relatively simple parsers, and making updates to the language is usually a straight-forward task. Additionally, because the problem space is so focused, the design and testing of changes is easier when compared to augmenting GPLs.

When your PM speaks of unanticipated changes that have to be addressed in the next sprint, you can nod your head and smile, retreat to your whiteboard, and start adapting/creating the DSLs that will allow your domain experts to properly communicate solutions.

Pure Functions

Overview: Striving to write pure functions (i.e., functions that are consistent and side-effect free) improves the testability, simplicity, and clarity of code.

What are Pure Functions?

Pure functions are consistent and side-effect free. A consistent function returns the same value every time for a particular set of arguments (this type of function is said to be referentially transparent, as calls to the function can be replaced by the return value without changing the program’s behavior.) A side-effect-free function does not change state through any means beyond its return value, meaning the values that existed before the function call (e.g., global variables, disk contents, static instances, UI, etc.) were not directly altered by the function; and it does not read any state beyond it’s arguments (i.e., no reading of data from files, databases, etc.) Think of pure functions like Mr. Spock: given a set of inputs, you will always get the same straight-forward, logical result (okay, okay, Spock showed an unpredictable, emotional response in “Amok Time”, but c’mon, he thought he had killed Captain Kirk.)

You don’t have to be using some fancy-pants functional programming language to benefit from pure functions. In languages that aren’t purely functional, you’ll have to work to avoid things like side effects and pay attention to whether the arguments you’ve received are copies or references, semantics that are language/context dependent. When dealing with references, you should treat them like you treat dad’s favorite belongings (like a special lamp, for instance): you can look (read the values), but don’t touch (edit the values)!

Examples of Pure and Impure Functions

Let’s work through some example functions and determine if they’re pure (i.e., consistent and side-effect free) or impure.

Below is a trivial example of a JavaScript function that returns the square of a number.

function square(x){
    return x * x;

Given a particular number x, this square function will always return the same result, so it is consistent. Additionally, it makes no changes to the global state beyond its return value. Therefore, it’s a pure function.

Next, an example Javascript function that checks out a book.

function checkOutBook(book, patron){
        return false;
    // changes to the book object alter the object beyond the scope of this function
    book.isCheckedOut = true;
    book.checkedOutTo = patron;
    return true;

The function is consistent, as passing in a particular set of arguments will always return the same result. However, the function changes some of the properties of the book object, changes that will persist even after the function has returned, so this function has side effects. Therefore, it’s an impure function.

The Benefits of Pure Functions

Pure functions facilitate simplicity and clarity. Because pure functions lack side effects, the outside world is completely abstracted away and the programmer can focus entirely on the parameters and control flow constructs contained within the function. Additionally, when calling a pure function, the programmer can focus solely on the visible context of the call and the return value, as the function has no other impact on state.

Testing pure functions proves extremely straight-forward. All possible paths/states of a pure function can be directly achieved by passing in different sets of arguments. The only things you’ll be mocking are Lions Fans (sure, we didn’t end the season well, but we really could have a great season next… oh, the abject sadness.)

A Usage Strategy for Pure Functions

Because of the benefits of pure functions, I follow the simple rule, “Strive for purity.” That is to say, I work hard to write as many functions as I can in a pure form, and when needed, I write functions that have side effects or are inconsistent.

Side effects aren’t bad. Any meaningful program will have side effects, and it doesn’t bother me in the least when it’s time to write an impure function. However, I try to keep the side effects isolated in small fall-through functions, so as to simplify the simplicity, clarity, and testability of the rest of the code base.