Ruby++, or why post-increment is tricky

David Brady mentioned that he’d dropped my name to Uncle Bob, so obviously I had to go watch the video. The whole call—in which David picks Bob’s brain about SOLID, and Bob laughs a lot—is fun and worth watching.

I got into a Twitter discussion with David after watching, about his desire to bring the ++/– pre-increment/post-increment/pre-decrement/post-decrement operators into Ruby. It was a fun conversation, partly because David and I apparently come from similar backgrounds: I too was a C++ language lawyer before I was a Ruby language lawyer. Yes, David, I have well-thumbed copies of bothEffective C++” and “More Effective C++” on my bookshelf, and frankly I’m appalled that more people don’t realize that ++i is usually preferable to i++.

I was going to email David to continue the conversation, but if there’s one thing I’ve learned from blogging it’s that if you have something potentially interesting to say that isn’t in confidence, you might as well blog it. So here we go. In the rest of this post “you” will refer to David.

I’m not philosophically opposed to the addition of increment operators to Ruby; occasionally I still reach for them and remember they aren’t there. I do think they are somewhat less needed in Ruby; but more importantly, I think choosing a useful way to implement them in Ruby is trickier than it might first appear.

There are basically two ways you could add increment/decrement operators to Ruby. The first is to make them a syntactical macro in the vein of the +=, -=, and ||= operators. The second is to make them first-class methods, a la C++. I think both approaches have pitfalls.

Let’s use post-increment as the example. A syntactical macro version would be similar to the way Ruby expands +=:

i = 1

# this:
i += 1

# ...is expanded out to this:
i = i + 1

We can define our own #+ operator on objects, but not #+=, because it is always expanded out to a call to #+ before being called.

As far as I know the reason for this design choice is the way numbers work in Ruby. Numerics, unlike most objects in Ruby, are immutable. For instance, there is no #incr! destructive increment method on Integer, and if we try to add one we run into trouble:

class Integer
  def incr!
    new_value = incr
    # ...er, wait, where do I save the new value?
  end
end

In fact, there is no way to alter a Numeric in place in Ruby. 3 is always three. The only way to “increment a number” is to update the variable to point to a different number. Thus, the implementation of #+= as syntax sugar.

OK, that’s fine, so we implement the ++/-- ops as syntax sugar. But as I understand it, that leaves your primary use case, a Count object, out in the cold. If I understand correctly, you want to be able to make a custom #++ operator on Count that updates the object in-place and returns the previous value:

class Count
  attr_reader :value

  def initialize
    @value = 0
  end

  def post++
    old_value = @value
    @value = @value += 1
    old_value
  end
end

c = Count.new
c.value # => 0
c++      # => 0
c.value # => 1

The problem then becomes that there is no way (that I can see) to map this back to its most common use case: incrementing a simple numeric. Because as we saw, there’s no way to internally update a numeric to a new value.

I will suggest, that while not as concise, simply adding a #post_incr! method to the classes that need it might be a “good enough” solution. I’m curious, though, which approach you had in mind for implementing ++/-- in Ruby.

UPDATE: After thinking about this a bit more, I realized that neither of the approaches above captures the C++ semantics for post-increment. In C++, the increment isn’t performed until the entire statement is executed. This is demonstrated with the following code:

#include 

int main(int argv, char** argc) {
  int n = 1;
  std::cout < < "increment expression: " <<
    ( n++ * 2 ) + ( n * 4 ) + ( n * 8 ) << std::endl;
  std::cout << "after increment: " << n << std::endl;
  return 0;
}

The output of this code is:

increment expression: 14
after increment: 2

The increment doesn’t take effect until the after the first statement is fully evaluated.

It occurs to me that a Ruby-ish way to handle this situation—where you want to increment a count but only after a full statement is executed using the old count—might be to use a block:

count = Count.new(1)
count.advance do |old_value|
  old_value # => 1
end
count.value # => 2

This captures the expected semantics, and in addition gives you a place to cleanly add in (for instance) some kind of transactional behavior later on.

Leave a Reply

Your email address will not be published. Required fields are marked *