In school, I used to pick up new programming languages for fun. I could take my time to learn & explore them in whichever way felt right, without pressure. I implemented for fun and the code rarely ended up in production. By contrast, at work, I pick up a new language because something urgent & important is best done in it and, typically, the time between learning and production deployment is less than a month. Going through this several times over the past two years at Swoop (most recently with R) I think I’ve finally cracked the code on how I can help myself learn a new language in the fastest possible way.
I’ve lost count of how many programming languages I’ve written code in over the years (the most obscure one is probably ROBASIC). I think about programming using meta-patterns that transcend languages. When I encounter a new language, I go through the following phases:
- Curious George. During this phase, which usually lasts only a few hours and involves semi-random exploration of tutorials, reference manuals, blog posts and StackOverflow posts, I get my bearings straight, find analogies between the new language and ones I’m more familiar with and choose my initial tooling.
- Mario Bros. -> Super Mario Bros. In this phase, which usually lasts several days, I try to bring the meta-patterns I’m comfortable working with from familiar environments (the sewers of New York) to the new environment (the Mushroom Kingdom). It involves diving head first into advanced language features and building various utilities that I find lacking in the environment, e.g., debugging tools, all in the context of early prototypes of whatever it is that I need to work on.
- Bull in a china shop. This is the phase where my noble goal of bending the language to the way I solve problems meets the harsh reality of me being a neophyte in its intricacies. The observable attributes of this phase are lower velocity, increased use of expletives and more time on StackOverflow. The amount of time in this phase varies. The “Eureka!” moments are fun but overall it’s a dip in the experience curve.
- Singin’ in the Rain. With newly acquired knowledge and improved language-specific testing/debugging skills, the bull gently transforms into Fred Astaire. Coding is a lot of fun again. It’s time to go to production.
- Obi-Wan Kenobi. Over time, the interaction with the new language improves the meta-patterns I use for problem solving. I tend to use less and simpler code using natural language idioms as opposed to generalized utilities & abstractions. It’s like changing from using the Force to allowing the Force to do things through you. It takes a long time to get here. More often than not, I never do.
In trying to optimize the way I learn languages, I focused on the obvious problem: the bull in the china shop phase. My guess as to the root cause of the problem was simple: I didn’t know what I didn’t know and it did have a tendency to bite me. Here are some examples of the types of advanced language features that have helped me in a big way but not without some very frustrating moments along the way:
- In C++ template meta-programming turned out to be awesomely useful and powerful but rich in undocumented cross-platform compiler bugs at a time when my company was shipping on four different OSes.
- In Java it had to do with dynamic bytecode generation for decorators of untrusted third party code in the days before J2EE was mature.
- In Python a logically simple approach to behavior injection ran into edge cases where stateful decorators didn’t mix well with inheritance. To boot, it crashed the debugger, which made it all the more fun to fix.
- In Ruby metaprogramming for dynamic domain specific language definition my approach ran afoul of the behind-the-scenes trickery of ActiveModel::Callbacks. To prove that Ruby is just like Python in all ways that matter, this was another situation where the debugger crashed liberally.
- In R, whose best and worst quality is that it is designed by statisticians, it was custom environment chaining.
In reflecting on this after my recent experience with R, I felt stuck between a rock and a hard place. If I use advanced language features to gain productivity I risk wasting time debugging bizarre edge cases. If I do not use the types of language features that make it easier to map how I think about problem solving to a particular language I lose productivity. The obvious approach was to look for a way to discover the unknown unknowns sooner but I didn’t have an easy way to do this without creating waste in the agile sense. Even test-driven development didn’t help much as the majority of the problems that cost me the most time reared their ugly head deep into implementation.
What I now think is an optimal approach to learning a new programming language–at least for me personally–required two serendipitous discoveries. The first came because of some work we are doing at Swoop with thoughtbot. I’ve often preached that it’s important to invest in learning the intricacies of whichever testing framework you use so that you are able to express the way you think about testing more naturally in code. I thought I knew how to do this reasonably well but seeing those guys work challenged me to think about testing abstractions in a new way. The second serendipitous discovery came as I was trying to extend the testthat R package with some new primitives for the types of tests I wanted to write. I ended up hitting several unknown unknowns in that process but, perhaps due to the natural way tests isolate state & dependencies, it happened early and in ways where a solution or a workaround was easy to discover.
With the help of serendipity and reading the code of several testing frameworks, I’ve come to the hypothesis that either reading and extending existing test frameworks or building new ones (perhaps as a tool to go along with early prototype work) is the best way for me to both efficiently learn the advanced features of a new programming language and to efficiently discover in practice the unknown unknowns that will likely bite me. I expect the same to be true for other experienced developers but I would not recommend this approach for developers who are not at ease with abstract & pattern-driven problem solving.
I’m going to test this hypothesis with Lua, which I started learning tonight because of some Redis scripting work we are exploring. Here is the result of my first hour with Lua. I’m not trying to re-create TDD frameworks such as telescope or busted. (In fact, I’ll probably transition to busted as it is the most RSpec-like thing in the Lua universe and I find contextual test writing to be far easier and more enjoyable than xUnit-style test writing.) This was simply the fastest way for me to dive into learning the language as opposed to using the language. This approach also yielded the first unknown unknown: the curious fact that the built-in mechanism for determining the length of a table (Lua type that doubles as a array and hash/dictionary) seems to only work for arrays (not hashes) without nil values. I’m sure every Lua programmer learns this sooner rather than later but I also found no mention of this in the handful of tutorials & blogs posts I explored during my Curious George phase or the section on tables in the definitive Programming in Lua. I am left with the hope that this time around my bull in the china shop phase will be shorter.