You can also find all 100 answers here π Devinterview.io - Ruby
Ruby is a dynamic, object-oriented programming language known for its simplicity and focus on developer productivity. Its main claim to fame in web development is the web application framework, Ruby on Rails (RoR), which transformed the way web applications are built by promoting convention over configuration.
-
Language Syntax: Ruby's syntax has an appeasing natural language style. This, paired with its dynamic typing, powerful metaprogramming features, and absence of semicolons, results in clean and expressive code.
-
Gems: Ruby's package manager, RubyGems, simplifies library management, making it easy to integrate numerous third-party extensions.
-
Database Integration: ActiveRecord, a popular object-relational mapping system, aids in managing database records via a natural, object-oriented interface.
-
MVC Pattern: Rails, in particular, is famed for its adherence to the Model-View-Controller pattern, offering a clear separation of concerns.
-
Code Rearrangement: The auto-loading mechanism allows for seamless navigation between related files and classes while coding.
-
Ecosystem Consistency: RoR brought about a
many
-to
-many
relationship with databases, streamlining and simplifying existing development patterns. -
Strong Community: The language's supportive community and its commitment to clean, readable code are evident in guiding principles like "Mediterranean" quality and "Matz's kindness."
-
Test-Driven Development: RoR promotes best-testing practices from the project's inception, ensuring reliability.
-
Giant Corporations' Indulgence: Prominent organizations such as GitHub, Shopify, and Airbnb have successfully tapped into the potential of Ruby on Rails.
Here is the Ruby code:
# config/routes.rb
Rails.application.routes.draw do
root 'welcome#index'
get 'products/:id', to: 'products#show'
resources :articles
end
This file configures routes for different URLs, specifying which controllers and actions to invoke. For instance, upon receiving a GET
request for products/5
, RoR would route it to the show
action in the ProductsController
with the ID parameter set to 5
. Such straightforward setups contribute to RoR's appeal.
First, you create a Ruby
script file with a .rb
extension that contains your Ruby code. You can then execute this script using the ruby
command in your command line.
-
Create a File: Use any text editor to write your Ruby code, and save the file with a
.rb
extension, e.g.,my_ruby_script.rb
. -
Write Ruby Code: Here is a simple example.
# Filename: my_ruby_script.rb puts "Hello, Ruby Script!"
-
Run the Ruby Script: Go to your command line and navigate to the folder where the Ruby file is saved. Then, type the following command:
ruby my_ruby_script.rb
After pressing enter, you will see the output:
Hello, Ruby Script!
You can access command-line arguments using special variables called ARGV
.
Here is the code:
# Filename: script_with_args.rb
puts "Arguments: #{ARGV.join(', ')}"
In the command line, you would run this script as:
ruby script_with_args.rb arg1 arg2 arg3
The output would be:
Arguments: arg1, arg2, arg3
Ruby scripts can engage with users using the gets
method.
Here is an example:
# Filename: interactive_script.rb
puts "What is your name?"
name = gets.chomp
puts "Hello, #{name}!"
When you run this script using ruby interactive_script.rb
, it will prompt you for your name, and after you provide it, it will greet you.
If you want a script to run in the background without blocking your command line, you can use the &
character.
For instance, to run a script called background_script.rb
in the background, you can use:
ruby background_script.rb &
For more complex shell operations, Ruby
offers the shell
library.
Here is the sample code:
require 'shell'
# Use 'open' to open a URL in your default browser.
sh = Shell.new
sh.open "https://example.com"
Ruby is claimed to treat "everything as an object". But like many languages, Ruby has both primitive and abstract data types.
- Numbers:
- Integers can be of any size (limited by system memory).
- Floating-Point numbers follow the IEEE 754 standard.
- Booleans: Represented by
true
andfalse
. - Symbols: Unique, immutable identifiers represented with a
:
followed by a name.
- Strings: Unicode with multiple encodings.
- Arrays: Ordered, indexed collections.
- Hashes: Key-value pairs, also known as dictionaries or maps in other languages.
Ruby, despite its philosophy of being completely object-oriented, has some underlying primitive paradigms due to its performance concerns and efficiency considerations.
-
nil: Represents 'nothing' or 'empty'. It's the only instance of
NilClass
. -
Booleans: While
true
andfalse
are themselves keywords, any other value in Ruby is considered truthy in a conditional context.
- Rational Numbers: Represented as a fraction (e.g.,
1/3r
). - Complex Numbers: Have real and imaginary parts (e.g.,
2 + 3i
). - Dates and Times: Provide various built-in classes like
Time
for dealing with date and time values.
Ruby shuns a "strictly-typed" system. Variables need not be declared upfront and can be reassigned to different types during execution. This freedom, although liberating, can lead to unexpected behavior, especially in larger codebases.
Ruby features both strings and symbols, each with distinct use cases.
- Type: Strings are of class
String
, while symbols are instances ofSymbol
. - Mutability: Strings are mutable, symbols are not.
- Memory: Symbols are stored as a single, unique object in memory, while each string is unique.
- Performance: As symbols are immutable, lookups are faster than for equivalent strings.
- Strings: For text and dynamic data that may change or be unique across different objects or occurrences.
- Symbols: Typically used as keys for hashes or unique identifiers in the program. They're advantageous for lookup efficiency and when the actual content of the identifier is less relevant than its unique identity.
- As symbols are stored only once in memory, they are memory-efficient in certain scenarios, like using the same symbol across different objects or operations. Be cautious, though, as unnecessarily creating a large number of symbols can lead to memory bloat.
- Strings may be more memory-intensive, especially when there are numerous unique strings. However, they are the right choice when dealing with data that genuinely varies or where mutability is required.
Here is the Ruby code:
# Strings
str_1 = "Hello"
str_2 = "Hello"
puts str_1.object_id == str_2.object_id # Output: false
# Symbols
sym_1 = :hello
sym_2 = :hello
puts sym_1.object_id == sym_2.object_id # Output: true
In Ruby, you declare a constant by using all uppercase letters. Constants are subject to lexical scoping. While reassignment is technically possible (spawning a warning), it should be avoided as a practice.
You can declare a Ruby constant using Object::CONSTANT
notation or by assigning a value directly to an identifier.
# Using Object::CONSTANT notation
Math::PI
# Direct assignment
RADIUS = 5.0
Constants have a global scope, but their visibility can be restricted within classes and modules.
-
Global Scope: Constants are accessible throughout the entire application.
A = 1 # Top level module M puts A # Outputs: 1 end
-
Local Scope: Constants are defined within a module or a class.
module M A = 2 A = 3 puts A # Outputs: 3 end
- Avoid re-assigning constants. Although this generates a warning, the reassignment can still take place, which can lead to unintended behavior.
- For areas where you want to have a constant's value remain unchanged, use
.freeze
on the constant or variable storing the constant's value.
require "warning"
# Generates a warning: already initialized constant
A = 1
A = 2
warning 'constant reassignment'
puts A # Outputs: 2
CIRCLE_AREA = Math::PI * (RADIUS ** 2)
CIRCLE_AREA.freeze
# Any reassignment will generate an error
# CIRCLE_AREA = 100
puts CIRCLE_AREA
Ruby uses both Require and Include to manage dependencies and to mix modules into classes.
-
Purpose: Loads external libraries, enabling access to their defined classes and modules.
-
Execution: Done at the top of the file or script.
-
Trigger: A
LoadError
is raised if the required file is not found. -
State Management: Tracks loaded libraries, subsequently ignoring further
require
calls for the same library.
Here is the Ruby code:
# In file application.rb
require 'my_library'
# In file my_library.rb
class MyLibrary
# ...
end
-
Purpose: Integrates a module's methods within a class, giving the class access to those methods.
-
Execution: On the specific class that necessitates the module's functionality.
-
State: Not applicable for classes, as they can include multiple modules.
- Require: Ensures the presence of the external library before continuing, a basic necessity for external code.
- Include: Mixes in module functionality only when needed, aligning with Rails' convention of using it in the classes contextually.
When it comes to Ruby, iterators allow for easy, streamlined data manipulation. Whether you're working with arrays, ranges, or other data structures, iterators help you efficiently apply operations to each element without needing to manage loop counters.
- Each: The most basic iterator, it goes through each element.
- Each with index: Similar to each, but it also gives the index of the current element.
Here is the Ruby code:
arr = [5, 4, 3, 2, 1]
# Iterating with Each
arr.each { |num| puts num }
# Output:
# 5
# 4
# 3
# 2
# 1
# Iterating with Each with Index
arr.each_with_index { |num, index| puts "#{index}: #{num}" }
# Output:
# 0: 5
# 1: 4
# 2: 3
# 3: 2
# 4: 1
- Each Char: Often used with strings, this iterator loops through each character.
- Each Line: Handy for reading files, it processes lines one at a time.
Here is the Ruby code:
str = "Hello, World!"
# Iterating Each Character
str.each_char { |char| puts char }
# Output:
# H
# e
# l
# l
# o
# ,
# ...
File.open('example.txt').each_line do |line|
puts line
end
These iterators select elements from a collection that match specific conditions. They are typically used in combination with blocks.
Examples include select
, reject
, and grep
. Each is designed to achieve specific selection goals:
select
returns elements that yield true in the block.reject
returns elements that yield false in the block.grep
returns elements that match a specified pattern.
Here is the Ruby code:
# Select even numbers
numbers.select { |num| num.even? }
# Reject short names
names.reject { |name| name.length < 5 }
# Grep to find email addresses
text = "Email me at [email protected]"
text.grep(/\b\w+@\w+\.\w+\b/)
These iterators process the elements and return a result. They include map
, collect
, and partition
.
map
: Transforms each input and returns a new array.collect
: Identical to map, but ops include the return value.partition
: Separates elements into two groups based on whether the block returns true or false.
Here is the Ruby code:
# Double each number
numbers.map { |num| num * 2 }
# Names all uppercase
names.collect { |name| name.upcase }
# Split numbers based on even or odd
numbers.partition { |num| num.even? }
These iterators modify their elements or perform side effects. Examples include each
and each_with_index
.
Often used for their simplicity, do exercise caution as these functions can have unexpected results, especially when combined with unintended side effects.
each
: Processes each element but does not return anything.each_with_index
: Similar to each, but also gives the index of the current element.
When working with ordered collections like arrays or ranges, Ruby provides various sorting options. Common sorting iterators include sort
, sort_by
, and reverse_each
. They all work with blocks to customize the sorting or iteration behavior.
These Ruby constructs are particularly useful in the context of text manipulation, allowing you to repeat characters (such as hyphens for formatting headers) for a specified number of times.
each_line
: Useful when processing multi-line strings or files.each_char
: Ideal for character-level processing in strings or enumerations.downto
: Iterates downwards to a specified value.upto
: Iterates upwards to a specified value.times
: Repeats the associated block a predetermined number of times.step
: Indents a set number of times, confined by a range.cycle
: Used primarily with blocks, it repeatedly moves through the specified range.
Here is the Ruby code:
# Print a line of stars
'*'.upto('*****') { |s| puts s }
# Output:
# *
# **
# ***
# ****
# *****
# Print numbers from 5 to 10, then their squares
5.upto(10) { |num| puts num }
5.upto(10).each { |num| puts num**2 }
Ruby's exception hierarchy enables developers to manage different kinds of errors. The two main exception types cater to a multitude of issues:
- StandardError: For generic problems that occur during code execution.
- SystemCallError: Specifically deals with errors originating from system calls.
Ruby leverages the at_exit
method for centralized error handling. This approach is mainly useful for logging errors before program exit or for cleaning up resources.
at_exit do
puts $!.message if $!
end
Utilize inline rescue, marked by the begin
and end
block. If an exception arises during the evaluation of the enclosed expression, it's caught.
result = begin
raise StandardError, "This is an error"
rescue StandardError => e
"Rescued: #{e.message}"
end
puts result # Output: Rescued: This is an error
Developers benefit from creating their custom exception classes or identifying specific exception types to tailor their error management strategies.
The Exception
class or, more preferably, its subclass, StandardError
, are parents to all user-defined exceptions. This inheritance ensures that all custom exceptions are catchable via rescue
.
class MyCustomError < StandardError
# Additional behavior or settings
end
raise MyCustomError, "Something went wrong!"
An error's distinct nature often demands a corresponding exception. For instance, consider a method handling file operations:
def read_file(file_path)
raise ArgumentError, "File path is empty" if file_path.to_s.empty?
raise IOError, "File not found" unless File.exist?(file_path)
File.read(file_path)
end
Upon calling read_file
, any exception correlated to an invalid file path can be reliably caught and addressed with a targeted rescue
block.
-
Keep it Precise: Make use of granular
rescue
blocks orcase
statements to align the corrective measures with the specific error. -
Maintain a Balance: Overuse of exceptions can convolute code and hinder its readability. Carefully select the exceptions likely to surface and necessitate special attention.
-
Locale Transparency: Choose either a local exception resolver that terminates in the current method or a global one that percolates up the call stack, but aim for consistency.
While exceptions can be invaluable for isolated and unexpected mishaps, triggering and managing them incurs a performance cost. It's typically wiser to leverage them predominantly in such scenarios and not as part of conventional program flow.
Let's set the record straight on the differences between local, instance, and class variables in Ruby.
All three variable types support:
- naming: ![A-Za-z0-9_](2, 50)
- assignment:
variable_name = value
- access control:
public
,private
, andprotected
- immediacy: their scope begins from where they are initialized and exists until the scope ends.
- Scope: Limited to the block where they are defined.
- Life Cycle: Created when the program reaches their definition and destroyed when the block is exited.
Here is the Ruby code:
def hello
name = "Ruby"
puts "Hello, #{name}!" # Output: Hello, Ruby!
end
# Accessing name outside its defined block will cause an error.
# puts name # Will raise an error
An instance variable is prefixed with a single '@' symbol.
- Scope: Primarily within the class, but is accessible from outside the class if the class is instantiated.
- Life Cycle: Created when an object is instantiated and remains available until that particular object is destroyed.
Here is the Ruby code:
class Greeting
def set_name(name)
@name = name
end
def display_greeting
puts "Hello, #{@name}!" # Output: Hello, Ruby!
end
end
greeting_instance = Greeting.new
greeting_instance.set_name("Ruby")
greeting_instance.display_greeting
A class variable is prefixed with two '@' symbols.
- Scope: Within the class and its inheritors but not accessible from outside.
- Life Cycle: Created when assigned within the class or its inheritors and accessible as long as the class or one of its inheritors is in memory.
Here is the Ruby code:
class Employee
@@company_name = "ABC Corporation"
def self.company_name=(name)
@@company_name = name
end
def display_company_name
puts @@company_name
end
end
employee1 = Employee.new
employee2 = Employee.new
# Output: "ABC Corporation" for both employee1 and employee2.
employee1.display_company_name
employee2.display_company_name
Employee.company_name = "New Company" # changes the class variable
# After changing, outputs for both employee1 and employee2 will be "New Company".
employee1.display_company_name
employee2.display_company_name
In Ruby, accessor methods allow you to manipulate object attributes. There are three types of accessor methods: attr_reader
, attr_writer
, and attr_accessor
, each serving a specific role in the attribute's lifecycle
attr_reader
: Generates a simple getter method for an attribute. It can be accessed but not modified externally.attr_writer
: Generates a basic setter method. The attribute can be modified but not read externally.attr_accessor
: Combines both getter and writer methods in one. This creates a full-fledged getter and setter for the attribute.
Here is the Ruby code:
class Person
attr_reader :name, :age
attr_writer :name, :age
def initialize(name, age)
@name = name
@age = age
end
end
person = Person.new("Alice", 30)
person.name # Returns "Alice"
person.name = "Bob" # Error: undefined method 'name='
person.age # Returns 30
person.age = 35 # Error: undefined method 'age='
person.instance_variables # Returns [:@name, :@age]
Ruby employs automatic memory management, which is primarily influenced by garbage collection techniques. Let's understand the specifics.
-
Step 1 - Mark: The process starts from the root of object references. The GC traverses memory, marking referenced objects.
-
Step 2 - Sweep: It scans for unmarked objects and reclaims their memory, making it available for future use.
To optimize the Mark-and-Sweep approach, Ruby introduces generational garbage collection.
-
Focused on Object Age: Objects are classified based on their age.
-
Young vs. Old Objects:
- New objects start in the Young Generation.
- Objects that persist multiple GC cycles move to the Old Generation.
-
Collection Frequency: The Young Generation is collected more frequently.
-
Short- and Long-Lived Object Management: It's easier to collect younger objects, reducing the scope and overhead of a complete garbage collection cycle.
Although CPython uses reference-counting to track object lifespans, Ruby typically does not.
-
ObjectSpace: It's a module that allows retrieval of all objects.
However, note that modern Ruby versions represent a hybrid system, sensitive to object types and context.
Here is the Ruby code:
# Enable trashcan (Ruby 2.6 onwards)
ObjectSpace::count_objects[:FREE] > 100_000 && GC.start
# Ruby versions before 2.6
GC.start
Let me go through the major difference.
-
Input Requirement:
gets.chomp
removes all trailing whitespace and the newline character.gets.strip
eliminates all leading and trailing whitespace, including the newline character.
-
Use Cases:
gets.chomp
: Suited when you anticipate or require specific trailing characters to be preserved.gets.strip
: Ideal for scenarios where you need to sanitize or validate user input by removing any extra leading or trailing spaces.
Here is the Ruby code:
# Using the gets.chomp method
puts "Enter your name (including a trailing space): "
name_chomp = gets.chomp
puts "Name with trailing space: #{name_chomp}"
# Using the gets.strip method
puts "Enter your name: "
name_strip = gets.strip
puts "Name without trailing space: #{name_strip}"
In Ruby, self
serves as a "mirror" that reflects the current context. Depending on where it's used, self
can represent different objects.
Here's the breakdown:
In this context, self
refers to the instance of the object on which the method is called.
Consider the following code:
class MyClass
def instance_method
puts self
end
end
object = MyClass.new
object.instance_method
The output would be the object reference #<MyClass:0x007fb4fa869358>
.
Within a class definition, self
refers to the class itself. This is why you use self.method_name
to define class methods.
For instance:
class MyClass
def self.class_method
puts self
end
end
MyClass.class_method
The output will be the class MyClass
.
When a method is missing due to a typo or other reason, Ruby executes method_missing
which can help handle such cases.
Consider this example:
class MyClass
def method_missing(m, *args)
puts "There's no method called #{m}"
end
def test_method
method_thaat_doesnt_exist
end
end
Calling test_method
will invoke method_missing
with the method name "method_thaat_doesnt_exist"
.
Convention over Configuration (CoC) is a software design principle that simplifies development by reducing the number of decisions developers need to make.
In its essence, CoC means that frameworks come with best practice defaults or "conventions" that are automatically applied unless explicitly configured to behave differently.
- Code-Base Structures: Many Ruby web frameworks, like Ruby on Rails or Sinatra, expect a certain directory structure that groups related files.
- Naming Conventions: Specially designed rules for naming classes, methods, and databases to help in identification and automatic linking.
- API Endpoints: Through consistent naming, it's possible to infer routing information in web applications.
- Database Schemas: Named fields and tables allow the ORM to deduce relationships and configurations.
In Ruby on Rails, the "conventions" for a resourceful route automatically map HTTP verbs to CRUD actions:
# config/routes.rb
resources :articles
# Routes:
# HTTP Path Controller#Action Used For
# ------------------------------------------------------------
# GET /articles articles#index Display a list
# GET /articles/:id articles#show Display a specific article
# GET /articles/new articles#new Display a form to create a new article
# POST /articles articles#create Add a new article to the database
# GET /articles/:id/edit articles#edit Display a form to edit an existing article
# PATCH /articles/:id articles#update Update an existing article in the database
# PUT /articles/:id articles#update (Alternate for update)
# DELETE /articles/:id articles#destroy Remove a specific article from the database
Here, the convention to map action names to routes frees the developer from configuring each route manually.
- Speed: It streamlines development and reduces boilerplate.
- Interoperability: CoC enables consistency across different projects and teams.
- Over-optimization: While it's efficient for simple, well-understood requirements, it can make advanced configurations and customizations cumbersome.
- Learning Curve: Newcomers might find it challenging to adapt to these standard conventions.
- Magic: Over-reliance on CoC can make the system seem like it has hidden, unexplained behaviors.
Ruby offers powerful metaprogramming capabilities, enabling developers to write flexible, dynamic code. Key to Ruby's metaprogramming are class
methods such as define_method
and language features like Open Classes
leading to advanced techniques like Dynamic Dispatch
.
- Dynamic Dispatch: Methods can be called at runtime, based on the object's context, using
send
. This makes it easier to manage method invocations in metaprogrammed code.
class MathOperations
def do_operation(operator, x, y)
send(operator, x, y) # Dynamic dispatch
end
private
def add(x, y)
x + y
end
def subtract(x, y)
x - y
end
end
result = MathOperations.new.do_operation(:add, 10, 5) # 15
-
Open Classes: Ruby allows changing a class's definition dynamically, even after its initial declaration.
This example adds a
reverse
method to theString
class.class String def reverse chars.reverse.join end end
-
Code Evaluation: Code strings can be executed within a bound context, enabling runtime code execution and evaluation.
This is an example using
eval
to define a method at runtime, equivalent todef double(x) x * 2; end
, but the method signature is constructed dynamically.method_signature = 'double(x)' method_body = 'x * 2' eval("def #{method_signature}; #{method_body}; end")
-
Binding Tasks:
proc
objects capture both the method (or block) and its associated context. They can be transferred across lexical scopes, allowing delayed execution of code.context = binding task = Proc.new { eval 'some_method', context }
-
Context Toggling: By toggling a method's visibility, you can control its access scope.
class MyClass def some_method "Public method" end private def toggle_method_visibility(visibility) # `send` here is being used for dynamic dispatch send(visibility, :some_method) end end instance = MyClass.new instance.toggle_method_visibility(:private)
-
Localizing Method Calls: In internationalization tasks where method calls need to be localized,
send
,public_send
, or even the more generaleval
can be suitable.def greeting(language) eval("#{language}_greeting") end def spanish_greeting "Hola Mundo" end
-
Method Missing: This feature is the heart of Ruby's duck typing. It allows classes and objects to respond to method calls even when their definitions are absent, rather than resorting to method-not-found errors.
This example cleans up a method call, removing spaces or underscores.
def method_missing(name, *args, &block) cleaned_name = name.to_s.delete(' ').delete('_') send(cleaned_name, *args, &block) end
-
respond_to_missing?
: This method is often used in conjunction withmethod_missing
, providing a way for a class to communicate whether it handles a method call beyond what is statically defined.