Now, what do we mean by “hard”? There’s the obvious definition: you have to do a lot of work to succeed. Time and energy have to be invested. Or, it’s a task that requires great skill and talent. These are our common ideas of “hard”, and they’re more what I meant in my original comment, but I want to highlight an alternate definition of “hard”.
A task is hard when there are more ways to screw it up than to do it correctly. If you’re blundering through the world of programming and software development, there are a lot more ways to be wrong than there are to be right. Most of what we do on this site is inventory all the ways in which people can mangle basic tasks. Think about how many CodeSODs work but are also terrible.
When we talk about learning a skill—be it programming, a musical instrument, writing, improvised comedy, etc.—what we’re really talking about is building habits, patterns, and practices that help us do things correctly more often than we do them wrong. And how do we do that? Usually by screwing up a bunch of times. Improvisers call this “getting your reps”, musicians call it “going to the woodshed” (because your playing is so awful you need to be away from the house so as not to disturb your family). As the old saying goes, “An expert is someone who has failed more times than a novice has even tried.” The important thing is that you can read up on best practices and listen to experts, but if you want to get good at something hard, you’re going to need to screw up a bunch. We learn best from failures.
Okay, so programming is hard, and we can deal with its inherent difficulty by building skill. But should programming be hard? After all, haven’t we spent the past few decades finding ways to make programming easier?
Well, I don’t want to imply that we should make programming hard just for the sake of making it hard. I don’t want to give the impression that we should all throw out our garbage collectors and our application frameworks and start making hand-crafted artisanal Assembly. Keep in mind how I defined hard: lots of ways to screw up, but only a few ways to succeed. That means difficulty and degrees of freedom are related. The more options and flexibility you have, the harder everything has to be.
Let’s shift gears. I didn’t learn to ride a bike until relatively late compared to my peers. As was standard at the time, my parents bought me a small bike and put training wheels on it. I was just awful at it. I’d fall over even with the training wheels on. I hated it, but for a kid too young to drive in the “yes, you can go out by yourself” era of the 80s, being able to ride a bike was a freedom I desperately wanted.
At some point, my parents decided I was too old for training wheels, so they took them off. “Sink or swim,” my dad said. And it was amazing, because I almost instantly got better. Within a few days, I was riding like I was born to it. Cheerfully running errands down to the corner shop, riding back and forth to school, it was everything I wanted.
Little did I know that training wheels are considered harmful.
To learn to bike, you must solve two problems: the pedaling problem and the balance problem. Training wheels only solve the pedaling problem—that is, the easy one. Learning to balance on a bike is much more difficult, and a “training” tool that eliminates the need to balance is worse than beside the point.
Which brings us to languages like Scratch and Blockly. Now, these are educational “toy” languages with syntax training wheels. You cannot write a syntactically incorrect program in these languages. You simply aren’t permitted by the editor. I am not a child educator, so I can’t speak to how effective it is as an educational tool. But these sorts of visual languages aren’t limited to educational languages. There’s SSIS and Windows Workflow. MacOS has Automator. I still see Pointy-Haired Bosses looking for UML diagrams before anybody writes a line of code.
It’s not just “visual” languages. Think about “simple” languages like JavaScript, PHP, VB. Think about the worst sin Microsoft ever committed: putting a full-featured IDE in their Office suite. In all these cases, the languages attempt to constrain your options, provide faster feedback, and make a “best guess” about what you’re trying to do. They try to steer you around some of the common mistakes by eschewing types, simplifying syntax, and cutting back on language features.
I am not trying to say that these languages are bad. I’m not trying to say that they don’t have a place. I definitely don’t want this to sound like Ivory Tower Elitism. That’s not the point I want to make. What I am saying is that simplifying languages by helping the users solve the easy problems and not the hard ones is a mistake.
Beyond that point, programming needs to be hard. Let’s focus on syntax. I remember in my first C++ class, I spent days—literal days— hunting for a semicolon. That’s a huge hurdle for a beginner, struggling with no progress and unhelpful feedback. Languages that simplify their syntax, that forgive missing semicolons, seem like a pretty natural way to bypass that difficult hump.
Here’s the problem, though: once you understand how to think syntactically, syntax itself isn’t really a bother. There’s a hump, but the line flattens off quickly. Learning a new language syntax ceases to be a challenge. By taking the training wheels off, you’re going to fail more often—and failing is how you learn. You can find more things to do, and more ways to do them, without having to start over—because you built strong habits. More than that, you can’t avoid the importance of syntax. At some point, your application will need to interact with data, and that data will need to be structured. We’re often going to wrap that structure in some kind of syntax.
This is just an illustration of the general problem. Making a hard task easier means giving up flexibility and freedom. Our tools become less expressive, and less powerful, as a result. The users of those tools become less skilled. They’re less able to navigate past the bad solutions because the bad solutions aren’t even presented to them.
Now, if this ended here, it’d be in danger of being a grognard-screed. “My powerful-but-opaque-and-incomprehensible-tooling isn’t the problem, you just need to ‘git gud’.” I emphatically do not want this to be read that way, because I strongly believe that programming and software development need to be accessible. In the wilderness of incorrect solutions, someone with little or no experience should be able muddle through and see progress towards a goal. I don’t want the IT field to be a priesthood of experts who require initiates to undergo arcane rituals before they can be considered “worthy”.
Hard means it’s easy to screw up. Accessible means that there are some sign-posts that well help keep you on the path. These are not mutually exclusive. Off the top of my head, here are a few things that we can keep in mind when designing languages, tools, and tutorials:
- Incremental Complexity: Each option a user has is another option to screw up. You’ve made their lives harder. That doesn’t mean you should take options away, but it does mean that you should present the options gradually. Concepts in your language, tool, or tutorial should stand alone or build on simpler concepts. There are so many designs that miss this.
- Glider Bikes, not Training Wheels: If you’re constraining degrees of freedom, if you’re nerfing your tool to avoid certain kinds of mistakes, you need to understand why. What’s the benefit? What habits do you want your users to build? This goes from things like building a visual syntax to even things like using garbage-collected memory. I’d argue that, by and large, garbage collection is a good thing.
- Fail or Succeed Fast and Obviously: In terms of making programming accessible, one of the greatest enhancements that ever happened was live syntax-checking in our editors and IDEs.
- Visualizations are Good: Right now, I’m trying to wrap my head around TensorFlow, Google’s Neural Network API. I’d be lost without TensorBoard, which takes your dataflow and creates a diagram of it. In general, providing an understanding of a program in multiple ways is good. Code is always truth, but UML diagrams aren’t terrible (until they’re used as a specification instead of a visualization).
- Signpost All The Things!: Signposts aren’t just warnings of danger, they’re also guides. If you want to get from here to there, go this way, not that way. These take the form of clear error messages, excellent tutorials, and useful libraries and functions. An implementation of the Builder pattern isn’t just a great way to design an API, it’s a great way to show someone how to design their own APIs. This also encapsulates the idea of “affordances”: the design of a thing tells you instantly how to use the thing.
- Be Unsurprising: If I’m in the unknown, I don’t know if I’m doing the right or the wrong thing. And if there are surprises in store, I’m going to get even more confused. A surprise is anything that’s different, and I don’t like things that are different.
Also, don’t lie. Programming is hard. That might be a “discouraging” label, but it’s an accurate label. What’s more discouraging, going: “This is hard, but here’s how you can get started,” or “This is easy (but it’s actually hard and you’re going to fail more often than you succeed)”?
Phew. This turned into a much longer piece than I imagined it would be. As developers, we’re constantly building tools for others to use: other developers, our end users, and maybe even aspiring developers. Understanding what makes something hard, and why it’s sometimes good for things to be hard, is important. Recognizing that we can deal with difficult problems by building skills, by practicing and honing our craft, means that we also have to recognize the value of being skilled.
by Remy Porter in Soapbox