BLOG

Rediscovering Ruby: 5 lesser-known features that can boost your code

Code using a slow method that memoizes the result of multiple methods using blocks

Juan Aparicio

Engineering Manager

Aug 11, 2023

6 min read

Ruby

Tips

Unleash the Iterator power with &:method

Code using method(:extract_username)
using method(:extract_username) to get around blocks

Take a look at the following snippet


User.all.map(&:name)
['Juan Aparicio', 'Nicolas Erlichman', ...]

That’s all and good, but what if you need to map a method that is not from the object which is iterated over?


def extract_user_name(user)
  if user.first_name && user.last_name
    "#{first_name} #{last_name}"
  else
    user.email
  end
end

Now this looks a bit more complex. There are cases where we want to map a method that receives the object which is iterated over as an argument. You may go with the longer approach:


User.all.map { |user| extract_user_name(user) }

It feels like there’s unnecessary stuff here. We could however rewrite this as:


User.all.map(&method(:extract_user_name))

Multiline memoization with blocks

Code using a slow method that memoizes the result of multiple methods using blocks
A slow method that memoizes the result of multiple methods using blocks

If you have been programming Ruby for a while, you have probably seen memoization in action:


def some_expensive_method
  @ome_expensive_method ||= long_array.select { |element| element.value == 42}
end

This method memoizes the value of long_array.select { … } so that the first time you call the method, the value is stored as an instance variable. Subsequent calls to the method don’t execute the code in the right hand of the mallet operator (||=).

There are some cases where you may want to memoize an expression that has multiple lines:


def thousand_fibonacci
  sequence = [0, 1]

  while sequence.length < 1000
    next_number = sequence[-1] + sequence[-2]
    sequence << next_number
  end

  sequence
end

Let’s suppose you need a method that returns the first 1000 numbers of the Fibonacci sequence in an array. The value will be used multiple times across multiple method calls inside a class. Ideally you would memoize the method. You could take the thousand_fibonacci method and wrap the body in a block, and assign the value of the block to an instance variable using the hammer operator:


def thousand_fibonacci
  @thousand_fibonacci ||= begin
    sequence = [0, 1]
  
    while sequence.length < 1000
      next_number = sequence[-1] + sequence[-2]
      sequence << next_number
    end
  
    sequence
  end
end

Defining methods dynamically

As a programmer we usually prefer to waste time trying to automate stuff, rather than doing it ourselves, even if it takes 10 times more to automate it.

I remember from my Java days, when you had to define getters and setters for each of the classes. It was incredibly frustrating.

Now that I do most of my coding in Ruby, those are problems of the past. Now all you have to do is type attr_accessor :instance_variable and you have a getter and setter for it.

The point of that rant is to show you the magic of define_method and how attr_accessor leverages the power of dynamic programming to achieve that.

Take a look at the following class:


class User
  ROLES = {
    read_only: 0,
    editor: 1,
    admin: 2
  }.freeze

  def initialize(role)
    @role = role
  end

  def read_only?
    @role == ROLES[:read_only]
  end

  def editor?
    @role == ROLES[:editor]
  end

  def admin?
    @role == ROLES[:admin]
  end
end

This really takes me back to my Java days. It feels like I’m writing getters and setters all over again.

Enter define_method

define_method allows you, the developer, to define methods in runtime. This means that you can define the template of a method, and when the code is evaluated at runtime, the method will be defined.

It’s better to see it with code, so let’s rewrite the <role>? methods using define_method


class User
  ROLES = {
    read_only: 0,
    editor: 1,
    admin: 2
  }.freeze

  def initialize(role)
    @role = role
  end

  ROLES.each do |role_symbol, role_value|
    define_method("#{role_symbol}?") do
      @role == role_value
    end
  end
end

When Ruby evaluates the content of the User class, it will iterate through the keys of the ROLES hash. Doing that defines an instance method called <key_name>? which checks if the value of the instance variable @role is the same as the value of the ROLES hash for that key.

Now you can have easy to understand methods for your User class:


u = User.new(1)
u.read_only? # => false
u.editor? # => true
u.admin? # => false

Reopening classes

Code adding overpowered? method to string
Adding the overpowered? method to strings

Because Ruby trusts the programmer with the inner workings of the language, (almost) everything that Ruby provides, can be reopened and modified. Core classes like String, Array and Integer can be modified so that new methods can be added into it (or to modify the methods).

Imagine you are tasked with writing a method in Ruby that receives an argument which is an Array of strings, integers, or nil elements. The method should return false if all of the elements in the Array contain empty strings, nil, or zeroes.

You could do the following:


def empty_array?(array)
  array.all? do |item|
    item == 0 || item == "" || item == nil
  end
end 

This approach is good, but you could approach the task from a different angle. What if String, Integer and NilClass (yes, nil is an instance of NilClass) all had the method empty?

Let’s open those classes and add the method empty? to each of them and pass that method to all?:


class String
  def empty?
    self == ""
  end
end

class Integer
  def empty?
    self == 0
  end
end

class NilClass
  def empty?
    true
  end
end

def empty_array?(array)
  array.all?(&:empty?)
end

Thats it, now you can call empty? on any instance of the classes String, Integer or NilClass!


nil.empty? # => true
"".empty? # => true
"Juan".empty? # => false
0.empty? # => true
1.empty? # => false

Easy debugging with the #methods method

Code using CSV.methods.join
Some of the CSV class methods

One of the most annoying things about programming in Ruby is that IDE’s language support is not as good as JavaScript, TypeScript or some of the other languages around. Not having a good IntelliSense makes not just coding, but also debugging a bit slower.

Luckily Ruby has a really high degree of introspection. This means that objects in Ruby can answer quite a lot about themselves. One of the methods which I used the most while debugging with external gems is calling the #methods method on an object while on a debugging session.

Consider the CSV class. It’s really useful for ingesting csv, converting the data with Ruby algorithms, and then exporting that data into another CSV. But what if you don’t really know which are the methods available?


CSV.methods # => [:read, :instance, :generate_line, :generate, ...]
CSV.instance_methods # => [:read, :binmode, :truncate, ...]

You can take it a step further and try the method arity, when used in conjunction with #method, looks like this:


CSV.method(:read).arity # => -2

Do you know any other hidden Ruby features that make your life easier and your code cleaner?

Juan Aparicio

Engineering Manager

Aug 11, 2023

6 min read

Ruby

Tips

BLOG

Unlock forbidden knowledge

Explore more

Arrow Icon
Efficiently Caching Translations in React with Zustand and Google Translate API

Frontend

Guides

Efficiently Caching Translations in React with Zustand and Google Translate API

Learn to integrate Zustand and Google Translate API for a seamless translation feature. Cache results to reduce API calls, boost performance, and enhance scalability.

Mauro Davoli

Fast API Development with Hono and Cloudflare Workers

Backend

Technology

Guides

Fast API Development with Hono and Cloudflare Workers

Pablo shares his journey of finding tools to build REST APIs quickly and efficiently. He highlights his experience with Hono, a lightweight framework reminiscent of Express, designed specifically for Cloudflare Workers.

Pablo Haller

A step-by-step infographic showing the process to set up authentication with Next.js using the next-auth library.

Guides

Technology

Basic GitHub OAuth Authentication with Next.js and next-auth

This blog explains how to set up GitHub OAuth in Next.js using next-auth. It covers creating a GitHub OAuth app, configuring environment variables, integrating authentication with a custom route, protecting routes, and managing sessions.

Pablo Haller