Last month I delivered a keynote for YOW!
Fantastic conference, wonderful people, excellent approach to an online conference given *waves hands at everything.*
Here’s the pitch for the talk:
- What we can learn from questioning how we categorize things
- How to identify “assumed context,” and why it matters
- Lessons learned from “domain specific” programming languages
Here is a recording of that talk:
If you’re more of the reading type, then below you’ll be pleased to find the transcript and slides for the most recent version. Enjoy!
Hi! I’m Chelsea Troy. I’m a programmer and an educator, and today we’re going to talk about what counts as a programming language.
This is really a talk about assumed context.
Throughout the talk, we’ll explore this theme within the EXEMPLARY domain of programming languages.
We’ll get to what that means. But before we do, I’d like to start this talk the way you would EXPECT a talk about what counts as a programming language to start…
…with a cursory literature review on the titular question: “What counts as a programming language?”
I truly did the bare minimum here, and I googled the question. What you see here on this slide are some of the most inclusive answers that I found among the results of that search.
According to code academy, a programming language comprises the tools we use to write instructions for computers to follow.
We have a similar definition from Wikipedia: a programming language is a formal language comprising a set of instructions that produce various kinds of output.
These both sound like fairly inclusive definitions, right? Especially the latter one: it includes almost anything—that could include even natural languages. But in practice, the discussion about what counts as a programming language is not such an inclusive one. Professional programmers frequently introduce a number of possible heuristics for determining whether something is a programming language or not.
And in having this discussion with professional programmers, I’ve heard a few different questions come up that might help us to determine what counts as a programming language in practice.
First, does it have to be Turing complete? Does it have to be able to solve any of the problems that a Turing machine might be able to solve? Well, in practice, this means it has conditionals that allow for branched logic and so on. I have heard Turing completeness used to rule out stuff like Markdown languages as programming languages, but we will get there later. Let’s look at some of these other questions.
Does it thave to be usable for solving a specific kind of problem? We count the things as programming languages that we use to solve what we categorize as “programming problems.” We often count the development of applications that take input from end users as programming problems, for example. We count algorithmic implementation as a programming problem. When it comes to app layout or design, we usually don’t count those as a programming problems, and we don’t count the tools we use to perform them as programming languages. But then there are the fuzzy cases. Data structure usage and class design—things like, for example, inheritance—often get categorized as programming problems right up until we’re talking about CSS classes and mixins. Those employ inheritance and class design principles, but CSS doesn’t get universal recognition as a programming language.
So we talked about whether a tool has to be usable for solving a specific kind of problem. Does it have to actually be used for solving that problem? For example, when we’re teaching people programming languages, we often do so by, for example, asking folks to implement tic tac toe on the command line. That sort of problem is useful for teaching people how to write in a programming language, but we’re not using the programming language for “programming problems” there—we can also solve “implement tic tac toe” with four lines and a pencil, which doesn’t count as a programming language.
Finally, does it have to have a compiler and/or interpreter? Here’s what’s interesting about this one. At first glance, this one sounds like it would be the strictest one. Well, maybe—but, at its core, what a compiler does, is it takes a representation of data in one format and it transports it to a format that we can use for something else. It turns frontend syntax into an abstract syntax tree—but is it, and HOW is it, fundamentally different from translating one natural language into another, or converting CSV data into JSON format? We don’t think of either of those types of translations as compiling, or even transpiling. But we also don’t have a clear delineation of the differences that doesn’t START with differentiation and then try to backward-justify it.
These boundaries that we want to think of as clear and objective are far fuzzier and wispier than perhaps we’d like.
We’ve talked about some of the general questions that might come up after work around the water cooler about what counts as a programming language.
The more specific questions might include something like this: is a markup language a programming language? Maybe HTML or markdown or XML or JSON, ways that we typically represent data for programming languages to consume.
I won’t claim there’s consensus, because there isn’t, but in general, programmers will say that these things don’t count as programming languages. And when you ask three different programmers for the reason, they’ll give you 4-5 different, and sometimes conflicting reasons. I’m not necessarily disagreeing with the delineation, and I’ll get to why not later, but I just can’t ignore this any longer and I have to state it for the record,
I do find it suspicious that these dubious claims about what counts or doesn’t count as a programming language move and change over time to shadow almost exactly whatever it is that the dominant demographic is willing and able to write at that time.
Once upon a time, programming wasn’t considered an intellectual task. It was considered “women’s work,” and early NASA missions had employed women in a role literally called “computer” to calculate mission trajectories. Women repaired the early ENIAC machine, and although the historical grounds for the claim that Grace Hopper coined the term “bug” are dubious, she is, in fact, of the demographic that would have been clearing moths out of the wires at that time. What a difference fifty years makes, huh? I highly recommend Robin Hauser Reynolds or Mar Hicks’s work for more on that, but it’s a topic for a different talk than this one. So we’ll move on.
How about a query language? Is that a programming language?
Take SQL—that’s a query language that we use for relationally-oriented databases.
Or Cypher—that’s a query language that we use for graph databases.
Once again, no consensus on the question, but generally, progammers tend to be more willing to think of query languages than programming languages compared to markup languages. Once again, though, ask three programmers why, and you get an amalgam of, at best, orthogonal answers.
So let’s take a step back: what are we trying to describe?
Why does it MATTER to us what counts as a programming language?
What are we trying to figure out here?
Because we said “programming languages.” But maybe some context is missing here.
Now by “context,” what I mean is the descriptors that characterize our specific situation.
The place where I grew up is a context.
The city that I live in is a context.
The industry that I work in is a context.
The type of software that I work on is a context.
The language that I learned as I was growing up is a context.
And it’s important for us to recognize those descriptors because they inform and characterize our perspective. Those things about me influence how I see EVERYTHING. And to me, the way that I see things can be difficult to separate from the truth, because it is what I SEE as the truth.
And here’s how that can bite us: when we are unaware of the context that we’re in, we can accidentally mistake our specific case, our perspective, with an imaginary general case that applies to everyone. An imaginary truth.
Before we go back to programming languages, I want to explore this idea, because I get that it’s a little abstract. So we’re going to look at some context-specific examples to make this clear.
So I want you to think about this statement for a second: “Generally, it is better to optimize our code for legibility than for speed.”
How many folks agree with that statement? Show of hands?
GENERALLY, it’s better to optimize our code for legibility than for speed.
You see the word there underlined, GENERALLY. What do we mean by generally?
It turns out, there is context lost here in “generally.” “Generally” here means “in most cases” but it doesn’t answer the question “In which cases.”
GENERALLY, it’s better to optimize our code for legibility than for speed. MAYBE, when we’re writing end user client applications it’s better to optimize for legibility than for speed. MAYBE, when we’re writing open source applications that rely on multiple developers across the globe communicating with each other asynchronously, such that later developers need to be able to understand and maintain the code that the first developer wrote. Maybe, then, it’s better to optimize for legibility than for speed.
Over the past two years, I started implementing compilers and interpreters. For the first one I was following the book Crafting Interpreters by Bob Nystrom. As I went through the beginning of the book, during the lexing portion where we identify tokens in the text of the source code, I came across a giant case statement that switched on all the tokens. And I decided that I could do a better job than that of expressing the hierarchy of tokens in my compiler. So I refactored that code into a series of conditionals, and I thought it read so cleanly, and I felt SO smart. And I reached out to Bob and I asked him, out of curiosity, why is it done with a case statement, when other solutions might be more legible?
And he said he agreed that the tiered conditionals added legibility, but the truth is that when you are writing a programming language, it’s extremely important that the thing be fast. Because you don’t have to worry about the speed of looping over fourteen keys and values in your Ruby app, or your Python app, or your Java app, because the people who wrote the compilers and interpreters of those languages made them fast enough to give you the luxury of optimizing for legibility, because THEY optimized for speed so you don’t have to do it in end user client application development.
Let’s talk about another example of the context hiding in “generally”
GENERALLY, composition is preferable to inheritance.
Have you heard this saying before? What do we mean by it? Once again we have this word, “generally,” that doesn’t do much to help us differentiate the common case from its exceptions.
I teach a class at the University of Chicago called Mobile Software Development; we learn Android and iOS. Both of those frameworks rely heavily on having the client developer inherit from classes provided by the framework. It ends up making those classes tough to subject to automated unit tests because Android and iOS handle their instantiation for you, and you have to use various hacks to instantiate them yourself for testing purposes.You have to go AROUND the framework. Why do it that way, if it makes testing so hard?
Well it turns out, that when you’re trying to make complicated stuff happen on a device the size of my palm, it’s really important to conserve memory. Which means it’s really important to conserve the number of objects in memory at a given time. And it’s a lot for a developer to think about, and a likely accessibility hurdle for client application developers. In order to take some of that load off the client developers, the folks implementing the Android and iOS frameworks wrote that behavior into prefabricated classes with convenient hook methods for client developers to put their own code.
End user application developers have the luxury of preferring composition to inheritance because those framework designers used inheritance to solve problems that we no longer have to think about.
And that brings us to the question, of what does generally mean? And we agreed earlier, that it means “in most cases.”
But which cases?
I understand that when someone is new to learning programming, we use “generally” to keep things simple. We don’t want to expose them to all of the nuances and special use cases right away: that’s an impractically complicated, not to mention intimidating, introduction to the field. No one starts by writing a compiler, or the Android framework, after all.
I agree that it makes sense to start simple.
But when we reach the point in our own technical advancement where we are responsible for teaching other people statements that start with “generally,”
At that point, we are responsible for knowing what that “generally” means. We are responsible to know which cases we are talking about when we say “generally.”
So now let’s return to the topic of programming languages.
There’s that word again: “general.”
General” programming languages, we say, differ from “domain-specific” programming languages like:
Cypher, the aforementioned query language for graph databases, or
Rspec, a domain-specific language for writing automated tests in Ruby, or
Regexes, a domain-specific language for describing patterns within text
(By the way, if you’re interested in regexes, Betsy Haibel has an excellent talk on the design of Regexes and why they are so pervasive that you can find on YouTube. I highly recommend it).
The point is that we have this idea of a general programming language that transcends what we expect from a language that is merely domain-specific.
Here’s the thing, though: I’m not sure what we mean by “general” here.
What if the “general” case is, in fact, specific to a domain, and we only think it’s general instead of domain-specific because we consider its domain to be the default one?
Much like the way that we call a tee shirt a “unisex” tee shirt, even though it’s just a men’s tee shirt that we sell to everyone, and then we have “women’s” tee shirts that drew the short straw and ended up as the special case that rarely gets provided in the conference swag. Really we don’t have a general case there. We have two specific cases, one of which gets treated as the default.
What if we do that with programming languages, too?
So, Bret Victor talks about this on his blog it’s called worrydream, and I recommend taking a look. He talks about the default domain for programming languages. What do we mean when we say a “general” programming language?
Maybe we’re actually talking about a domain-specific programming language, but the default domain is, end user applications where the user can change something.
We’re not talking about static web pages because the programming community has decided that the HTML and CSS we would use to write that aren’t programming languages.
Here’s the thing. This “end user application” domain—maybe it’s not really the default domain. Maybe it’s just the one that has received the most attention.
Now, why is it the one that has received the most attention? That might be a topic for another talk, but for now, let’s entertain the idea that it’s a domain-specific case, and that there are other domain-specific cases, that it would make sense for us as programmers to pay more attention to.
Take Alloy, for example. This is a domain-specific programming language. The domain in this case is structural verification. Alloy helps us check our assumptions about how our domain models will work given the constraints we intend to impose on how they work and interact with one another. It uses a sat solver to show us configurations of our domain models that would satisfy our constraints, and it can specifically identify configurations that we think SHOULDN’T happen but that our constraints ALLOW.
Alloy and another domain-specific programming language TLA+ can be used to identify domain model issues that you otherwise wouldn’t identify as a defect in the system until 17 steps down the line when some complicated data corruption has happened.
But we’re not prone to using Alloy and TLA+. We use general programming languages rather than domain-specific ones. So instead of verifying our domain model ahead of time to eliminate those defects up front, we instead write an app, get 6 months down the road with 60,000 lines of code, find a weird bug that we can’t figure out how to solve, write a ticket to investigate it, put a bunch of angry face emojis in there, come back to it every month or so when the issue resurfaces but never ultimately figure it out.
That’s not better, right?
What if we were prepared to identify those situations in which general programming language wisdom fails us and ask what we can learn from domain-specific solutions like structural verification languages?
Let’s look at another example.
Modelica. There’s a similar programming language called SimulX. These are domain-specific programming languages for modeling physical systems, used frequently by mechanical engineers. A big client is Ford (General Motors) designing cars.
These types of programming languages are extremely effective at helping to determine how complex mechanical systems need to work together.
Getting complex systems to work together is another thing that the general programming language community categorizes as “difficult.” What could we learn about how to perform that task effectively from the designs of those two languages? How could end user application development benefit from importing those lessons into a new domain?
One more example.
My favorite! Cypher. We’ve already talked about Cypher twice, but I keep bringing it up because I think it’s a fantastic query language.
My favorite thing about it is that a query in Cypher often looks like a little diagram of exactly what you’re trying to get out of the database. These round parentheses enclose the models represented in our query, and the square brackets indicate the relationships between those models. The query draws a little picture for you of what you’re trying to do.
And I think that’s not just a cute language feature—it’s significant.
I think it’s significant because, so far in my tenure as a programmer, I have seen at least three attempts to make a diagrammatic development environment that allows developers to write a general programming language by somehow drawing diagrams or pictures of what they want the system to do.
All three of those efforts, that I have seen, failed. And so I wonder, if we’re willing to promote query languages like Cypher to the level of importance that we give general programming languages, what could we learn about how to succeed with the diagrammatic representations of program logic we’ve been trying so hard to make?
The point is that other domains exist, and paying attention to them can make us better at this programming thing.
I want to know what we can learn as engineers from languages that we think of as domain-specific.
I think the only way we can do that is if we recognize them as siblings, rather than subordinate, to so-called “general” programming languages, because it turns out those languages ALSO have their specific domain.
We talked about this question earlier. What separates converting CSVs into JSON format from parsing a high level programming language to an abstract syntax tree?
One operation might be more complicated than the other, maybe, but it’s difficult to come up with an objective answer as to what makes one of these processes subordinate to another.
And even if we COULD do that with confidence, I’m not convinced that it’s a particularly productive conversation to have.
Rather, there may be more value in learning to identify CONTEXTS, and share information ACROSS contexts, than in formalizing our understanding of the differences between domain specific languages in two separate domains.
It doesn’t matter what counts as a programming language. What matters is “Where can we find contexts where there might be transferable knowledge?”
You (and I) swim in a sea of context.
And the thing about context is, it’s very easy to not realize it’s there. To take it as a default, as a given.
But, it’s worth learning to identify, because by identifying it, you can access a deeper and better-connected understanding of programming languages…
…and maybe anything, but I’ll stick to what I know…
…by learning to find the invisible context hidden in general statements.
So how do we do that?
I have a couple of ideas. I’ll share four of them with you.
First of all, I recommend asking yourself a question. What principles or ideas or best practices do you think of as ALWAYS right, or GENERALLY right? What strikes you as right or true?
I recommend pulling out a notebook. Write down 2 or 3 of these. You don’t need more than that for now—2 or 3 will already give you a ton of practice identifying context that you didn’t realize was there.
All right, once you have those written down…
Let’s LOOK for the context in which this is the case.
Look for the water around you: ask “if this thing is GENERALLY true, what do I mean by ‘generally?” If it’s true in MOST CASES, then what, exactly, are those cases where it’s true?”
Once you’ve positively identified cases where that thing is true, it’s time to look for patterns. Suppose you have seen this to be true in X, Y, and Z cases. What was similar about X, Y, and Z? For example, were they all end-user applications? Were they all for a specific market, or a specific type of client? Did they all solve a similar problem? What did they have in common? That’s a really strong clue as to what the context is that most heavily informs your perspective. Write that down. That’s important to keep in mind.
Maybe you can see where we’re going with this, and guess what the next step will be.
Now it’s time to step OUTSIDE that perspective. And for this specific truth, you can do that by looking for cases where this truth isn’t true. What are the exceptions? Under what circumstances might this truth no longer apply? Go looking for applications that DON’T have the thing in common that X, Y, and Z had in common.
I’m asking you to go looking for contexts that are different from yours to better understand why your best practices work in your context.
Nothing demonstrates why something works better than an example of where it doesn’t! So doing this also gives you a better understanding of how your practices work, and when they work.
Finally, and this is the thing I’d love to see us do that’s going to allow us to draw wisdom from Alloy, and TLA+, and Modelica, and SimulX, and Cypher. Go looking for context similar to yours.
What can your tried-and-true approaches contribute to this similar context?
What can you learn from this similar context about what new things to try in your own context?
Because when we learn to humble ourselves and understand our perspective and the fact that it differs from a universal truth, that gives us an opprtunity to capitalize on other perspectives to build a better model of what the truth is and how we can find solutions for the problems that we face.
Learning to see our own context, and understand and compare other contexts, will help us open doors as solutioneers that previously we couldn’t, not only open, but we couldn’t even see.
If you liked this transcript, you might also like:
The talks category on my blog, where you’ll find more transcripts like this one
This talk, which actually isn’t mine; it’s by Hillel Wayne and it’s about esolangs (esoteric programming languages). If you liked “What Counts as a Programming Language,” you’ll love this talk—or, if you’re more of a reader, the companion reference article.
This piece about the pitfalls of data-driven innovation – it’s not super related to this talk, but the audience that likes this talk will get some food for thought out of it, I promise