There’s something magical about the way that Ruby flows from your fingertips. Perhaps that’s why it was once said that “Ruby will teach you to express your ideas through a computer.” And it’s most likely the reason that Ruby has become such a popular choice for modern web development.
Just as in other languages, there are numerous ways to say the same thing in Ruby. I spend a lot of time reading and nitpicking people’s code on Exercism. And I often see exercises solved in a way that could be greatly simplified if the author had only known a certain Ruby method.
Here’s a look at some lesser-known Ruby methods that solve specific types of problems very well.
1. Object#tap
Ever find yourself calling a method on some object, and the return value is not what you want it to be? You’re hoping to get back the object, but instead you got back some other value. Maybe you want to add an arbitrary value to a set of parameters stored in a hash, so you update it with Hash.[]
, but you get back 'bar'
instead of the params hash—so you have to return it explicitly.
def update_params(params)
params[:foo] = 'bar'
params
end
The params
line at the end of that method seems extraneous. So, we can clean it up with Object#tap.
It’s easy to use. Just call it on the object, then pass tap
a block with the code that you wanted to run. The object will be yielded to the block and then be returned. Here’s how we could use it to improve update_params
:
def update_params(params)
params.tap {|p| p[:foo] = 'bar' }
end
There are dozens of great places to use Object#tap. Just keep your eyes open for methods called on an object that don’t return the object, when you wish that they would.
2. Array#bsearch
I don’t know about you, but I look through lots of arrays for data. Ruby enumerables make it easy to find what I need; select, reject, and find are valuable tools that I use daily. But when the dataset is big, I start to worry about the length of time it will take to go through all those records.
If you’re using ActiveRecord and dealing with a SQL database, there’s a lot of magic happening behind the scenes to make sure your searches are conducted with the least algorithmic complexity. But sometimes you must pull all the data from a database before you can work with it. For example, if records are encrypted in the database, you can’t query them very well with SQL.
At times like these, I think hard about how to sift through data with an algorithm that has the least complex Big-O classification that I can. If you don’t know about Big-O notation, check out Justin Abrahms’s Big-O Notation Explained by a Self-Taught Programmer or the Big-O Cheat Sheet.
The gist is that algorithms can take more or less time, depending on their complexity, which is ranked in this order: O(1), O(log n), O(n), O(n log(n)), O(n^2), O(2^n), O(n!). So, we prefer searches to be in one of the classifications at the beginning of this list.
When it comes to searching through arrays in Ruby, the first method that comes to mind is Enumerable#find, also known as detect. However, this method will search through an entire list until the match is found. While that’s great if the record is at the beginning, it’s a problem if it’s at the end of a long list. It takes O(n) complexity to run a find search.
Luckily, there’s a faster way. Array#bsearch can find a match with only O(log n) complexity. To learn more about how a Binary Search works, check out my post Building A Binary Search.
Here’s a look at the difference in search times between the two approaches when searching a range of 50,000,000 numbers:
require 'benchmark'
data = (..50_000_000)
Benchmark.bm do |x|
x.report(:find) { data.find {|number| number > 40_000_000 } }
x.report(:bsearch) { data.bsearch {|number| number > 40_000_000 } }
end
user system total real
find 3.020000 .010000 3.030000 (3.028417)
bsearch .000000 .000000 .000000 (.000006)
As you can see, bsearch is much faster. However, there’s a pretty big catch involved with using bsearch: The array must be sorted. While this somewhat limits its usefulness, it’s still worth keeping in mind for occasions where it might come in handy—such as finding a record by a created_at timestamp
that has already been loaded from the database.
3. Enumerable#flat_map
When dealing with relational data, sometimes we need to collect a bunch of unrelated attributes and return them in an array that is not nested. Let’s imagine you had a blog application, and you wanted to find the authors of comments left on posts written in the last month by a given set of users.
You might do something like this:
module CommentFinder
def self.find_for_users(user_ids)
users = User.where(id: user_ids)
users.map do |user|
user.posts.map do |post|
post.comments.map |comment|
comment.author.username
end
end
end
end
end
You would then end up with a result such as:
[[['Ben', 'Sam', 'David'], ['Keith']], [[], [nil]], [['Chris'], []]]
I know, I know. You just wanted the authors! Here, we’ll call flatten
.
module CommentFinder
def self.find_for_users(user_ids)
users = User.where(id: user_ids)
users.map { |user|
user.posts.map { |post|
post.comments.map { |comment|
comment.author.username
}.flatten
}.flatten
}.flatten
end
end
Another option would have been to use flat_map
, which does the flattening as you go:
module CommentFinder
def self.find_for_users(user_ids)
users = User.where(id: user_ids)
users.flat_map { |user|
user.posts.flat_map { |post|
post.comments.flat_map { |comment|
comment.author.username
}
}
}
end
end
It’s not too much different but better than having to call flatten
a bunch of times.
4. Array.new with a Block
One time, when I was in bootcamp, our teacher Jeff Casimir—founder of Turing School—asked us to build the game Battleship in an hour. It was a great exercise in object-oriented programming. We needed Rules, Players, Games, and Boards.
Creating a represention of a Board is a fun exercise. After several iterations, I found the easiest way to set up an 8×8 grid was to do this:
class Board
def board
@board ||= Array.new(8) { Array.new(8) { '0' } }
end
end
What’s going on here? When you call Array.new with an argument, it creates an array of that length:
Array.new(8)
#=> [nil, nil, nil, nil, nil, nil, nil, nil]
When you pass it a block, it populates each of its members with the result of evaluating that block:
Array.new(8) { 'O' }
#=> ['O', 'O', 'O', 'O', 'O', 'O', 'O', 'O']
So if you pass an array with eight elements a block that produces an array with eight elements that are all 'O'
, you end up with an 8×8 array populated with 'O'
strings.
Using the Array#new with a block pattern, you can create all kinds of bizarre arrays with default data and any amount of nesting.
5. <=>
The spaceship—or sort—operator is one of my favorite Ruby constructs. It appears in most of the built-in Ruby classes, and is useful when working with enumerables.
To illustrate how it works, let’s look at how it behaves for Fixnums
. If you call 5<=>5
, it returns . If you call
4<=>5
, it returns -1
. If you call 5<=>4
, it returns 1
. Basically, if the two numbers are the same, it returns , otherwise it returns
-1
for least to greatest sorting and 1
for reverse sorting.
You can use the spaceship in your own classes by including the comparable module and redefining <=>
with logic branching to make it return -1
, , and
1
for the cases you want.
Okay, but why would you ever want to do that? Here’s a cool use of it I came across on Exercism one day.
There’s an exercise called Clock, where you have to adjust the hours and minutes on a clock using custom +
and -
methods. It gets complicated when you try to add more than 60 minutes, because that will make your minute value invalid. So you have to adjust by incrementing another hour and subtracting 60 from the minutes.
One user, dalexj
, had a brilliant way to solve this, using the spaceship operator:
def fix_minutes
until (...60).member? minutes
@hours -= 60 <=> minutes
@minutes += 60 * (60 <=> minutes)
end
@hours %= 24
self
end
It works like this: until the minutes passed in are between 0 and 60, he subtracts either 1 or -1 from the hours, depending on whether the minute amount is greater than 60. He then adjusts the minutes, adding either -60 or 60 depending on the sort order.
The spaceship is great for defining custom sort orders for your objects and can also come in handy for arithmetic operations if you remember that it returns one of three Fixnum
values.
Wrapping Up
Getting better at writing code is a process of learning. Because Ruby is a language, a lot of the time I spend trying to improve is spent on reading “literature” (e.g. code on Exercism and GitHub) and reading what is essentially the dictionary for my language: Ruby-Doc.
It’s so much easier to write expressive code when you know additional methods. I hope that this collection of curiosities helped expand your Ruby vocabulary.
What are your favorite Ruby methods to use in making your code more succinct and understandable? Leave a comment below!
‘We didn’t have a dedicated DevOps team at the start, so we chose Engine Yard to run our highly scalable help desk application. This allowed us to focus on building new features, and we released our pre-beta software in three months – without Engine Yard, it would have taken at least double that time.’
Kiran Darisi
Director Technical Operations
Freshdesk
Ready to give Engine Yard a spin? Try 500 hours free!
{{cta(‘5019e5e3-8348-4b05-b8bf-ede6bb67025e’)}}