Tithe.ly Engineering
Greater readability using Ruby’s Variable-like Method Syntax
Prior to joining Tithe.ly, PHP had been my bread and butter for 14 years. Having solved a variety of problems with it, I came to see it as a capable language. Though it was capable, however, I never found it to be particularly readable. I strove to write readable code, but the language didn’t seem to be doing much to help.
Coming to Ruby from PHP, I noticed there was a much larger emphasis on readability. Ruby code I wrote in the style of PHP would come up in Code Review with suggestions that greatly clarified its intent. Time and again, I have discovered Ruby lets me express ideas in code moreso than many other languages I have used.
I would like to share a couple of examples which show how Ruby’s method-invocation syntax facilitates readability.
Using variable assignment syntax for method calls
In PHP, there’s a syntactic distinction between variables and methods.
// Setting an instance variable
$foo->bar = 42;
// Calling a method
$foo->baz();
This is simple enough, but directly setting instance variables breaks encapsulation, so in PHP it’s common to define getters and setters for instance variables.
class Foo {
private $bar;
public function getBar() {
return $this->bar;
}
public function setBar($value) {
$this->bar = $value;
}
}
$foo = new Foo();
$foo->setBar(42); // assign 42 to the bar property
This is a good practice, but it means we have to use method call syntax for what is essentially an assignment.
One thing it buys us is the ability to modify the behavior of setting the bar
property without needing to change the call sites. This is a win for maintainability, but it’s a loss for readability owing to the extra verbosity of method call syntax, especially with the get/set prefixes.
Ruby, on the other hand, supports a method call syntax that looks like variable assignment.
class Foo
attr_reader :bar
# explicitly defined for pedagogical purposes
def bar=(value)
@bar = value
end
end
foo = Foo.new
foo.bar = 42 # assign 42 to the bar property
What looks like an instance variable assignment is actually a method call. The foo.bar = 42
syntax is syntactic sugar for foo.bar=(42)
. We are sending the bar=
message to the foo
object. This is useful because it allows us to use the simpler syntax of variable assignment while being able to easily add behavior later on to the bar=
method without needing to change the call sites. This is a win for both readability and maintainability.
Using methods instead of local variables
When writing a method, we often need to get some pieces of data to process, and the getting of that data can make the method harder to read. For example, consider the log
method:
def log
author = current_user || "Anonymous"
date = backdate || postdate
commit("Processed entry #{id} with date #{date} by #{author}", date, author)
end
The heart of the method is the call to commit
, but the lines where we set the author
and date
variables obscures this. To make it clearer, we might add a blank line before the call to commit
; however, let’s explore an alternative. Notice that we are referencing the current_user
, backdate
, postdate
and id
methods as if they were variables that we didn’t have to set. What happens if we take this further and do the same thing with author
and date
?
def log
commit("Processed entry #{id} with date #{date} by #{author}", date, author)
end
def date
backdate || postdate
end
def author
current_user || "Anonymous"
end
This is a small change, but it makes the main logic of the log
method more prominent.
Since Ruby does not require parentheses for method calls, calls to date
and author
look like reading the value of a variable. And since methods can have any arbitrary logic in them, we can hide a lot of the complexity involved in obtaining the value, keeping it outside of methods that need the value but would become harder to read if they had to calculate it themselves.
With this approach, there are a couple of caveats related to the potential of making multiple calls to the same method.
- getting the data might be expensive
- getting the data might return different values in between calls
As an example of the second caveat, consider the following code:
def log
commit("Processed entry #{id} with date #{date} by #{author}", date, author)
end
def date
Date.today # <=== ERROR: date could change in between calls
end
def author
current_user || "Anonymous"
end
Here, the call to date
could return different values. We are calling it twice in the log
method, so if the first call happens before midnight and the second call happens after midnight, we would get different values for date
in the arguments being sent to commit
.
In this case, we would want to use a cache: either a regular variable or through memoizing the method.
A note on value In most cases, the identity of the object returned does not need to be the same in between calls. It just needs to always respond in the same way to the messages we send to it. For example, if we call
date
and it returns aDate
object, we don’t care if it’s the sameDate
object every time, as long as it represents the same date. An exception is if your code checks for object identity withobject_id
orequal?
.
The upshot is that this technique works best when it’s cheap to get the data and the data doesn’t change in between calls.
Reflecting on this a bit, I am starting to see regular variables as just caches: you save time by not having to recalculate the value every time you need it, and you can trust that the value won’t change in between calls (at least not due to making repeated calls to get it). The drawback is that adding these caches can obscure the main logic of the method, so it’s a trade-off.
Conclusion
Ruby has some nice syntactic sugar that makes it easier to write and read code. The ability to use variable assignment syntax for method calls and to use methods instead of local variables can make our code more readable and maintainable.