Def Return Value
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