Five Things to Avoid in Ruby | AppSignal Blog (2024)

As a contract software developer, I am exposed to oodles of Ruby code.Some code is readable, some obfuscated. Some code eschews whitespace, as ifcarriage returns were a scarce natural resource, while other code resembles aliving room fashioned by Vincent Van Duysen. Code, like the people who authorit, varies.

Yet, it's ideal to minimize variation. Time and effort are best spenton novel problems.

In this post, we'll explore five faux pas often seen in Ruby code and learn how to turn these idiosyncrasies into idioms.

But first, how can we minimize variance in Ruby?

Minimizing Variance in Ruby with Rubocop and Idioms

Ruby developers can gain efficiency with Rubocop, which unifies style within asingle project or across many projects. Rails is a boon to uniformity, too, asit favors convention over configuration. Indeed, disparate Rails codebases areidentical in places and, overall, quite similar.

Beyond tools and convention, variance is also minimized by idioms, or those"structural forms peculiar to a language". For example, Ruby syntax isa collection of explicit idioms enforced by the interpreter. Reopening a classis a Ruby idiom, too.

But not all Ruby idioms are dicta. Most Ruby idioms are best practices,shorthand, and common usages Ruby developers share informally and in practice.

Learning idioms in Ruby is like learning idioms in any second spoken (orprogramming) language. It takes time and practice. Yet the more adept you are atrecognizing and proffering Ruby's idioms, the better your code will be.

What to Avoid in Ruby

Now, let's move on to looking at five things to avoid in Ruby:

  • Verbosity
  • Long expressions to detect nil
  • Overuse of self
  • Collecting results in temporary variables
  • Sorting and filtering in memory

Verbosity

If you've delved into Ruby, you can appreciate how expressive, compact, fun,and flexible it is. Any given problem can be solved in a number of ways. Forexample, if/unless, case, and the ternary operator ?: all expressdecisions — which one you should apply depends on the problem.

However, per Ruby idiom, some if statements are better than others. Forinstance, the following blocks of code achieve the same result, but one isidiomatic to Ruby:

ruby

# Verbose approach actor = nil if response == 1 actor = "Groucho"elsif response == 2 actor = "Chico"elsif response == 3 actor = "Harpo"else actor = "Zeppo"end

ruby

# Idiomatic approach actor = if response == 1 "Groucho" elsif response == 2 "Chico" elsif response == 3 "Harpo" else "Zeppo" end

Almost every Ruby statement and expression yields a value, including if, whichreturns a final code statement value and a matching condition. Theversion of the if statement in the second code block above leverages this behavior. If responseis 2, actor is set to Chico. Assigning the result of an if statement isidiomatic to Ruby. (The same construct can be applied to case, unless,begin/end, and others.)

Another Ruby idiom is present: you need not predefine a variable usedwithin an if statement (or while, for, and others). So, the latter coderemoves the line actor = nil. In Ruby, unlike other languages, the body of anif statement is not considered a separate scope.

Long Expressions to Detect nil

nil represents "nothing" in Ruby. It's a legitimate value and is its own class(NilClass) with methods. Like other classes, if you call a method that's not definedon nil, Ruby throws an exception akin to undefined method 'xxx' for nil:NilClass.

To avoid this exception, you must first test a variable to determine if it's nil.For example, code such as this is common in Rails applications:

ruby

if user && user.plan && user.plan.name == 'Standard' # ... some code for the Standard planend

The trouble is that such a long chain of assertions is unwieldy. Imagine having torepeat the condition user && user.plan && ... every time you have to referencea user's plan name.

Instead, use Ruby's safe navigation operator, &. It is shorthand forthe logic "If a method is called on nil, return nil; otherwise, call the method per normal."Using &. reduces the code above to the much more readable:

ruby

if user&.plan&.name == 'Standard' // ... some codeend

If user is nil and plan is nil, the expression user&.plan&.name isnil.

The example above assumes user and plan represent a custom structure orRails model of some kind. You can also use the safe navigation operator if avalue represents an Array or Hash.

For example, assume a_list_of_values is an Array:

ruby

a_list_of_values[index]

If the variable a_list_of_values is nil, an exception is thrown. If you expectantly trya_list_of_values&.[index], a syntax error occurs. Instead, use &. with theArray#at method.

ruby

a_list_of_values&.at(index)

Now, if a_list_of_values is nil, the result of the expression is nil.

Overuse of self

Ruby uses self in three substantive ways:

  1. To define class methods
  2. To refer to the current object
  3. To differentiate between a local variable and a method if both have the same name

Here's an example class demonstrating all three usages.

ruby

class Rectangle def self.area(length, width) new(length, width).area end def self.volume(length, width, height) area(length, width) * height end def initialize(length, width, height = nil) self.length = length self.width = width self.height = height end def area length * width end def volume area = 100 self.area * height end private attr_reader :length, :width, :heightend

def self.area is an example of the first purpose for self, defining a classmethod. Given this class, the Ruby code puts Rectangle.area(10, 5) produces50.

The code self.length = length demonstrates the second application of self:the instance variable length is set to the value of the argument length.(The attr_reader statement at the bottom defines the instance variable andprovides a getter method.) Here, the statement self.length = length isfunctionally the same as @length = length.

The third use of self is shown in the volume method. What does puts Rectangle.volume(10, 5, 2) emit? The answer is 100. The line area = 100sets a method-scoped, local variable named area. However, the line self.arearefers to the method area. Hence, the answer is 10 * 5 * 2 = 100.

Consider one more example. What is the output if the volume method is writtenlike this?:

ruby

def volume height = 10 self.area * heightend

The answer is 500 because both uses of height refer to the local variable,not the instance variable.

Inexperienced Rails developers make unnecessary use of self, as in:

ruby

# Class User# first_name: String# last_name: String# ... class User < ApplicationRecord ... def full_name "#{self.first_name} #{self.last_name}" endend

Technically, this code is correct, yet using self to refer to model attributes is unnecessary. If you combine Rubocop with yourdevelopment environment, Rubocop flags this issue for you to correct.

Collecting Results in Temporary Variables

A common code chore is processing lists of records. You might eliminate recordsdue to certain criteria, map one set of values to another, or separate one set ofrecords into multiple categories. A typical solution is to iterate over the listand accumulate a result.

For instance, consider this a solution to find all evennumbers from a list of integers:

ruby

def even_numbers(list) even_numbers = [] list.each do |number| even_numbers << number if number&.even? end return even_numbersend

The code creates an empty list to aggregate results and then iterates over thelist, ignoring nil items, and accumulating the even values. Finally, itreturns the list to the caller. This code serves its purpose, but it isn'tidiomatic.

A better approach is to use Ruby's Enumerable methods.

ruby

def even_numbers(list) list.select(&:even?) # shorthand for `.select { |v| v.even? }`end

select is one of many Enumerable methods. Each Enumerable method iteratesover a list and collects results based on a condition. Specifically, selectiterates over a list and accumulates all items where a condition yields a truthy value.In the example above, even? returns true if the item is an even number.select returns a new list, leaving the original list unchanged. A variant namedselect! performs the same purpose, but alters (mutates) the original list.

The Enumerable methods include reject and map (also known as collect).reject collects all items from a list where a condition yields a falsey value.map returns a new list where each item in the original list is transformed byan expression.

Here's one more example Enumerable method in action. First, some non-idiomatic code:

ruby

def transform(hash) new_hash = {} hash.each_pair do |key, value| new_hash[key] = value ? value * 2 : nil end return new_hashend

And now a more idiomatic approach:

ruby

def transform(hash) hash.transform_values { |value| value&.*(2) }end

transform_values is another Enumerable method available for Hash. It returnsa new hash with the same keys, but each associated value is transformed.Remember, even integers are objects in Ruby and have methods. value&.*(2)returns nil if value is nil, else value * 2.

Sorting and Filtering in Memory

Here's one last faux pas — this one is specific to Rails and ActiveRecord. Let's examine this code:

ruby

# class Student# name: String# gpa: Float# ...# class Student < ApplicationRecord ... def self.top_students Student .all .sort_by { |score| student.gpa } .select { |score| student.gpa >= 90.0 } .reverse endend

Calling Student.top_students returns all students with a GPA greater than orequal to 90.0 in ranked order, from highest to lowest. Technically, this codeis correct, but it isn't very efficient in space or time because:

  • It must load all records from the students table into memory.
  • It first sorts all records and then filters based on GPA, performing unnecessary work.
  • It reverses the order of the list in memory.

Sorting and filtering are best performed in the database, if possible, using ActiveRecord'stools.

ruby

def self.top_students Student.where(gpa: 90.0..).order(gpa: :desc)end

The shorthand 90.0.. in the where clause is a Ruby Range expressing a valuebetween 90.0 and Float::INFINITY. If gpa is indexed in the students table,this query is likely very fast and loads only the matching records. If the studentrecords are being fetched for display, more efficiency (in memory) can be gainedvia pagination (albeit at the possible expense of more queries to fetch the batches).

Wrapping Up and Next Steps

In this post, we've covered five key things to avoid in Ruby and the idioms to use instead.

I highly recommend reading the documentation for Ruby's core classes andmodules, including Array,Hash, and Enumerable.The documents are atreasure trove of methods and techniques, and chances are a method exists tosolve your problem at hand. Turn knowledge into practice by writing small codesamples to learn how each method works.

Add Rubocop into your workflow, even your text editor. Rubocop keeps your codelooking nice but can also flag idiosyncratic code. Using Rubocop is one of the best ways to learn to code the Ruby way.

Finally, read other developers' code, especially open-source Ruby projects. Toperuse the code of any gem, run bundle open <gem>, where <gem> is the nameof a library. If you've included a debugger in your Gemfile, you can even setbreakpoints in any gem and step through the code.

Go forth and code!

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!

P.P.S. Use AppSignal for Ruby for deeper debugging insights.

Five Things to Avoid in Ruby | AppSignal Blog (2024)
Top Articles
Latest Posts
Article information

Author: Ouida Strosin DO

Last Updated:

Views: 6337

Rating: 4.6 / 5 (76 voted)

Reviews: 91% of readers found this page helpful

Author information

Name: Ouida Strosin DO

Birthday: 1995-04-27

Address: Suite 927 930 Kilback Radial, Candidaville, TN 87795

Phone: +8561498978366

Job: Legacy Manufacturing Specialist

Hobby: Singing, Mountain biking, Water sports, Water sports, Taxidermy, Polo, Pet

Introduction: My name is Ouida Strosin DO, I am a precious, combative, spotless, modern, spotless, beautiful, precious person who loves writing and wants to share my knowledge and understanding with you.