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" }