Rediscovering Ruby: Embrace New Features for Optimal Performance

Ruby is constantly evolving, and staying updated with its latest features can make a significant difference in your coding efficiency. If you’re still using Ruby 1.9.2 techniques in the Ruby 3.3 era, it’s time to refresh your skills and embrace the new enhancements.

Many developers tend to stick to the familiar, old methods, missing out on the benefits of the latest updates. Embracing new features is crucial to maintaining Ruby’s relevance and efficiency in the modern programming landscape.

RBS (Ruby Signature)

Traditionally, ensuring type safety in Ruby required extensive testing and documentation, which could be time-consuming and error-prone.

RBS, introduced with Ruby 3.1, is a new language designed to describe the types and interfaces of Ruby code. It allows for static type checking and IDE autocompletion, enhancing code quality and developer productivity.

While alternatives like Sorbet exist, RBS is officially supported by Matz, making it the standard for type checking in Ruby.

# Point.rbs
class Point
  attr_reader x: Integer
  attr_reader y: Integer

  def initialize: (x: Integer, y: Integer) -> void
end

# Point.rb
class Point
  attr_reader :x, :y

  def initialize(x, y)
    @x = x
    @y = y
  end
end

Steep, a static type checker for Ruby, can be integrated into CI/CD pipelines to ensure type safety and catch errors early.

Here’s how to configure Steep in a GitHub Actions CI/CD pipeline:

# .github/workflows/ci.yml
name: CI

on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v2

      - name: Setup Ruby
        uses: actions/setup-ruby@v1
        with:
          ruby-version: '3.3'

      - name: Install dependencies
        run: bundle install

      - name: Run Steep
        run: bundle exec steep check

Integrating Steep into your CI/CD pipeline helps maintain code quality by enforcing type safety and preventing runtime errors. This promotes best practices and enhances the reliability of Ruby applications.

However, adopting RBS might require a significant investment in learning and setting up the necessary tools, and maintaining type annotations can add overhead.

Pattern Matching

Pattern matching, a highly anticipated feature, was introduced in Ruby 2.7 and further enhanced in Ruby 3.0. Matz wanted to include pattern matching to make Ruby more expressive and align it with modern programming languages.

Before pattern matching, handling complex data structures required verbose code, making it harder to maintain.

Pattern matching allows for concise and expressive destructuring and matching of data, improving code readability and maintainability.

Example:

def company_location_contact_id(company_location_id)
  query = <<~GRAPHQL
    query($company_location_id: ID!) {
      companyLocation(id: $company_location_id) {
        ...
      }
    }
  GRAPHQL

  response = @client.query(
    query:,
    variables: { company_location_id: "gid://shopify/CompanyLocation/#{company_location_id}" }
  ).body

  case response.deep_symbolize_keys
  in errors: [{ message: error_message }]
    Rollbar.error("#{error_message} for", company_location_id:)
  in data: { companyLocation: nil }
    Rollbar.error("Company location not found", company_location_id:)
  in data: { companyLocation: { roleAssignments: { edges: [{ node: { companyContact: { id: contact_id } } }] } } }
    contact_id
  end
end

Without pattern matching, the code becomes verbose and error-prone, involving nested conditionals or manual parsing of the response data.

One-Line Pattern Matching

Destructuring hashes or arrays often required multiple lines of code. One-line pattern matching, introduced in Ruby 2.7, simplifies this by allowing destructuring in a single line.

data = { user: { name: "Alice", details: { age: 25, city: "Paris" } } }
data => { user: { name:, details: { age:, city: } } }
puts "Name: #{name}, Age: #{age}, City: #{city}"

While concise, the one-line syntax may be less readable for complex patterns, requiring familiarity with the new syntax.

Rightward Assignment

Rightward assignment, introduced in Ruby 3.0, simplifies syntax by allowing assignments in a more readable manner.

read_data() => user_data => { user: { name:, details: { age:, city: } } }
save!(user_data)
puts "Name: #{name}, City: #{city}"

This syntax, though more natural in flow, might be unfamiliar and potentially confusing if overused in complex expressions.

Refinements

Global modifications to core classes could lead to conflicts. Refinements, introduced in Ruby 2.0, allow scoped modifications, reducing side effects.

module ArrayExtensions
  refine Array do
    def to_hash
      Hash[*self.flatten]
    end
  end
end

class Converter
  using ArrayExtensions

  def self.convert(array)
    array.to_hash
  end
end

puts Converter.convert([[:key1, "value1"], [:key2, "value2"]])

Refinements are powerful but can lead to unexpected behavior if not active in the expected context.

Enumerator::Lazy

Handling large collections efficiently is crucial. Lazy enumerators, introduced in Ruby 2.0, enable efficient chaining without creating intermediate arrays.

lazy_numbers = (1..Float::INFINITY).lazy
result = lazy_numbers.select { |n| n % 2 == 0 }
                     .map { |n| n * n }
                     .take(10)
                     .to_a

puts result.inspect

While improving performance, lazy enumerators can complicate debugging due to deferred operations.

Enumerator::Chain

Chaining multiple enumerables was less expressive. Enumerator::Chain, introduced in Ruby 2.6, simplifies this, enhancing readability.

evens = (2..10).step(2)
odds = (1..9).step(2)
combined = evens.each.chain(odds.each)

puts combined.to_a.inspect

Though it simplifies chaining, it introduces another concept for developers to learn.

Module#prepend

Using include to mix in modules could make method overrides complex. Module#prepend, introduced in Ruby 2.0, provides a more predictable method override mechanism.

module Logging
  def process
    puts "Logging before processing"
    super
    puts "Logging after processing"
  end
end

class DataProcessor
  def process
    puts "Processing data"
  end
end

class CustomProcessor < DataProcessor
  prepend Logging
end

processor = CustomProcessor.new
processor.process

Prepend can complicate the method lookup chain, requiring a clear understanding of include and prepend differences.

End-less Method Definition

Defining simple methods required multiple lines. Endless method definitions, introduced in Ruby 3.0, provide a concise syntax for single-expression methods.

class Calculator
  def add(a, b) = a + b
  def multiply(a, b) = a * b
  def power(base, exponent) = base**exponent
end

calc = Calculator.new
puts calc.add(2, 3)       # Output: 5
puts calc.multiply(4, 5)  # Output: 20
puts calc.power(2, 3)     # Output: 8

This new syntax, while reducing boilerplate, might be less familiar and potentially reduce readability if overused.

it Parameter

Using explicit block variables could be verbose for simple operations. The it parameter, introduced in Ruby 3.0, simplifies block syntax.

["apple", "banana", "cherry"].map { it.upcase.reverse }

This parameter can make code less readable for complex operations, introducing a new convention for developers to learn.

String#casecmp?

Case-insensitive string comparison was verbose. String#casecmp?, introduced in Ruby 2.4, simplifies this operation.

strings = ["Hello", "world", "HELLO"]
matches = strings.select { |s| s.casecmp?("hello") }
puts matches.inspect # => ["Hello", "HELLO"]

Object#yield_self and then

Chaining operations on an object could be verbose. Object#yield_self and its alias then improve readability and fluidity.

result = "hello"
         .yield_self { |str| str.upcase }
         .then { |str| str + " WORLD" }