I just squashed a bug that had me scratching my head for at least a good half-hour or so involving a class constant that kept on getting changed. Here’s the setup (anonymized so that I’m not exposing gooey proprietary secrets):

class WebTransaction
 
  # Base URL for transaction web service
  SERVICE_URL = "http://accountingservice.com/"
 
  def transaction_url
    url = SERVICE_URL
    url << "VAL1=foo"
    url << "&VAL2=bar"
    url
  end
 
end

The class is meant to represent a transaction being posted against a rather odd web service that actually uses GET rather than POST for posting transactions (bad web service!). In my actual code, the URL parameters appended to the SERVICE_URL base would be determined on a per-object basis but I’ve simplified it here.

Here’s the punchline: SERVICE_URL was changing! Calls to transaction_url would keep on appending more variables to it. If you’re particularly clever with Ruby, you’ve already figured out exactly why. But if you’re scratching your head like I was, here are some hints:

  • In Ruby, constants aren’t. In fact, they’re really no different from variables except for the fact that Ruby detects that variable names in all-caps are probably supposed to stay constant and warns if you try to assign to them.
  • <<, for strings, will append to the end of the string.
  • Ruby strings are mutable. That is, operations on a string variable will usually be done in place, rather than returning a new string. (Method calls, on the other hand, are a different story!)
  • Assigning a String variable to another String variable will assign the reference. That is, the two variables will be pointing at the same object.

Hit the jump to see the solution to this little mystery…

SERVICE_URL is only directly assigned to when it is declared. But what happens when I assign SERVICE_URL to the local variable in transaction_url, url. They’re pointing to the same object!

Then I go ahead and I start appending things to the end of the string. Ruby has no complaints because I’m not assigning to SERVICE_URL but the object that it is pointing to is being changed! The operation is causing the URL parameters to be appended in place.

And that, friends, is what happens when constants aren’t. Let’s fix this little error:

class WebTransaction
 
  # Base URL for transaction web service
  SERVICE_URL = "http://accountingservice.com/"
 
  def transaction_url
    url = SERVICE_URL.clone
    url << "VAL1=foo"
    url << "&VAL2=bar"
    url
  end
 
end

Now, instead of appending to the string object referenced by SERVICE_URL directly, I’m creating a copy of the object and then appending URL parameters to that.