All Posts

Ruby best practices beginners should know

Ruby best practices beginners should know

Python's dominance is never really questioned when it comes to the best for programming novices because it checks almost every box that defines a simple language. It's remarkably easy to pick up and can rise to any challenge. But what about Ruby?

Although it does not get enough credit for being one, Ruby is an awesome language for beginners. It provides powerful constructs (like blocks) and versatile concepts (like message passing à la Smalltalk) but retains Python's fluid and elegant English-like syntax. In fact, in many cases, one might argue that the unique design choices that went into the Ruby syntax beat even Python in terms of readability and expressiveness. If you're getting into programming, it's very easy to recommend that you go with Ruby.

This article aims to establish some practices that are generally accepted by the Ruby programming community. Before we dive right into how you, as a beginner, can channel Ruby's power, let me mention two Japanese phrases that you should hold in memory: "okonomi" and "omakase". I promise this is relevant, and you'll see why in a bit.

"Okonomi"

Ruby has always been driven by the principle to "optimize for programmer happiness" and, as a result, always offers several different ways to do the same thing, unlike Python, which has valued the philosophy of having "one and preferably only one way to do something". As a result, many people who come to Ruby from another language find themselves baffled at the sheer number of choices the language offers for accomplishing any given task.

This is what the Japanese call "okonomi", a phrase that typically applies to sushi, which translates to "I'll choose what to order".

Statements are expressions

In Ruby, there are no statements. Every line and every command is an expression that evaluates to something. Even function definitions return something, the symbolized function name.

So while you cannot hold functions in variables (something Python and JavaScript allow), you can hold their identifiers as symbols and invoke them with send (more on that in a bit).


name = 'DeepSource'     # returns the string 'DeepSource' itself...
b = name = 'DeepSource' # ...so this is also valid

puts name               # displays "DeepSource" and returns nil

name.split('').select do |char|
  char.downcase == char # block returns true or false
end                     # returns ["e", "e", "p", "o", "u", "r", "c", "e"]

def prime? num
    # ...
end                     # returns :prime?

How do you make the most of this? This particular design decision leads to a few notable features, the most recognizable of which is that both the return and next keywords are, for the most part, redundant, and the recommendation is not to use them unless you want to terminate the function or block early.


def prime? num
  if num >= 2
    f = (2...num).select do |i|
      ~~next~~ num % i == 0
    end
    ~~return~~ f.size == 0
  else
    ~~return~~ false
  end
end

Bye bye brackets

In Ruby function, parentheses are, with certain caveats, optional as well. This makes Ruby ideal for defining domain-specific languages or DSLs. A domain-specific language is a language built on top of another that defines abstractions for a specific specialized purpose.

For examples of DSLs, you need not look further than Rails or RSpec but for the sake of simplicity, consider Sinatra, a straightforward web server built in Ruby. This is how a simple service would look built with Sinatra.


require 'sinatra'

get '/' do
  'Hello, World!'
end

What's not immediately apparent from this example is that get here, which looks very much like a language keyword, is actually a function! The get function here takes two arguments, a string path and a block that contains the code to be executed when the path matches. The return value of the block will be the HTTP response for the request matching the method and path.

How do you make the most of this? By embracing this freedom, you can define your own abstractions for your use cases! Ruby actively encourages developers to define their own little DSLs for day-to-day abstractions and write clean code by using them. The best part is that code then reads like pseudocode which is always a good thing.

While parentheses are optional, the convention is to generally omit the parentheses only for zero or one arguments, while retaining them when there are more than one, for the sake of clarity.


require 'date'

def log message
  timestamp = DateTime.now.iso8601
    puts "[#{timestamp}] #{message}"
end

log "Hello World!"
# [2020-12-30T21:18:30+05:30] Hello World!

Method names?!

Ruby enables method names to have all sorts of punctuation such as =, ?, !. Each of these punctuation has its purpose, by convention, and indicates the particular function's nature.


| Punctuation | Purpose                                       | Example   |
| ----------- | --------------------------------------------- | --------- |
| `?`         | Returns a Boolean value, `true` or `false`    | .nil?     |
| `!`         | Modifies values in place or raises exceptions | .reverse! |

Many Ruby functions come in two variants, regular and "bang". To understand the difference, consider this example with two variants of the reverse method on strings.


str = 'DeepSource'

str.reverse  # "ecruoSpeeD"
str          # "DeepSource"

str.reverse! # "ecruoSpeeD"
str          # "ecruoSpeeD"

In Ruby, functions with the exclamation mark in the name modify the object they are called on. In Rails, the exclamation mark indicates that the function will raise an exception if it cannot accomplish said task instead of failing silently like its non-bang counterpart.

How do you make the most of this? You should imbibe the same convention in your code so that users of the code can easily get an idea of the function's nature.

Symbols over strings

The concept of symbols is something unique to Ruby. Symbols are just strings that are interned in memory and are not allocated new memory every time they are defined.


'text'.object_id # 200
'text'.object_id # 220

:text.object_id  # 2028508
:text.object_id  # 2028508

Symbols are used in tons of places across Ruby, notably as keys for hashes and constants defining identifiers and functions.

You can symbolize a string in two ways, prefixing the colon: before the string (quotes are required unless the string is a valid identifier) or invoking to_sym method on it.'


:identifier                # :identifier
:'not an identifier'       # :"not an identifier"
'not_an_identifier'.to_sym # :"not an identifier"

How do I make the most of this? In Ruby hashes, keys and values are generally separated using the rocket operator =>. Since version 1.9, Ruby also provides a more concise, cleaner and JavaScript-esque syntax using colons :, but only if your keys are symbols.


person_one = { 'name' => 'Alice', 'age' => 1 }
person_one['name'] # 'Alice'

person_two = { :name => 'Bob', :age => 2 } # Replace strings with symbols
person_two[:name]  # 'Bob'

person_three = { name: 'Carol', age: 3 } # Use new style
person_three[:name]  # 'Carol'

The second notation is easier to read, performs better for larger data sizes, and is widely recommended now as the preferred way to define hashes. Symbols are also used for passing messages to objects using send !).

Talk over messages

Ruby inherits the concept of message passing from Smalltalk and goes all-in with this idea. Every method call and every variable access in Ruby is achieved by sending a message to the instance and receiving a response.

Messages are sent by using the send function (or public_send to not bypass visibility) and passing the symbol with the name of the method you want to invoke, and any arguments that function may require. Ruby also allows you to dynamically define methods using define_method (duh!) and even perform actions when a method is not defined on an object.


str = "DeepSource"

str.respond_to? :reverse # true
str.send(:reverse)       # "ecruoSpeeD"

str.respond_to? :[]      # true
str.send(:[], 4..9)      # "Source"

str.respond_to? :dummy   # false

The above examples are the same as what you could achieve by invoking the string's reverse and split methods. If a class responds to a message, it will perform the action or raise an exception if it doesn't.

How do I make the most of this? Playing with this message functionality unlocks a whole new world of meta-programming that few other languages can match. You should try to use these features to remove boilerplate from your code and write.


class Person
  def initialize(name, age)
    @name = name
    @age = age
  end

  [:name, :age].each do |attr|
        # Get getters for @name and @age with logging
    define_method attr do
      val = instance_variable_get("@#{attr}".to_sym)
      puts "Accessing `#{attr}` with value #{val}"
      val
    end
  end

  # Handle missing attribute
    puts "Person does not #{m.id2name}"
  end
end

person = Person.new("Dhruv", 24)
person.name # "Dhruv" | prints "Accessing `name` with value Dhruv"
person.age  # 24      | prints "Accessing `age` with value 24
person.sing # nil     | prints "Person does not sing"

Monkey patching

This is perhaps the most controversial but also the most powerful feature of Ruby.

Monkey patching, a play on the phrases "guerrilla patching" and "monkeying about", is the process of opening up pre-defined classes and changing their existing functionality or adding functionality to them. Ruby as a language is one of the only few that actively encourage the use of monkey patching as a legitimate way to add functionality to the language.

While languages like Python and JavaScript discourage the process, Ruby embraces it and makes it dead easy to extend any class or module as simple as this:


class String
  def palindrome?
    self == reverse
  end
end

'racecar'.palindrome? # true
'arizona'.palindrome? # false

How do I make the most of this? Frameworks like Rails actively use monkey patching to add features to built-in Ruby classes like String and Integer. If used carefully and appropriately documented, it is a powerful way of adding functionality in one location and making it available everywhere across the codebase.

Care should be taken to not overdo this and only add the most general purpose code to the root classes.

To drive the point home, feast your eyes on monkey patched code beauty from Rails ActiveSupport. You can install it separately from Rails and see a comprehensive list of examples in their docs.


time = '2021-01-01 12:00:00'.to_time        # 2020-01-01 13:00:00 +0530
day_prior = time - 1.day                    # 2019-12-31 13:00:00 +0530
almost_new_year = day_prior.end_of_day      # 2019-12-31 23:59:59 +0530
happy_new_year = almost_new_year + 1.second # 2020-01-01 00:00:00 +0530

1.in? [1, 2, 3]                                                    # true

{ sym_key: 'value' }.with_indifferent_access[:sym_key.to_s]        # "value"
{ 'str_key' => 'value' }.with_indifferent_access['str_key'.to_sym] # "value"

Less is more

In the pursuit of programmer happiness, Ruby is packed to the brim with syntax sugar and method aliases focused on readability.

How do I make the most of this? You should preferably use shorthand wherever Ruby's syntax allows it because the reduced punctuation and added conciseness makes the code easier to read and process mentally.

Want to make an arrays of strings, symbols or numbers? Use % notation.


%w[one two three] # ["one", "two", "three"]
%i[one two three] # [:one, :two, :three]

There are uppercase versions of these shorthands that also allow interpolation.


pfx = 'item'
%W[#{pfx}_one #{pfx}_two #{pfx}_three] # ["item_one", "item_two", "item_three"]
%I[#{pfx}_one #{pfx}_two #{pfx}_three] # [:item_one, :item_two, :item_three]

Another interesting shorthand notation is when you're trying to map or select over an array, and all you want to do is invoke a single method on all of the objects.


strings = %w[one two three]
strings.map &:upcase # ~ strings.map { |str| str.upcase! }

Many of the operations we take for granted in a programming language, like mathematical operations, are all syntax sugar notations! Since Ruby is built on message passing, any valid string is a valid method name such as + or [].


1.+(2)           # ~ 1 + 2

words = %w[zero one two three four]
words.[](2)      # ~ words[2]
words.<<('five') # ~ words << 2

"Omakase"

Remember the other word I asked you to remember when we started? I promised it was relevant. "Omakase" is an opposite version of an "okonomi" meal, this time consisting of sushi selected for you by the chef. While Ruby's offer of several ways to accomplish something boosts creativity and expression, it comes with its own set of drawbacks, especially when it comes to consistency and code review.


def is_prime(num)
  if num >= 2
    f = (2...num).select do |i|
      next num % i == 0
    end
    return f.size == 0
  else
    return false
  end
end

is_prime(0) # false
is_prime(1) # false
is_prime(2) # true
is_prime(3) # true
is_prime(4) # false
class Integer
  def prime?
    return false if self < 2

    (2...self).none? { |i| self % i == 0 }
  end
end

0.prime? # false
1.prime? # false
2.prime? # true
3.prime? # true
4.prime? # false

Based on what we've leaned, we can make the following changes to the code:

  • monkey patch method definition into the Integer class
  • use ? in method names that have a Boolean return type
  • remove redundant parentheses ( )
  • exit early instead of indenting the entire code inside an if block
  • remove redundant return and next keywords
  • inline blocks with a single expression using braces { }
  • use from Ruby's huge assortment built-in functions, such as Array::none?

Using a comprehensive style guide and linter is key when getting started with programming in any new language. Not only does it improve the quality of your code, but it also provides you an excellent way to learn by pointing out improvements in real-time as you code.

A linter will reduce the number of ways you have for writing a program, but what you lose in the creativity of expression is more than made up for inconsistency and code clarity. I would gladly make that trade-off even as a professional, much more so if I were a novice.

Let me recommend two tools that you should use as a beginner.

RuboCop

When it comes to a linter for Ruby, nothing comes even close to RuboCop. It's a linter, formatted, and style guide rolled into one, and while it is configurable, the configuration that shipped with out-of-the-box is more than enough to put you on the right track for code quality. RuboCop also helps you scan for security vulnerabilities, giving you yet another reason to set it up.

Install and try it out today. Your future self will thank you, and possibly me as well.

DeepSource

When it comes to working in a team, reviewing other people's code becomes important. DeepSource is an automated code review tool that manages the end-to-end code scanning process and automatically makes pull requests with fixes whenever new commits are pushed, or new pull requests are opened against the main branch.

Setting up DeepSource for Ruby is extremely easy. As soon as you have it set up, it will scan your entire codebase, find scope for improvements, fix them, and open PRs for those changes.

That's all folks! Have fun learning Ruby, it's a fantastic language.

Get started with DeepSource

DeepSource is free forever for small teams and open-source projects. Start analyzing your code in less than 2 minutes.

Newsletter

Read product updates, company announcements, how we build DeepSource, what we think about good code, and more.

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.