Saturday, June 30, 2007

I don't like side-effect programming. (Subtitle: Don't program ruby like it's C)

I know this will be the kind of comment that starts fights and ends friendships - but I don't like to see side-effect programming in Ruby.

This isn't to say that I think every method you write in Ruby should be a pure function. I know that many of the qualities that make Ruby useful are available because there are certain contextual rules for how methods are executed - it's one of the reasons I use it.

What I'm talking about is changing the values of variables which originate in a scope outside of your method, or the class to which that method belongs.

One of the primary offenders on my list of annoyances comes down to methods that change the parameters they are passed. In Ruby it's perfectly possible to do this - as the language is crazy with pass by reference. However, in Ruby there is really no reason to do this.

First, a little history. The idea of changing parameters given to a function originates (in the way that most programmers will be familiar with it) because of a limitiation in the C programming language. It comes mainly from the fact that since C is basically a huge macro for assembly it's better to pass a reference to something other than the data structure itself (the reasons for this having to do mainly with speed, and other things which I won't go into here). It also arises from the fact that C was pretty terrible at passing back a function result such as a variable-length array. I know some of you will argue this point, but with the exception of strings (which have a standard way to denote their ending data block) - a variable length array containing objects of an arbitrary structure generally required a 'length' variable to be passed as a parameter - that variable would then be filled in with the length of the received array. Even though at the time this was mainly a limitation specific to C, eventually the stylistic convention would spread all over the place (even into languages where there was no need for it, such as Java(big TM here so SUN doesn't come after me)).

In Ruby, it is rare that you would need to put in a variable as a parameter, and then expect the method to change that parameter - methods don't have limits on what they can return. As well as there no longer being the old c-esque limitation of function returns.

Additionally, there are two other reasons why this could be considered 'bad'. The first reason is that it really smacks in the face of programming by contract (or at least my personal conception of it). Personally, I don't want to contract with methods that change what I give them. Second, instead of just expecting a return value from your method - I now need to know what all the methods inside yours may be doing if I ever REALLY want to understand what's occuring in your code. And if you use nested methods that do this - well - forget it. The utility of such a method to others is mainly negated - because they have to watch your method to see what it's doing to their variables. This really hurts any time you might be wanting to modularize significant portions of your application.

There are a bunch more examples of this (other than changing your parameters) but I'll just leave it with this example for now, as well as a little advice.

A few hard and fast guidelines for avoiding most of the hobgoblins of side-effect programming in ruby:
  1. A method should either return a value, or set multiple instance variables. It's also acceptable to try to set multiple instance variables and then return as your value whether the assignments and other methods you called were successful or not.
  2. Methods shouldn't change parameters they are passed. There are exceptions to this, but usually you need a VERY good reason.
  3. If your method is going to change variables directly, i.e. it's a method that sets multiple field values, etc. - limit those variables to those owned by your method's class.
  4. If a variable doesn't come from the scope of your class or your method, you probably shouldn't be changing it. If you need to manipulate the value in a way that would by default change that variables contents (e.g. in ruby strings are a mutable reference) - make a copy and mess with that one.