Dynamic Ducks vs. Static Gophers
Dynamic typed languages have drawbacks that are now widely discussed around the web. After a certain point we really felt the affects the absence of types in Ruby were having on our business, so we decided to move our core logic to Go services. This was my first experience working on a full scale web backend in a statically typed language and it was a great experience for me. I hope I can convince you to make a similar move to benefit your work and your business.
Starting to Explore
The problem, simplified, was this - we had accumulated some code debt in our RoR application, and we wanted to disentangle some of the spaghetti before migrating to Go.
We started by extracting BL to plain Ruby. We did this so we could write classes that capture the high level logic of the system. Naturally, these classes didn’t have Rails magic, so we avoided a major problem that all RoR developers know: Classes that inherit from a Rails class are loaded at runtime with dozens of methods, so you would need to memorize most of the api of a class. But there’s just too much to memorize. Can I be sure a certain method doesn’t access the params global variable under the hood? Can the object be serialized as an HTML form, or is it just a plain struct? The answers were in the code and the Rails documentation, no doubt, but ut the code was hard to deal with. A single Rails object can implement a huge amount of methods, and there’s no automatic way to know what an object implements or not unless you document it.
And as I’ve learned, the best way to document your code is using tests. Simple tests can add context and meaning to what you’re building. However, covering a RoR app with tests might not be as simple as it seems at the start.
A Clash of Kings: Duck Type Testing vs. RoR
Practical Object-Oriented Design has a chapter describing how to write interfaces and tests. If you’re writing Ruby and you haven’t read POODR it’s fine. I forgive you, but I recommend that you read it now. It’s basically the Ruby bible. Most of the tests are pretty straight forward: A certain object is required to implement a certain interface, so you:
- Write a test that checks if your input implements the required interface
- Write a mock that returns the correct types and values that would make sense to pass as input for your tested method
- Run the tested method with these mocks just to make sure the interface of your mock holds
- Add a test that makes sure your mock and the passed input match in interface
It makes sense, but it’s a lot of manual work. Consider the massive API that is added to each class that can connect to a db and also be rendered (to multiple formats). You will never be able to cover all of those with tests. There was a genuine effort to cover some of the functionality that we used., and even if we completed it, we ended up with a lot of test code. And that’s where, to me, everything falls apart for dynamic types.
The codebase is large. Therefore, a testing codebase is created. In dynamic typed languages the testing codebase is also large. So how do we solve it? More tests. It’s an infinite loop!
This realization came to me after I was shown a youtube talk.
Duck type testing is clerical work
In his DBX conference talk, The Future of Programming, Bret Victor told an anecdote about a professor Von Neumann who was furious at one of his students because the student was compiling a piece of code instead of just writing a program in plain binary. Von Neumann said “It was clerical work, and clerical work is for people.” This was the 60’s. Imagine someone writing a program bit by bit in 2019. It seems just reckless to be honest.
We’ve come a long way since writing binary code directly, and the community is mostly in favor of using as many tools as possible in order to make programmers write less code. We’re willing to sacrifice thousands of machine hours in order to safely compile, build, and test a piece of code in order to save a few moments of clerical work of engineers.
And that’s when I realized, I don’t want to write in dynamic languages anymore. Types are simply a tool for us to reason about our own code. It’s not about performance or complying to some ISO. Types are there for me, as a programmer, to declare a set of rules that I want to be enforced, and let the computer validate me instead of me validating myself. Instead of creating tests, I let the compiler check those things for me.
After a while, I was given an awesome opportunity to “own” a backend service. We’re developing most of the services in Go, which has static types and a lot of strict rules. As with most static typed languages, building a Go binary involves a lot of static checks and analysis to pass in order to finish. Go is a good fit for us because the Go and Ruby programming cultures both rely on duck typing, it’s just that Go statically validates it on build time.
Von Neumann got it wrong. Computers should also do clerical work, and leave the humane part of coding to us. If we had to write all the type tests, as we have tried in the ruby system, we would again collide with the immovable rock - it’s not humanly possible to maintain all the duck type tests in our RoR app.
As programmers, I believe we should be the first to accept technological tools that delegate clerical work. One of the first such solutions is using a static typed language. Types have been there since the start of professional programming, and I think I know why.
(Cover Photo by Andrew Wulf on Unsplash)