A Language for Humans


A Language for Humans: Porting a Compiler to Go

Time for some systems programming fun! DestroyAllSoftware produces high quality screencasts that teach how to create fundamental software tools. I took advantage last week of the free sample that Gary gives out - his compilers screencast.

I loved the compilers class I took a couple of semesters ago. It was a fast paced class, and I didn’t feel like I completely understood many of the topics. The compilers screencast seemed like a good place to start to brush up on some of the concepts I’d missed.

Changing it up

The compilers screencast used Ruby to implement the simple compiler. I didn’t want to copy the code line by line, so I decided to switch the languages up and write in Go. Go has become one of my top 2 languages alongside JavaScript. It’s a quirky language for sure, but once you start using it, you will get used to its eccentricities. I wanted to take this small project as an opportunity to learn more about the Go. I hadn’t written a project of my own in Go before, so I knew that there was more to explore.

Why are you like this?

As I mentioned, Go has several interesting perspectives on language design. The nature of this project meant that I came up against some of these design choices made by the Go team. As I followed along, I was in essence porting the compiler that Gary had written in Ruby into Go. Ruby’s biggest difference from Go is its dynamic typing system. This means that you don’t specify types for variables, parameters, or functions. Instead, Ruby will determine at runtime what types are being used. The interpreter does its best to match types for you behind the scenes. The result is a language that is fast to write and looks an awful lot like English. These characteristics make it ideal for Gary’s format. The video is about teaching high-level compiler concepts with in about 30 minutes. Getting caught up in details about types or other rules isn’t the aim here. A dynamically typed language like Ruby is great for situations where the goal is to get an idea into code as quickly as possible.

Go, of course, is statically typed. You must specify types for pretty much everything in Go. You won’t be able to compile your program if, for instance, you attempt to return an integer from a function that returns a string. While Ruby or another language would either gloss over this distinction, or make the conversion for you, Go expects you to take care of it yourself. This difference made following along in Go difficult at the outset. Gary would often keep rolling onto the next topic, leaving me behind. When he would keep going, I would have to pause the video to reflect on what types I was using. I would often ask myself, “Why on Earth would someone want to write code like this way?” I felt like my productivity was slashed in half when compared to writing JavaScript (my main language). It’s 2019! Why not let the computer deal with dotting all the is and crossing all the ts?

On top of having a much stricter type system than Ruby or JavaScript, Go also has other design features that stuck in my craw from time to time. One of the classic ones that many Go developers complain about is the unused var/import rule. Go actually won’t let you compile if your code contains any unused variables or imports. While there are good tools to help with these issues, it can still be frustrating to deal with code that won’t compile for an innocuous reason. As you can imagine, following along with a fast paced tutorial aggravates those frustrations. Another rule that often tripped me up was the commenting rule for exported values. To export a value or function from a Go package, you make the identifier start with an Uppercase letter. The linter expects you to comment every single exported value - you can’t compile without the comment. So

func MyCoolFunc() {
  fmt.Println("Cool stuff!😎")
}

won’t compile. You have to add a comment before the function, and it has to start with the exported identifier!

// MyCoolFunc does cool stuff!
func MyCoolFunc() {
  fmt.Println("Cool stuff!😎")
}

Don’t try to be smart either. The linter will yell if you try to save some keystrokes by simply commenting the identifier’s name:

// MyCoolFunc
func MyCoolFunc() {
  fmt.Println("Cool stuff!😎")
}

results in:

$ golint
$ main.go:5:1: comment on exported function MyCoolFunc should be of the form "MyCoolFunc ..."

After a while, this really started to drive me up the wall. I kept running into more and more of these infuriating language features! There’s no default values for function parameters in Go. Also, no function overloading. Want a function to accept two different sets of arguments and have fundamentally the same functionality? You’ve got to write a new one with a different name. Little things like this started piling up, and soon I was worn out by all the extra naggage that Go seemed to be piling on.

Aha!

While I was working on this project, I was reading up on the various Go features that were bothering me so much. Everywhere I saw talking about how this design was simply better, without going much into detail about why it was better. The reasoning didn’t really click until I remembered something about the origin of Go, as well as an interesting observation about the nature of software.

Go was designed for tackling some of Google’s heaviest workloads. They needed a language that could be, above all things, resilient. This wasn’t going to be a language for implementing elegant machine learning algorithms or mathematical functions. The code written in this language was going to be the backbone of many of Google’s most used services, code that needed to be quick but also easy to maintain.

As I was chewing on Go’s origins, I was struck by an observation I’d read earlier:

80% of the lifetime cost of a piece of software goes to maintenance.

And that’s when it clicked!

Go is focused on making it easy to write battle-hardened code that’s in it for the long haul. The features that had been bugging me - mandatory comments, explicit type management, lack of “shortcuts” like overloading or default parameter values - were there to make it easier to write long-term code. If you’re trying to bang a project out in 30 minutes for a video, then Go isn’t the move. If you’re trying to write a mission-critical service that’s going to be in production for years, however, you’re going to want it to be as easy to maintain as possible. As we all know, code is for humans, not computers. The Ruby that I was looking at in the screencast looked much closer to English than my Go code did. But I realized that it was actually easier to follow along with the logic of what the Go was doing. By forcing you to be explicit about types, or making you comment parts of your code that other packages are going to see, Go is actually helping you write maintainable code. Overloading is a pretty cool feature, and definitely has its uses. It can also lead to confusion, especially if you’re not the original developer. By forcing you to create a new function with a new name, Go eliminates the ambiguity that overloading introduces. I realized that I’d inadvertently reaped benefits from porting this code to a stricter language. Since I had to think about things like types, I was forced to think more deeply about how the code itself worked. I had a better grasp on the concepts that I was learning because I had to think in Go.

Here for a long time - and a good time

Lest my description of Go make it sound like it’s a complete pain to work with, I want to be clear - it’s very enjoyable. Static analysis tools are the bomb! Even in a dynamically typed language, you’re going to need to know something’s type eventually. When that time comes, it may be very difficult to determine that type. vim-go makes it very easy to take advantage of all the static analysis tools that Go has to offer. You can pull up a type’s definition with a single command. You can also inspect a variable or function to find what type it is and its components if it’s a struct type. And documentation is at your fingertips, even for your own code, using godoc. go vet will double check your code for errors before you compile, so you don’t get frustrated after waiting for your code to build. Once you’re in Go’s world, it’s hard to leave it!


This little project helped me understand compilers, but I’m most gratified by what it taught me about Go, and about writing good code in general. Many of the concepts that Go pushes are achievable in other languages! Unfortunately, you won’t have the language itself reminding you of what to do. In general, we should all try to write our code with a little more perspective, with a longer glance towards the future. The next person in your job - or even just future you! - will thank you for that perspective.

Cheers,

Jahz.