This is a sequel to my earlier piece Why Racket? Why Lisp?, which I wrote a year or so after I discovered Racket. As someone new to Lisp languages, I had trudged through bogs of Lisp flattery, never sure what to make of the often handwavy claims. For instance, that Lisp eventually induces “profound enlightenment”. Sure—whatever you say, bro.
I had a simpler question: what’s in it for me, now? My earlier piece tried to answer that question. I summarized why someone who hadn’t looked into a Lisp language—or Racket in particular—might want to.
I included a list of the nine language features that were most valuable to me as a Racket noob. Feature #5 was “create new programming languages”. This technique also goes by the name language-oriented programming, or LOP.
In the years since, language-oriented programming has risen to become my favorite part of Racket. Along the way, I converted my enthusiasm into action. In addition to making languages, I wrote this online book—Beautiful Racket—that teaches LOP as a technique, and Racket as a tool.
In my own work, one example is Pollen, a text-based language I wrote to make my online books Practical Typography and Beautiful Racket possible. + Pollen is based on Racket’s documentation language Scribble, which handles most of the heavy lifting. In Pollen, the paragraph above is programmed like so:
1 2 3 | #lang pollen I included a list of the ◊link["why-racket-why-lisp.html#so-really-whats-in-it-for-me-now"]{nine language features} that were most valuable to me as a Racket noob. Feature #5 was "create new programming languages". This technique also goes by the name ◊em{language-oriented programming}, or ◊em{LOP}. |
Another example is brag, a parser generator (in the lex/yacc style) that takes a BNF grammar as its source code. A simple example for the bf language:
1 2 3 4 5 | #lang brag bf-program : (bf-op | bf-loop)* bf-op : ">" | "<" | "+" | "-" | "." | "," bf-loop : "[" (bf-op | bf-loop)* "]" |
Both these languages are implemented in Racket, and can be run with the usual Racket interpreter, or inside the Racket IDE (called DrRacket).
And yet. Though this book has started thousands of people on their LOP journey, I sometimes fear that I’ve fallen into the same quicksand as the Lisp advocates I once criticized.
If LOP is so great, then you shouldn’t need to spend a few days working through the tutorials in this book. Right? I should be able to explain it concisely, with minimum handwaving. I should be able to answer two simple questions:
What problems are best suited to language-oriented programming?
Why is Racket best suited for the task of making languages?
The second question is easy. The first question isn’t. I’ve been asked it many times. I’ve often resorted to the answer made famous by Justice Potter Stewart: you’ll know it when you see it. That answer is good enough for those already LOP-curious. But not for those still on the sidelines who want more of a practical justification.
So here’s a better attempt. Bear in mind that I’m not a computer-science professor. I’m not going to be talking about programming languages the way they might. Rather, I use Racket and DSLs for practical purposes—my daily work depends on them. My goal is to frame the issue in a way that will be useful for other practical users. (If I haven’t, you’re welcome to click in the left margin and send me a comment.)
Language-oriented programming is really an interface-design technique. It’s unbeatable for tasks that demand minimum notation while preserving maximum precision. Minimum notation = the only notation is what you permit. There is nothing extraneous. Maximum precision = the meaning of that notation is exactly what you say. There is no scaffolding or boilerplate. LOP gets to the point like nothing else.
(The impatient can jump ahead to these categories of tasks that are likely to benefit from LOP.)
Racket is ideal for LOP because of its macro system. Macros are indispensable for making languages because they make compiler-style code transformations easy. Racket’s macro system is better than any other.
About half the readers of this piece are now departing to post anonymous internet comments disputing the above claims. Before you leave, please know: I’m hedged either way. LOP and Racket have been an incredible force multiplier on my programming productivity. I’m happy to share this knowledge with you, so that you too may benefit. But I’m equally happy for these tools to remain my secret weapons, so I can keep producing work that is more ambitious, more impressive, and more profitable than the other 99.9%.
The choice, however, is yours.
I finally started to unravel the Big Questions by considering a metaquestion: why does it seem hard to explain the benefits of language-oriented programming?
Perhaps because when we speak of programming languages—or just languages, shorthand that I’ll use interchangeably—the term is freighted with expectations about what a language is and what it does. As long as we’re standing inside that box, it’s more difficult to see the value of language-oriented programming.
But if we zoom out and look at languages as part of the larger category of human–computer interfaces, then it becomes easier to see the special benefits of LOP.
So let’s do that.
First, some terminology. Language-oriented programming (aka LOP) is the idea of solving a programming problem by making a new programming language, and then writing a program with the new language. Often, these “little languages” are known as domain-specific languages or DSLs.
As the name implies, a domain-specific language is one tailored to the needs of a particular set of problems. For instance, PostScript, SQL, make, regular expressions, .htaccess, and HTML qualify as domain-specific languages. They don’t try to do everything. Rather, they focus on doing one thing well.
At the other end of the language spectrum are what we’ll call general-purpose languages. Out here we find the usual suspects—C, Pascal, Perl, Java, Python, Ruby, Racket, etc. Why aren’t these languages DSLs? Because they hold themselves out as being suitable for a wide range of computing tasks.
In practice, general-purpose languages often have certain jobs that they do better than others, depending on the conditions of their birth. For instance, C excels at systems programming. Perl excels as a sysadmin scripting language. Python excels as a language for beginners. Racket excels at LOP. In each case, because that’s what the language was designed to do.
The distinction between domain-specific and general-purpose languages is permeable. For instance, Ruby started as a general-purpose language, but became popular largely as a web-application DSL through its association with Ruby on Rails. Going the other direction, JavaScript was originally a DSL for web-browser scripting. Like a mutating virus, it has since grown far beyond.
If all these things on the spectrum between domain-specific languages and general-purpose languages deserve to be called languages, then what are the defining features of a language?
I know what you’re thinking: “Well, that’s where you’re wrong. HTML isn’t a language. It’s just markup. It can’t express algorithms.” Or maybe: “Regular expressions aren’t a language. They can’t stand on their own. They’re just syntax within another language.”
I once felt that way too. But the closer I looked, the more these distinctions seemed vaporous. Thus, my first major claim (of three): a programming language is inherently a medium of exchange—a notation system mutually intelligible to humans and computers.
“Notation” connotes that a language has syntax; “intelligible” connotes that a language has meaning (or semantics, to use the fancier word) attached to the syntax. This definition covers all general-purpose programming languages. And all DSLs. (But not every data stream—about which more later.)
(By the way, even though “programming” and “language” are words idiomatically used together, these languages are not used strictly by humans to program computers. Sometimes they’re used by computers to communicate with us; + For instance: S-expressions. sometimes with each other. + For instance: XML, JSON, HTTP. Definitionally, it seems wrong to exclude these possibilities. But in practice, yes—what we’re usually doing with a programming language is, you know, programming.)
Consider HTML. It’s a way of telling a computer—in particular, a web browser—how to draw a web page. It’s a notation system (= angle brackets, tags, attributes, and so on) intelligible to the human and computer (= the charset meta defines the character encoding, p tag contains a paragraph, and so on).
Here’s a little HTML page:
1 2 3 4 5 6 7 8 9 10 | <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>My web page</title> </head> <body> <p>Hello <strong>world</strong></p> </body> <html> |
Suppose you dispute that HTML is a programming language. Fine. We’ll output our page with Python. That’s a real programming language, right?
1 2 3 4 5 6 7 8 9 10 | print("<!DOCTYPE html>") print("<html>") print("<head>") print("<meta charset=\"UTF-8\">") print("<title>My web page</title>") print("</head>") print("<body>") print("<p>Hello <strong>world</strong></p>") print("</body>") print("<html>") |
If Python is a programming language and HTML is not, then it must be true that this Python sample is a program, and the HTML sample is not.
Plainly, this distinction is tortured. Here, the Pythonization adds nothing but complexity and boilerplate. More piquantly, the only interesting semantic content in the Python program—in terms of controlling the web browser—is what’s embedded in the HTML. + Arguably, HTML tags—like DOCTYPE and meta and strong—are nothing more than functions that take arguments. My DSL Pollen builds on this correspondence. Consistency compels us to conclude that HTML, though simpler and less flexible than Python, is nevertheless a programming language too.
Our HTML example was contrived. But this pattern—a DSL nested within another language—is pervasive. Languages used in this way are called embedded languages. They represent the most common form of language-oriented programming. As a programmer, you’ve relied on LOP for years, even if you didn’t know its name.
For instance, regular expressions. + Other examples: printf formatting strings, CLDR date/time patterns, SQL. We may not think of regular-expression notation as an independent language. But every programmer knows what this means:
1 | ^fo+(bar)*$ |
Moreover, you can probably type this regular expression into your favorite programming language and it will just work. This consistent behavior is only possible because regular-expression notation is an embedded language, externally defined (by POSIX).
As with HTML, we could write the equivalent string-matching computation in the notation of the host language. For instance, Racket supports Scheme Regular Expressions (SREs), which are regular expressions that use S-expression notation. The above pattern would be written like so:
But Racket programmers rarely use SREs. They’re too long and too hard to remember.
Another ubiquitous example of an embedded DSL: math expressions. Every programmer knows what this means:
1 | (1 + 2) * (3 / 4) - 5 |
On their own, math expressions don’t make interesting programs. We need to combine them with other language constructs. But as with regular expressions, that’s an ergonomic and practical consideration. Math expressions have their own notation and meaning, intelligible by both humans and computers, and thus qualify as a separate embedded language.
Indeed I am. I’m claiming that HTML (and regular expressions and math expressions) qualify as rudimentary programming languages. This necessarily implies that writing HTML (or regular expressions or math expressions) qualifies as rudimentary programming.
Please—don’t panic. We can agree that someone who billed themselves as a “programmer” on LinkedIn while only knowing HTML and arithmetic would be considered delusional. + And next week, will probably have a $180K software-engineering job in Menlo Park. But that spills into a separate issue about what the designation “programmer” typically denotes in the job market. That’s not our concern here.
If this definition of programming languages still rankles, maybe it’s because you think a real programming language needs to be capable of expressing all possible algorithms—that is, it must be Turing complete.
I see why that might be part of the intuition. Every general-purpose programming language is Turing complete.
The problem? Turing completeness is a low bar. It’s a technical measurement that doesn’t tell us anything interesting about the language in the real world. + “Beware of the Turing tarpit in which everything is possible but nothing of interest is easy.“—Alan Perlis For instance, regular expressions aren’t Turing complete, but they’re valuable because they express a lot of computation with minimal notation. HTML is also not Turing complete, but it’s a useful way to control a browser. By contrast, the bf language is Turing complete, but even the most banal tasks require acres of impenetrable code.
Does my definition of a programming language include everything? No.
Binary data formats don’t qualify as languages. For instance, a jpeg file. Though a computer can understand it, a human cannot. Or a PDF: if you crack one open, you’ll find some parts that seem human readable. But that’s incidental to how PDF works. There’s no sense in which a human is intended to notate ideas using PDF constructs.
Plain text files are not languages. Suppose we have a file containing Homer’s Iliad. We humans can read and understand this file. Though a computer can trivially process the file—say, by printing its contents—the text within the file is unintelligible to the computer.
Graphical user interfaces are not languages. Yes, they’re notation systems (that rely on text and image). But they’re only intelligible to humans. Computers draw GUIs, but they’re not intelligible to the computer. + But see Racket’s 2d language, an embedded language that makes ASCII-art boxes intelligible.
Above, I described a programming language as a “medium of exchange” between humans and computers. In that way, languages fit among the broader category of things we call interfaces.
This brings me to my second major claim (of three): that language-oriented programming is fundamentally an interface-design technique. If you like thinking about interfaces, you’ll love LOP. If you don’t, you may still love LOP, because it enables certain interfaces that are otherwise unattainable.
One of my favorite examples of language as interface is brag, a parser-generator language made with Racket. If you’ve ever used the lex/yacc toolchain, you know the goal is often to generate a parser from a BNF grammar. For instance, the BNF grammar for the bf language looks like this:
1 2 3 | bf-program : (bf-op | bf-loop)* bf-op : ">" | "<" | "+" | "-" | "." | "," bf-loop : "[" (bf-op | bf-loop)* "]" |
To make a parser in a general-purpose language, we’d have to translate this grammar into a pile of native code. Even with the help of a parser generator, it’s a tedious job. And pointless—haven’t we already notated the grammar? Why do it again?
With brag, however, our wish comes true. To make a parser, we simply add #lang brag to the file, which magically converts our BNF grammar into brag source code:
1 2 3 4 | #lang brag
bf-program : (bf-op | bf-loop)*
bf-op : ">" | "<" | "+" | "-" | "." | ","
bf-loop : "[" (bf-op | bf-loop)* "]"
|
That’s it! When compiled, this file will export a function called parse that implements this BNF grammar.
This is one of my favorite examples of LOP because it’s unassailably superior to the long-winded alternative. Moreover, with a general-purpose language, this kind of interface is essentially impossible.
But the language-oriented programmer does it all the time.
This brings me to my third and final major claim, which is that among interfaces, languages have unique strengths. The categories below are not exhaustive or exclusive, of course. But I’ve found that LOP has a lot to offer in these situations:
When you want to create an interface usable by less skilled programmers, or nonprogrammers, or lazy programmers (don’t underestimate the size of that last category).
For instance, Racket has an elaborate web-application library. But it’s also possible to spin up a simple web server quickly with the web-server/insta language:
1 2 3 4 | #lang web-server/insta (define (start request) (response/xexpr '(html (body "Hello LOP World")))) |
Matthew Flatt’s article Creating Languages in Racket demonstrates a language that generates playable text adventures. As with brag, it looks more like a specification than a program, but it works:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | #lang txtadv ===VERBS=== north, n "go north" south, s "go south" get _, grab _, take _ "get" ===THINGS=== ---cactus--- get "Ouch!" ===PLACES=== ---desert--- "You're in a desert. There is nothing for miles around." [cactus, key] north meadow south desert |
When you want to simplify the notation. Regular expressions are one example. Another example is my DSL Pollen, a language for making online books (including this one). Pollen is like Racket, except that you start in plain-text mode, and use a special character to denote Racket commands, which are evaluated and spliced into the content. + Pollen is based on Racket’s documentation language Scribble, which handles most of the heavy lifting. So the first part of this paragraph is programmed like so:
1 2 3 | #lang pollen When you want to simplify the notation. Regular expressions are one example. Another example is my DSL ◊link["https://pollenpub.com"]{Pollen}, a language for making online books (including this one). |
Pollen takes care of inserting all the necessary tags and converting it to infallible HTML. I get the benefits of manual markup (I still have total control of what ends up in the page) but none of the costs (I cannot, say, accidentally omit a closing tag).
Another example of simplified notation is lindenmayer, a language for generating and drawing fractals called Lindenmayer systems, like this one:
In ordinary Racket, a Lindenmayer program might look like this:
But one can use the simplified notation merely by changing the #lang designation at the top of the file:
1 2 3 4 5 6 7 8 | #lang lindenmayer/simple ## axiom ## A ## rules ## A -> AB B -> A ## variables ## n=3 |
The language presumes you already know something about Lindenmayer systems. But the simplified notation makes it easy to translate what you know into a program that does what you want.
When you want to build on existing notation. Above, we saw brag, a DSL that uses a BNF grammar as source code.
1 2 3 4 | #lang brag bf-program : (bf-op | bf-loop)* bf-op : ">" | "<" | "+" | "-" | "." | "," bf-loop : "[" (bf-op | bf-loop)* "]" |
Another example—early on, people who tried Pollen said “that’s cool dude, but I prefer Markdown.” Wish granted. pollen/markdown is a Pollen dialect that offers the semantics of Pollen but accepts ordinary Markdown notation:
1 2 3 4 | #lang pollen/markdown When you want to simplify the notation. Regular expressions are one example. My DSL [Pollen]("https://pollenpub.com") is a language for making online books. |
The nicest part? It took me about one hour to create this dialect by combining a Markdown parser with my existing code.
When you want to create an intermediate target for other languages. JSON, YAML, S-expressions, and XML are all DSLs that define data formats meant to be machine-writable and -readable.
In Beautiful Racket, one tutorial language is called jsonic. It lets us embed Racket expressions in JSON, thereby making JSON programmable. So a source file like this:
1 2 3 4 5 6 7 8 9 10 11 | #lang jsonic // a line comment [ @$ 'null $@, @$ (* 6 7) $@, @$ (= 2 (+ 1 1)) $@, @$ (list "array" "of" "strings") $@, @$ (hash 'key-1 'null 'key-2 (even? 3) 'key-3 (hash 'subkey 21)) $@ ] |
Compiles to an ordinary JSON result:
1 2 3 4 5 6 7 | [ null, 42, true, ["array","of","strings"], {"key-1":null,"key-3":{"subkey":21},"key-2":false} ] |
When the bulk of the program is configurational. Dotfiles, for instance, can be characterized as DSLs. A more sophisticated example in Racket is Riposte by Jesse Alama, a language for testing JSON-based HTTP APIs:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | #lang riposte $productId := 41966 $qty := 5 $campaignId := 1 $payload := { "product_id": $productId, "campaign_id": $campaignId, "qty": $qty } POST $payload cart/{uuid}/items responds with 200 $itemId := /items/0/cart_item_id GET cart responds with 200 |
As a miniature scripting language, Riposte is a lot smarter than the average dotfile. It hides all the intermediate code required for HTTP transactions, and lets the language user focus on writing tests. It’s still housekeeping. But at least you can focus on the housekeeping that you care about.
A common critique of LOP is “why make a domain-specific language? Isn’t that more work than writing a native library?”
Not if you have the right tool. Racket is unusual: it’s been designed from the ground up to support LOP. Thus, implementing a DSL in Racket is faster, cheaper, and easier than the alternatives. For instance, in the first tutorial in this book, I show how you can make a language in one hour—even if you’ve never used Racket.
Under the hood, every DSL in Racket is actually a source-to-source compiler that converts the notation and semantics of the DSL into the equivalent Racket program. For this reason, a Racket DSL isn’t going to run as fast as one written in hand-carved C. But it also makes all of Racket’s tooling and libraries accessible in every Racket DSL. So what you lose in performance, you get back many times over in convenience. And when DSLs are convenient and cheap, they become a realistic option for a much wider range of problems.
Thus, to answer the critique—no, a DSL is not necessarily more work than a native library. Moreover, as we’ve already seen, as an interface, a language can do things that a native library cannot.
Because Racket DSLs compile to Racket, a language-oriented programmer using Racket needs to write some syntax transformers that convert the DSL notation into native Racket. These syntax transformers are known as macros. Indeed, macros can be characterized as extensions to the Racket compiler.
Racket’s macro system is vast, elegant, and undeniably its crown jewel. But a lot of this book is about the joy of Racket macros. That material is ready when you are. For now, the two standout features:
Racket has a specialized data structure called a syntax object that is the medium of exchange among macros. Unlike a string, which can only contain raw code, a Racket syntax object packages the code in a way that preserves its hierachical structure, plus metadata like lexical context and source locations, and arbitrary fields called syntax properties. This metadata stays attached to the code during its various transformations. (See syntax objects for the details.)
Racket macros are hygienic, which means that by default, the code produced by a macro retains the lexical context from where the macro was defined. In practice, this eliminates a huge amount of the housekeeping that would ordinarily be required to make DSLs work. (See hygiene for the details.)
Is it possible to implement a DSL in, say, Python? Sure. In fact, I wrote my first DSL in Python—one that I still use to help with my type-design work. Yikes. Once was enough. Ever since, I’ve used Racket.
At this point, you may be having one of two reactions:
“LOP seems interesting, but I don’t know what I’d do with it.” Sure—I wasn’t necessarily expecting to immediately recruit you into the LOP army. Rather, my goal has been to torque your thinking in a productive way. You’ve now learned about a previously unfamiliar tool. Today, you might not see a use for it. But someday, you’ll confront a problem that exceeds the limits of your current favorite language. On that day, LOP will have its opening.
“OK, you’ve convinced me, but there’s no way I can get LOP or Racket into my workplace.” Jesse Alama’s story of how he introduced his DSL Riposte is a great example of winning over colleagues with LOP (emphasis mine below):
[One can try] to get permission upfront to do something in Racket. That’s less likely to succeed, I think, than just making something great and explaining its benefits ... In my case, that meant talking with co-workers about their work and asking: “How can we model the proposed change in the API and be sure that we’ve really succeeded?” The implied answer being: “Write a Riposte script.” That’s the moment where it becomes clear that [the DSL] I made has real benefits. I don’t even “push” Racket. I just introduce the DSL and show them how it helps them.
Jesse talked more about Riposte at RacketCon 2020.
At the end of Why Racket? Why Lisp?, I said that a Lisp language “offers you the chance to discover your potential as a programmer and a thinker, and thereby raise your expectations for what you can accomplish”.
LOP offers a similar opportunity: to raise our expectations for what programming languages can do for us. Languages are not black boxes. They are interfaces that we can design. In so doing, we open up new possibilities for what we can accomplish with programs.
If you can find a better programming technique, use it. Now that I have LOP & Racket, I’m never going back.
Beautiful Racket by me, Matthew Butterick. The online book that you’re currently visiting. But you’re in the appendix. The main part of the book is a progressive series of LOP tutorials, all relying on Racket and its fantastic macro system.
Language-Oriented Programming in Racket: A Cultural Anthropology by Jesse Alama. An imposing title, but inside is a friendly and readable set of interviews that Jesse has conducted with numerous Racketeers (me included), with contrasting perspectives on how LOP fits into our work.
Creating Languages in Racket by Matthew Flatt. Matthew is the lead architect of Racket (and wrote the foreword for this book). This brief article nicely demonstrates the increasing levels of sophistication in DSL design, using a clever game-creation DSL.
More examples of Racket-implemented DSLs, and even more.