DEV Community

Cover image for Expressive Ruby and Rails: Communicate Effectively With Your Code
Daniel for AppSignal

Posted on • Originally published at blog.appsignal.com

Expressive Ruby and Rails: Communicate Effectively With Your Code

Ruby is an expressive language. This is no accident; Matz very consciously designed Ruby as an intuitive language to more or less read like English. It's safe to say that he succeeded. Methods are named very carefully, and do what they say they do; they also tend to have inverse methods which do the opposite.

In this post, we'll look at why expressive code is important and its impact on your productivity as a developer. Then, we'll explore how to best use some of Ruby's methods.

Let's get started!

The Importance of Expressiveness in Ruby

Expressive code goes beyond readability ("What does this code do?"), and clearly communicates intent: it clues us into the developer's considerations and concerns, and the edge cases they were accounting for. It makes clear not just what the code does, but why it was written how it was.

Why is this important? It saves us from spending a lot of time familiarizing ourselves with the context of the code we're working on, clues us in to what we should be looking out for when we modify it, and keeps us from potentially bulldozing carefully-coded functionality and measures meant to safeguard our application.

Ruby takes to heart the principle that good code should be self-documenting; it largely obviates the need for comments. This is likely one of the reasons we often associate commented code in Ruby with bad code: because, in many cases, it is. Working with external APIs or unfamiliar gems is one thing, but generally, for most of our core functionality, writing idiomatic Ruby means we don't need to write (or read) comments. Usually, any time I find myself with comments all over the place, it's a sign I need to refactor.

Ultimately, expressive code saves us time, and our company money.

The Impact of Expressive Coding on Productivity

What problems do we face if we aren't conscientious about how we write and structure our code? The answer is at once straightforward and easy to forget in the moment. The most obvious issue — a lack of clarity — in addition to being its own problem, can lead to a host of other headaches:

Opacity

In addition to being confusing, inexpressive code also turns the thought process of the developer who coded it into something of a black box, rather than a window into their decision-making.

As touched on above, this can force the developer maintaining our code to delve into the implementation in greater detail than they should to get context. This only serves to derail us from our task at hand, and makes our lives harder.

Loss of Trust

When a particular feature or area lacks expressiveness, it can be frustrating, but it is also an opportunity to improve our app. But when a lack of expressiveness is a pattern across our entire codebase, we lose faith in our code as a reliable source of truth to communicate the potential values it may be handling.

This inevitably leads to constant defensive programming, which can serve to further muddy the waters. We may wind up accounting for potential values that aren't even real possibilities, magnifying the existing problem and further eroding trust. In addition, we might add verbosity to our code, and send potentially misleading signals to other developers (and even our future selves) about what we're accounting for and trying to accomplish.

On the other hand, a lack of trust can lead to bad assumptions when the right method is actually being used. Consider a scenario where someone has just spent time cleaning up a section of code and spots something like if @user.address.present?.

Rather than stopping and thinking: "hey, there must be a reason present? was used," they might think: "hey, this particular field is optionally set by the user... so either they set it and it's a string, or it wasn't set, so it's nil." So they change it to if @user.address. Except, what happens when the user has a value set, and then later removes it and submits the form? We'll now have an empty string (which now, without present?, evaluates as truthy).

Type Issues

Constantly having to check for potential data types and the possibility of nil values can be genuinely exhausting and really slow us down. It not only wastes time by forcing us to work out the possible data types of inputs, but it can also break our focus and flow, and add unnecessary cognitive overhead.

In some ways, we can think of Ruby's expressiveness as an answer to a lack of static typing. By employing clear variable names and self-explanatory methods that do what they say in a fixed, predictable manner, we can automatically infer the type (or potential types) of data we're working on in any given line with little effort.

Maintenance and Extensibility Issues

Inexpressive code can be a nightmare to maintain, and it often leads to tangled code, quickly becoming tough to modify without breaking something. It's even harder to extend with new features and functionality, and can become a bug-prone black box that's slow and challenging to develop for.

Hurting the Bottom Line

We pile up technical debt, which costs us increasingly painful amounts of time, loses our business money, and potentially hurts our reputation if this leads to missed deadlines or buggy releases.

On top of all this, the less expressive our code is, the more painful turnover becomes; onboarding new developers takes longer, as it will be a while before they're productive and know the codebase, and losing developers who do know it has a greater impact. And, of course, even when they're up and running, they and everyone else working on the application will simply be less productive than they could be.

Now, let's focus on how we should best use methods in Ruby and Rails to avoid some of these issues.

Comparing Similar Ruby and Rails Methods

As we touched on early in this post, Ruby and Rails come together to offer us a wide variety of methods that accomplish an array of everyday tasks and answer basic questions we might need to ask as programmers. At first glance, there might appear to be a lot of overlap, and we may think that many of these methods can simply be used interchangeably without issue.

Of course, as we know, these methods were named very carefully. Both Ruby and Rails were designed with close attention to what these methods do and don't do.

As a result, methods (excluding aliases) each behave slightly differently, with different intended purposes. Their correct use can help us provide specific context to clue in anyone reading our code to what's going on. Conversely, failing to do so can have the opposite effect. Let's take a look at some methods that sometimes get used interchangeably, to our detriment — starting with nil?, empty?, blank?, and none?.

nil?

This method does exactly what you'd expect: it tells us whether a value is nil. I generally like to use it to avoid doing things like !@user, but it can also indicate that a value is not a boolean, if that's not already clear. While it's difficult to misuse, it's easy to use another, technically valid but misleading method (such as blank?) in its place. Don't do this!

If a value can only be nil or truthy, nil? should always be used. Doing otherwise can mislead anyone modifying the code and potentially lead them to code for non-existent edge cases, which is both a waste of time, and adds unnecessary complexity and verbosity.

empty? (Nearly Opposite Method: any?)

empty? is useful when you want to check whether a collection array, string, hash, or other type of collection contains no elements. It's a straightforward and intuitive method that returns true if the collection contains no elements, and false otherwise.

For example, "".empty? and [].empty? both return true, because the string and the array do not contain any elements. Similarly, {}.empty? returns true because the hash does not contain any key-value pairs.

When you use empty?, you're communicating to other developers that the object you're checking should be a string or collection of some sort, and you're interested in whether it contains anything.

blank? (Opposite Method: present?)

blank? is often used to check if a string is empty. However, as you're likely aware, it checks for more than just empty strings; it will return true if a value is nil, false, or an empty array or hash. Essentially, it asks and answers !object || object.empty?, so we expect that the potential values are probably either nil, or an empty string, array, or hash — this is what you're communicating to other developers when you use this.

It is often used in place of empty? or nil? when it really shouldn't be.

none? (Opposite Method: all?)

This asks whether an array or hash is empty or only contains falsy values, and it's not too often misused. Sometimes, though, we might see someone use blank? instead.

This is a mistake. In addition to the fact that blank? asks: "is this object falsy or empty?", none? asks not only: "is this array or hash empty?", but also: "...or does this array have no truthy values?". [false, nil].blank?, for example, will return false, but [false, nil].none? will return true (likewise, any? would return false).

So not only have we introduced ambiguity, we've introduced a bug waiting to happen — and sadly, this one is not a feature.

size Vs. length

size

Put simply: size is for collections (ActiveRecord Collections, arrays), and should almost always be used for them. It never makes sense to call on a string.

length

length is generally for strings, and except in specific cases, shouldn't be used for collections (and probably never for arrays).

boolean? Methods in Ruby

Every Ruby boolean? method answers a yes or no question: "Is this value truthy or not?". Most Rails developers are aware of this and generally use the question mark to good effect, adding it to the end of the boolean methods that they write. But many may not be aware that Rails itself does, too.

Any Rails object automatically responds to a column with a ? on the end. So, a verified column on your user table can be called with @user.verified?. Setting up a standard for your team to only use these methods for boolean columns can help provide even more context for other (especially new) team members. They won't have to check the schema to see if verified is maybe a timestamp or even a string rather than a boolean.

And let's not forget the methods we write ourselves; generally speaking, any method we write that answers a yes-or-no question should return a boolean method, and end with a question mark.

For example, say we have a method that tells us whether or not a value is outside a particular limit. We can define something like this:

class Vendor
  def use_exceeds_limit?
    get_time_slot_use_count(1.month) > time_slot_tier.limit
  end
Enter fullscreen mode Exit fullscreen mode

We can then conditionally email to let users who have exceeded the limit know they'll be charged at a higher rate for the month:

class DailyLimitCheckJob < ApplicationJob
  def perform(vendor)
    VendorMailer.alert_vendor_limit_exceeded(vendor) if vendor.use_exceeds_limit?
  end
end
Enter fullscreen mode Exit fullscreen mode

So, great, we know that methods that end in a ? return a boolean. But what about other methods?

Note the method get_time_slot_use_count. It may be slightly more explicit than it needs to be, but it also tells us exactly what we're getting: an Integer. If it were get_time_slot_usage, we might wonder if it wasn't a Float, or even an ActiveSupport::Duration (with a return value like 173.hours). Being explicit helps us know what we'll get without looking at the actual implementation elsewhere in the Vendor class.

So, we can also apply this principle elsewhere: we probably don't need it for strings like name or title, but if we're assigning variables to dynamically hold parts of a query, we probably do want to name it something like query_string, rather than query.

The same is true of arrays and hashes: generally, a plural name (say, products) indicates that we're dealing with a collection. We often don't need to explicitly specify "hash", either; consider pagination_options. We know options in contexts like these are typically hashes. But sometimes we have products as a key-value pair collection; we should probably name this products_hash.

Further Resources

For those seeking a deeper dive into the world of Ruby, a number of resources are available.

Check out the books The Ruby Programming Language and Ruby Best Practices, the official Ruby docs, and Rails Guides.

Wrapping Up

In this post, we've walked through the pitfalls of inexpressive code, including a lack of clarity, a decrease in trust, and other problems. In contrast, well-crafted, expressive code can help avoid issues like technical debt, make our lives easier, and save money.

The design philosophy of Ruby prioritizes expressiveness and intuitiveness. Properly leveraging the tools Ruby provides to communicate your intent can eliminate the need for extensive comments and lead to substantial savings in both time and resources.

To fully unlock the power Ruby and Rails affords us, it's critical to be conscientious in your variable and method naming, and to use the right method for the job. Expressive code makes you — and everyone around you — more productive.

Happy coding!

P.S. If you'd like to read Ruby Magic posts as soon as they get off the press, subscribe to our Ruby Magic newsletter and never miss a single post!

Top comments (0)