Sunday, August 25, 2013

The Big Bad Bang, or, The OTHER Gotcha with Ruby's Bang Methods

   A few people have asked me why certain Ruby methods end in an exclamation mark (!), commonly known in programmer shorthand as "bang".  Examples include upcase! (to get the all-uppercase version of a string) and uniq! (to get the unique elements of an array).  Long story short, the bang means that you should use it with caution.

   Usually this is because it modifies the object passed and returns it, as opposed to returning a modified copy of it.  (In Ruby on Rails and many similar frameworks, this may also be because it will throw an exception if anything goes wrong.  However, we will focus on core Ruby methods.)  I'll show you another reason in a moment, but for now, let's just examine the normal usually-expected behavior.  For instance:
  str = 'foo'
  p str.upcase
  p str
will output FOO and then foo.  While upcase returned the uppercased version of str, it did not modify str.  On the other claw, if we add a bang, doing:
  str = 'foo'
  p str.upcase!
  p str
we get FOO and then FOO again!  In other words, upcase! returned the uppercased version, just as the non-bang version did, but it also uppercased str itself!

   Similarly, if we use uniq:
  arr = [1, 3, 3, 7]
  p arr.uniq
  p arr
we get [1, 3, 7] and then [1, 3, 3, 7], showing again that the non-bang version returned the unique values within arr, but did not modify arr, whereas if we add a bang and do:
  arr = [1, 3, 3, 7]
  p arr.uniq!
  p arr
we get [1, 3, 7] and then [1, 3, 7] again, showing that arr itself was modified this time.

   So far so good.

   But wait!  There's more!  There's another big bad gotcha waiting to getcha!

   Do not depend on the bang versions returning the same value as the non-bang versions!  (Even though that value seems to be the whole point of both functions!)

   In the specific cases above, yes they do.  But let's look at what happens if the variable is already how we want -- in other words, if the string is already all uppercase, or the array already has only unique values.  If we do:
  str = 'FOO'
  p str.upcase!
  p str
then, as expected, since it already fit our needs, str is unchanged.  But look at str.upcase! -- it's nil!

   Let's see what happens in the numeric case.  If we do:
  arr = [1, 3, 7]
  p arr.uniq!
  p arr
then, just as above, arr is unchanged... but arr.uniq! is nil!  How come?

   Long story short, standard Ruby bang methods often return nil if no change was needed.

   Worse yet, even that is not completely consistent.  When using any bang-method that you are not already very familiar with, be sure to RTFM.