Friday, January 15, 2010

How I learned to stop worrying about types and typos in ruby.

I had a bit of an "aha!" moment earlier while working on my ES project that helped me get over one of the major sticking points I had with dynamic languages like ruby.  But I'm going to make you read through some background before I actually share the moment itself.  If you don't like it, well, too bad.  You'll just have to skip a paragraph or three.


Up until a couple years ago the only languages I did any meaningful work in were C++ and Java.  I found that I really liked Java's approach to OOP, and I got used to the explicit typing of everything.  Most of the errors made in the code could be found by the compiler due to the strict typing requirements (broke down a little bit with collections, but that got better in Java 5).

Then I got pulled into a project that used Flash.  I had to deal with both actionscript 2 and server-side actionscript (essentially just javascript but with some different built-in objects).  AS2 has some typing support with interfaces and classes, but those are just a compile-time concept and it's very easy to lose the type information when you start putting things in Arrays.  I ended up doing a lot of work in these dynamic languages, and the lack of type safety really bugged me.  Especially in the SSAS code where types didn't exist at all.  It really aggravated me whenever I was writing code that had to be passed in some kind of callback (like adding listeners to something).  In java or c++ I could define an actual type that included the method signature and require that.  But in SSAS I couldn't, and that seemed like a really big problem.  There was also the issue where the compiler couldn't really find typos, and a few times in the SSAS code a typo in the code went unnoticed until something broke in production.

Over time I started getting used to the dynamic approach and found a few pieces I really liked.  I was still put off by the loss of the safety net Java provided with its compile-time type checking, but I started to notice that I could be a lot more expressive with my code, especially once I discovered closures.

By the time I started learning Ruby I had mostly gotten over my discomfort with dynamic languages, but I still worried about typos causing problems.  Then some experiences I had working on ES and a snarky comment (link goes to question the comment is on) on stackoverflow clicked together and made me realize the typo problem was already being handled, just not by the compiler.

Typos and other errors that dynamic language compilers/parsers won't find are problems because they only manifest at runtime and only when the actual code path where the typo exists in is hit.  Unless your QA people (you have QA people, right?) happen to hit that case it probably won't be found.  If it's a rare case that isn't easily triggered by the testers then it's even worse.

Unless you have unit tests.  If your code is covered by unit tests then you should find the typos and other errors right away.  Mistype a variable name?  Test fails with a NameError.  Pass the wrong type?  You should get an error when the code tries accessing some method that doesn't exist.

I'm trying to have full code coverage in ES, and it's been very helpful finding these.  If one of my methods has multiple code paths (for example: handling for a valid move request and raising error for invalid move request) then I'll write a test for each of those paths.  Since each path is triggered when I run the tests the errors in the code are found immediately.

Conceptually the unit tests have become the compiler, at least in the context of checking for errors.  After I've written some ruby code I'm triggering the tests just like I would have triggered the compiler when writing java code.  But it's even better than the compiler.  Not only does it help to find typos, it also finds problems in the logic itself, which the compiler never could.  It's more work to have, as you have to write the tests, but the benefit of having the tests far outweighs the cost of writing them.  You end up finding both code and logic errors much faster, along with a number of other benefits like making it easier to verify any refactoring you did didn't break anything.

PS: On a completely unrelated note - I bought my first domain! This blog is now at http://blog.aherrman.com (or just http://aherrman.com) instead of aherrman.blogspot.com)



Unit tests work as a compiler replacement for finding typos and other code errors that dynamic languages like ruby don't normally fail on.

2 comments:

  1. When I learned ML and functional programming, one of the things that was repeated often was that the advantage of that sort of (bondage-level) strong typing is that, hypothetically, if the program compiled then only algorithmic flaws would result in errors. But the great thing about tests is that they validate the algorithm also; if you're writing them, you can cover both sides of the coin and the advantage of statically-analyzed typing falls by the wayside.

    Of course, the other advantage of static typing is that it's automatically computable. I wonder if any more aspects of test writing could be automated than what I already have?

    ReplyDelete
  2. Functional programming has an interesting approach (at least from what I remember from SML). You never really had to define types for your variables, but everything was strongly typed. From what I remember, the system would parse all the code from the entry point and make sure the types being passed around were always consistent. Even if it didn't know the actual type it would still make sure the code used everything consistently.

    It was kind of cool because even if the type itself wasn't known it would still make sure that once a type was provided everything would work out. Sadly, I don't think that kind of processing could be done for languages like Ruby, since the types can actually be modified at runtime.

    ReplyDelete