In Progress
Unit 1, Lesson 21
In Progress

Def Return Value

Since Ruby 2.1, method definition statements have returned the symbolic name of the method being defined.

In this episode, you’ll learn how you can use this feature to make your code both more concise and more intention revealing.

Video transcript & code

In Episode #219, we met the Adamantium gem. It enabled us to easily construct immutable value objects. One of the nifty features of Adamantium that is the ability to use the memoize macro to ensure that a method's return value is only calculated once.

require "./quantities"
require "adamantium"

class Dimension
  attr_reader :name
  def initialize(name)
    @name = name
  end

  def hash
    puts "called: #{self}#hash"
    super
  end
end

class Measurement
  include Adamantium
  attr_reader :dimension, :quantity
  def initialize(dimension, quantity)
    @dimension = dimension
    @quantity  = quantity
  end

  def hash
    [dimension, quantity, self.class].hash
  end
  memoize :hash
end

Let's put a pin in that for a moment, and talk about defining methods for a minute. In Ruby, every statement is an expression. What this means is that no matter what code we execute, it always returns a value. For some statements this is a no-brainer: if 2 + 2 didn't return 4, the language wouldn't be very useful.

2 + 2                           # => 4

But Ruby also returns values from constructs that have traditionally been treated as pure statements in other imperative languages. For instance, Ruby's if statement also has a return value.

outcome = if rand(6) == 5
  "you win!"
else
  "you lose!"
end

outcome                         # => "you lose!"

Of course, some return values are more useful than others. Just like everything else in Ruby, method definition statements return a value. But for most of Ruby's existence, this value has always been nil. An expression that invariably returns nil is the closest thing Ruby has to a pure statement.

retval = def greet
           puts "hello, world"
         end

retval                          # => nil

In Ruby 2.1, however, this has changed. Method definitions now return a symbol, which is the name of the method. So if we define a method named greet, the return value of the definition is the symbol :greet.

retval = def greet
           puts "hello, world"
         end

retval                          # => :greet

This might not seem like a very interesting change. But it has some repercussions for code style.

Prior to Ruby 2.1, if we wanted to make a single method private without affecting the visibility of any other methods, we'd do it like this: first define the method, then follow up the method definition with private and the symbolic name of the method.

class Foo
  def sekrit
    # ...
  end
  private :sekrit
end

Now that Ruby returns symbolic names for methods from their definitions, we can tighten up this code by putting the private modifier before the method.

class Foo
  private def sekrit
    # ...
  end
end

This is not only more concise, it's also more intention-revealing. Before, the important fact that the method was private was put off as an easy-to-miss afterthought. Now it's right at the beginning of the definition, much like we'd find in other object-oriented languages.

Now let's return to Adamantium and the memoize macro. You can probably already see where I'm going with this. In a Ruby 2.1 project, we can move the call to memoize in front of the method to be memoized.

class Measurement
  # ...  
  memoize def hash
    [dimension, quantity, self.class].hash
  end
end

Since memoization should, in theory, have no externally visible effect on a method's operation, it is perhaps less important from an intention-revealing perspective to move this method modifier to the front of the definition. But it's still shorter. And it if we ever need to move this method, we'll be less likely to accidentally leave the memoize call behind.

And that's all for today. Happy hacking!

Responses