CrackedRuby logo

CrackedRuby

Operator Methods

Overview

Ruby treats operators as syntactic sugar for method calls, allowing objects to define custom behavior for operations like +, -, ==, and []. When Ruby encounters an expression like a + b, it translates this to the method call a.+(b). This mechanism enables objects to participate naturally in mathematical expressions, comparisons, and other operations while maintaining Ruby's object-oriented foundation.

The standard library defines operator methods across core classes including Numeric, String, Array, Hash, and others. Custom classes can implement these methods to integrate seamlessly with Ruby's operator syntax. Ruby recognizes specific method names that correspond to operators, and the parser automatically translates operator expressions into the appropriate method calls.

class Vector
  def initialize(x, y)
    @x, @y = x, y
  end
  
  def +(other)
    Vector.new(@x + other.x, @y + other.y)
  end
  
  def to_s
    "(#{@x}, #{@y})"
  end
  
  attr_reader :x, :y
end

v1 = Vector.new(1, 2)
v2 = Vector.new(3, 4)
result = v1 + v2  # Calls v1.+(v2)
# => Vector object representing (4, 6)

Ruby categorizes operator methods into several groups: arithmetic operators (+, -, *, /, **, %), comparison operators (==, !=, <, >, <=, >=, <=>), bitwise operators (&, |, ^, ~, <<, >>), and special operators ([], []=, =~). Each category serves different purposes in Ruby's type system and method resolution.

# Arithmetic operators are methods
5.+(3)        # Same as 5 + 3
"hello".+(" world")  # Same as "hello" + " world"

# Comparison operators enable sorting
[3, 1, 4].sort   # Uses <=> method internally

# Bitwise operators work on integers and can be customized
class Flags
  def initialize(value)
    @value = value
  end
  
  def &(other)
    Flags.new(@value & other.value)
  end
  
  attr_reader :value
end

The operator method system integrates with Ruby's method lookup chain, inheritance, and module inclusion. Objects inherit operator behaviors from their class hierarchy and can override methods to provide specialized implementations. This design maintains consistency between operator expressions and regular method calls while enabling domain-specific operator semantics.

Basic Usage

Implementing operator methods requires defining methods with specific names that correspond to Ruby's recognized operators. The method names use Ruby's operator symbols, with special naming conventions for operators that cannot appear in method names directly.

class Money
  def initialize(amount, currency = :USD)
    @amount, @currency = amount, currency
  end
  
  def +(other)
    raise TypeError, "Currency mismatch" unless @currency == other.currency
    Money.new(@amount + other.amount, @currency)
  end
  
  def -(other)
    raise TypeError, "Currency mismatch" unless @currency == other.currency
    Money.new(@amount - other.amount, @currency)
  end
  
  def ==(other)
    @amount == other.amount && @currency == other.currency
  end
  
  def >(other)
    raise TypeError, "Currency mismatch" unless @currency == other.currency
    @amount > other.amount
  end
  
  attr_reader :amount, :currency
end

price1 = Money.new(100)
price2 = Money.new(50)
total = price1 + price2  # Money.new(150, :USD)
difference = price1 - price2  # Money.new(50, :USD)

Unary operators require special method names since Ruby distinguishes between unary and binary versions of operators like + and -. Unary operators append @ to the operator symbol to create the method name.

class Temperature
  def initialize(celsius)
    @celsius = celsius
  end
  
  def +@  # Unary plus
    self
  end
  
  def -@  # Unary minus
    Temperature.new(-@celsius)
  end
  
  def to_s
    "#{@celsius}°C"
  end
end

temp = Temperature.new(25)
positive = +temp  # Calls temp.+@
negative = -temp  # Calls temp.-@, returns Temperature.new(-25)

The spaceship operator <=> serves as the foundation for comparison operations in Ruby. Many comparison methods can be automatically generated by including the Comparable module and defining only <=>.

class Version
  include Comparable
  
  def initialize(version_string)
    @parts = version_string.split('.').map(&:to_i)
  end
  
  def <=>(other)
    @parts <=> other.parts
  end
  
  def to_s
    @parts.join('.')
  end
  
  protected
  
  attr_reader :parts
end

v1 = Version.new("2.1.0")
v2 = Version.new("2.0.5")
v3 = Version.new("2.1.0")

v1 > v2   # true (uses <=> internally)
v1 == v3  # true (uses <=> internally)
[v2, v1, v3].sort  # Sorted array using <=>

Index operators [] and []= enable objects to behave like collections or provide key-based access to internal data. These methods can accept multiple arguments and implement complex indexing schemes.

class Matrix
  def initialize(rows, cols)
    @data = Array.new(rows) { Array.new(cols, 0) }
    @rows, @cols = rows, cols
  end
  
  def [](row, col)
    validate_indices(row, col)
    @data[row][col]
  end
  
  def []=(row, col, value)
    validate_indices(row, col)
    @data[row][col] = value
  end
  
  private
  
  def validate_indices(row, col)
    raise IndexError if row < 0 || row >= @rows || col < 0 || col >= @cols
  end
end

matrix = Matrix.new(3, 3)
matrix[1, 1] = 42  # Calls matrix.[]=(1, 1, 42)
value = matrix[1, 1]  # Calls matrix.[](1, 1), returns 42

Advanced Usage

Operator methods support complex patterns including method chaining, operator precedence manipulation, and integration with metaprogramming techniques. Ruby's operator precedence remains fixed, but custom operator methods can create sophisticated behaviors that work within this constraint.

Method chaining with operators enables fluent interfaces where operations can be composed naturally. The key consideration involves ensuring operator methods return objects that support subsequent operations in the chain.

class QueryBuilder
  def initialize(table = nil)
    @table = table
    @conditions = []
    @joins = []
    @order = nil
  end
  
  def <<(condition)
    clone_with { |q| q.conditions << condition }
  end
  
  def +(join_clause)
    clone_with { |q| q.joins << join_clause }
  end
  
  def >(order_clause)
    clone_with { |q| q.order = order_clause }
  end
  
  def to_sql
    sql = "SELECT * FROM #{@table}"
    sql += " #{@joins.join(' ')}" unless @joins.empty?
    sql += " WHERE #{@conditions.join(' AND ')}" unless @conditions.empty?
    sql += " ORDER BY #{@order}" if @order
    sql
  end
  
  private
  
  attr_accessor :conditions, :joins, :order
  
  def clone_with
    new_query = dup
    new_query.conditions = @conditions.dup
    new_query.joins = @joins.dup
    yield(new_query)
    new_query
  end
end

query = QueryBuilder.new('users')
result = query << "age > 18" << "active = true" + "JOIN profiles ON users.id = profiles.user_id" > "created_at DESC"
# Creates complex query through operator chaining

Operators can implement domain-specific languages by overloading their meaning within particular contexts. This technique requires careful consideration of operator precedence and associativity to ensure expressions evaluate as intended.

class LogicGate
  def initialize(value)
    @value = value
  end
  
  def &(other)  # AND gate
    LogicGate.new(@value && other.value)
  end
  
  def |(other)  # OR gate  
    LogicGate.new(@value || other.value)
  end
  
  def ^(other)  # XOR gate
    LogicGate.new(@value ^ other.value)
  end
  
  def !@       # NOT gate (unary)
    LogicGate.new(!@value)
  end
  
  def to_bool
    @value
  end
  
  protected
  
  attr_reader :value
end

# Circuit logic using operators
a = LogicGate.new(true)
b = LogicGate.new(false)
c = LogicGate.new(true)

# Complex logic expression
result = (a & b) | (c ^ !@a)
# Evaluates: (true && false) || (true ^ !true) = false || false = false

Metaprogramming techniques can generate operator methods dynamically, creating families of related operators or delegating operations to internal objects. This approach maintains code organization while providing extensive operator interfaces.

class NumericWrapper
  OPERATORS = [:+, :-, :*, :/, :%, :**].freeze
  COMPARISONS = [:==, :!=, :<, :>, :<=, :>=, :<=>].freeze
  
  def initialize(value)
    @value = value
  end
  
  # Generate arithmetic operators
  OPERATORS.each do |op|
    define_method(op) do |other|
      other_value = other.respond_to?(:value) ? other.value : other
      result = @value.send(op, other_value)
      self.class.new(result)
    end
  end
  
  # Generate comparison operators  
  COMPARISONS.each do |op|
    define_method(op) do |other|
      other_value = other.respond_to?(:value) ? other.value : other
      @value.send(op, other_value)
    end
  end
  
  def to_s
    @value.to_s
  end
  
  attr_reader :value
end

wrapped = NumericWrapper.new(10)
result = wrapped + 5 * 2  # Chain of operations
greater = wrapped > 8     # Comparison operations

Operator method implementations can leverage Ruby's dispatch system to create polymorphic behaviors that work across different object types while maintaining type safety and appropriate error handling.

class Polynomial
  def initialize(coefficients)
    @coefficients = coefficients.dup.freeze
  end
  
  def +(other)
    case other
    when Polynomial
      add_polynomials(other)
    when Numeric
      add_constant(other)
    else
      raise TypeError, "Cannot add #{other.class} to Polynomial"
    end
  end
  
  def *(other)
    case other
    when Polynomial
      multiply_polynomials(other)
    when Numeric
      multiply_constant(other)
    else
      raise TypeError, "Cannot multiply Polynomial by #{other.class}"
    end
  end
  
  private
  
  def add_polynomials(other)
    max_degree = [@coefficients.length, other.coefficients.length].max
    result_coeffs = Array.new(max_degree, 0)
    
    @coefficients.each_with_index { |coeff, i| result_coeffs[i] += coeff }
    other.coefficients.each_with_index { |coeff, i| result_coeffs[i] += coeff }
    
    Polynomial.new(result_coeffs)
  end
  
  def add_constant(value)
    result_coeffs = @coefficients.dup
    result_coeffs[0] = (result_coeffs[0] || 0) + value
    Polynomial.new(result_coeffs)
  end
  
  def multiply_constant(value)
    Polynomial.new(@coefficients.map { |coeff| coeff * value })
  end
  
  def multiply_polynomials(other)
    result_degree = @coefficients.length + other.coefficients.length - 1
    result_coeffs = Array.new(result_degree, 0)
    
    @coefficients.each_with_index do |a_coeff, i|
      other.coefficients.each_with_index do |b_coeff, j|
        result_coeffs[i + j] += a_coeff * b_coeff
      end
    end
    
    Polynomial.new(result_coeffs)
  end
  
  protected
  
  attr_reader :coefficients
end

Common Pitfalls

Operator precedence in Ruby follows fixed rules that cannot be changed by custom implementations. Developers often expect custom operators to behave according to mathematical conventions, but Ruby's precedence rules always apply regardless of the operator's semantic meaning.

class Measurement
  def initialize(value, unit)
    @value, @unit = value, unit
  end
  
  def +(other)
    # Developers might expect this to have higher precedence than *
    # but Ruby's precedence rules still apply
    Measurement.new(@value + other.value, @unit)
  end
  
  def *(scalar)
    Measurement.new(@value * scalar, @unit)  
  end
  
  attr_reader :value, :unit
end

m1 = Measurement.new(10, :meters)
m2 = Measurement.new(5, :meters)

# This expression follows Ruby precedence, not mathematical precedence
result = m1 + m2 * 2  # Evaluates as m1 + (m2 * 2), not (m1 + m2) * 2
# Result: 10 + (5 * 2) = 20 meters

# To get mathematical precedence, use explicit parentheses
result = (m1 + m2) * 2  # Evaluates as (15) * 2 = 30 meters

The equality operator == does not automatically implement != in custom classes. While many core Ruby classes provide both, custom implementations must explicitly define both operators or risk unexpected behavior with negated equality tests.

class Person
  def initialize(name, age)
    @name, @age = name, age
  end
  
  def ==(other)
    return false unless other.is_a?(Person)
    @name == other.name && @age == other.age
  end
  
  # Missing != implementation causes problems
end

person1 = Person.new("Alice", 30)
person2 = Person.new("Bob", 25)
person3 = Person.new("Alice", 30)

person1 == person3  # true (custom implementation)
person1 != person2  # Uses default != which calls !(person1 == person2)
person1 != person3  # false (works correctly by negating ==)

# However, this becomes problematic with inheritance or mixed types
class Employee < Person
  def ==(other)
    super && other.is_a?(Employee)
  end
end

emp = Employee.new("Alice", 30)
person1 != emp  # May not behave as expected due to asymmetric comparison

Type coercion in operator methods requires careful handling to avoid infinite recursion and maintain consistency across different operand orders. Ruby does not automatically handle commutative operations.

class Matrix
  def initialize(data)
    @data = data
  end
  
  def *(other)
    case other
    when Matrix
      matrix_multiply(other)
    when Numeric
      scalar_multiply(other)
    else
      # Attempt coercion - this can cause problems
      if other.respond_to?(:coerce)
        a, b = other.coerce(self)
        a * b
      else
        raise TypeError, "Cannot multiply Matrix by #{other.class}"
      end
    end
  end
  
  private
  
  def matrix_multiply(other)
    # Matrix multiplication implementation
  end
  
  def scalar_multiply(scalar)
    # Scalar multiplication implementation  
  end
end

# Problem: 2 * matrix will not work unless Numeric#coerce handles Matrix
# Solution: Implement coerce method in Matrix
class Matrix
  def coerce(other)
    case other
    when Numeric
      [self, other]  # Return [receiver, other] for self.*(other)
    else
      raise TypeError, "Cannot coerce #{other.class} with Matrix"
    end
  end
end

Method aliasing and inheritance can create subtle bugs when operator methods are redefined or overridden. The interaction between original methods, aliases, and subclass overrides may not behave intuitively.

class BaseNumber
  def initialize(value)
    @value = value
  end
  
  def +(other)
    self.class.new(@value + other.value)
  end
  
  alias_method :add, :+
  
  attr_reader :value
end

class ModularNumber < BaseNumber
  def initialize(value, modulus)
    super(value)
    @modulus = modulus
  end
  
  def +(other)
    # Override + but not add
    result = (@value + other.value) % @modulus
    self.class.new(result, @modulus)
  end
  
  attr_reader :modulus
end

base = BaseNumber.new(10)
mod = ModularNumber.new(8, 12)

# These may produce different results unexpectedly
result1 = mod + BaseNumber.new(5)  # Uses ModularNumber#+
result2 = mod.add(BaseNumber.new(5))  # Uses BaseNumber#+ via alias!

Operator methods can break object immutability expectations when they modify the receiver instead of returning new objects. This pattern contradicts Ruby's typical operator semantics and can cause surprising behavior.

class MutableArray
  def initialize(elements)
    @elements = elements
  end
  
  def +(other)
    # Bad: modifies receiver instead of returning new object
    @elements.concat(other.elements)
    self  # Returns self instead of new object
  end
  
  def <<(element)
    # Acceptable: << typically modifies receiver
    @elements << element
    self
  end
  
  attr_reader :elements
end

arr1 = MutableArray.new([1, 2, 3])
arr2 = MutableArray.new([4, 5, 6])
original_arr1 = arr1

result = arr1 + arr2  # Expecting new object
# arr1 is now modified! This violates expectations for +
# result and arr1 reference the same object
# original_arr1 also references the modified object

Performance & Memory

Operator methods frequently appear in tight loops and mathematical computations, making their performance characteristics crucial for application efficiency. Ruby's method dispatch overhead affects operator performance, particularly when objects implement complex operator behaviors or perform type checking.

require 'benchmark'

class SimpleNumeric
  def initialize(value)
    @value = value
  end
  
  def +(other)
    SimpleNumeric.new(@value + other.value)
  end
  
  attr_reader :value
end

class ComplexNumeric  
  def initialize(value)
    @value = value
  end
  
  def +(other)
    # Complex type checking and validation
    case other
    when ComplexNumeric
      result = perform_calculation(other)
    when Numeric
      result = @value + other
    else
      raise TypeError, "Invalid operand type"
    end
    
    ComplexNumeric.new(result)
  end
  
  private
  
  def perform_calculation(other)
    # Additional computation overhead
    (@value + other.value).tap do |result|
      validate_result(result)
    end
  end
  
  def validate_result(result)
    # Validation logic
  end
  
  attr_reader :value
end

# Performance comparison
iterations = 1_000_000
simple_nums = Array.new(100) { |i| SimpleNumeric.new(i) }
complex_nums = Array.new(100) { |i| ComplexNumeric.new(i) }

Benchmark.bm(15) do |x|
  x.report("Native Fixnum:") do
    iterations.times do |i|
      result = (i % 100) + ((i + 1) % 100)
    end
  end
  
  x.report("SimpleNumeric:") do
    iterations.times do |i|
      result = simple_nums[i % 100] + simple_nums[(i + 1) % 100]
    end
  end
  
  x.report("ComplexNumeric:") do
    iterations.times do |i|
      result = complex_nums[i % 100] + complex_nums[(i + 1) % 100]
    end
  end
end

Memory allocation patterns in operator methods significantly impact garbage collection pressure. Methods that create new objects for each operation can generate substantial garbage, while in-place modification approaches may improve performance but violate immutability expectations.

class Vector3D
  def initialize(x, y, z)
    @x, @y, @z = x, y, z
  end
  
  # Memory-intensive approach: always creates new objects
  def +(other)
    Vector3D.new(@x + other.x, @y + other.y, @z + other.z)
  end
  
  # Memory-efficient approach: reuses object when possible
  def add!(other)
    @x += other.x
    @y += other.y  
    @z += other.z
    self
  end
  
  # Hybrid approach: provides both options
  def add(other, in_place: false)
    if in_place
      add!(other)
    else
      self + other
    end
  end
  
  attr_reader :x, :y, :z
end

# Memory usage comparison for vector operations
vectors = Array.new(10000) { Vector3D.new(rand, rand, rand) }

# High garbage generation
result_vectors = vectors.each_cons(2).map { |v1, v2| v1 + v2 }

# Lower garbage generation  
vectors.each_cons(2) { |v1, v2| v1.add!(v2) }

Operator chaining can create intermediate objects that consume memory unnecessarily. Implementing lazy evaluation or expression templates can defer computation and reduce memory overhead for complex operator expressions.

class LazyExpression
  def initialize(value)
    @operations = []
    @initial_value = value
  end
  
  def +(other)
    clone_with_operation(:+, other)
  end
  
  def -(other)
    clone_with_operation(:-, other)
  end
  
  def *(other)
    clone_with_operation(:*, other)
  end
  
  def evaluate
    @operations.inject(@initial_value) do |acc, (op, operand)|
      acc.send(op, operand)
    end
  end
  
  private
  
  def clone_with_operation(operator, operand)
    new_expr = dup
    new_expr.instance_variable_set(:@operations, @operations + [[operator, operand]])
    new_expr
  end
end

# Deferred computation reduces intermediate object creation
expr = LazyExpression.new(10)
complex_expr = expr + 5 - 3 * 2 + 7  # No computation yet
result = complex_expr.evaluate  # Single evaluation pass

Polymorphic operator methods that handle multiple types require performance considerations for type dispatch. Implementing efficient type checking and avoiding repeated type detection can improve operator performance in polymorphic scenarios.

class OptimizedCalculator
  # Cache type checks to avoid repeated introspection
  TYPE_HANDLERS = {
    Integer => :handle_integer,
    Float => :handle_float,  
    Rational => :handle_rational,
    Complex => :handle_complex
  }.freeze
  
  def initialize(value)
    @value = value
    @type_handler = determine_handler(value)
  end
  
  def +(other)
    other_handler = determine_handler(other)
    
    # Fast path for same types
    if @type_handler == other_handler
      fast_add(other)
    else
      # Slower path for mixed types
      mixed_type_add(other)
    end
  end
  
  private
  
  def determine_handler(value)
    # Use direct class lookup instead of is_a? checks
    TYPE_HANDLERS[value.class] || :handle_generic
  end
  
  def fast_add(other)
    OptimizedCalculator.new(@value + other.value)
  end
  
  def mixed_type_add(other)
    # Handle type conversion and mixed arithmetic
    result = convert_and_add(@value, other.value)
    OptimizedCalculator.new(result)
  end
  
  def convert_and_add(a, b)
    # Optimized type conversion logic
    a + b
  end
  
  attr_reader :value
end

Reference

Arithmetic Operators

Method Operator Parameters Returns Description
#+ + other (Object) Object Addition operation
#- - other (Object) Object Subtraction operation
#* * other (Object) Object Multiplication operation
#/ / other (Object) Object Division operation
#% % other (Object) Object Modulo operation
#** ** other (Object) Object Exponentiation operation
#+@ +obj None Object Unary plus operation
#-@ -obj None Object Unary minus operation

Comparison Operators

Method Operator Parameters Returns Description
#== == other (Object) Boolean Equality comparison
#!= != other (Object) Boolean Inequality comparison
#< < other (Object) Boolean Less than comparison
#> > other (Object) Boolean Greater than comparison
#<= <= other (Object) Boolean Less than or equal comparison
#>= >= other (Object) Boolean Greater than or equal comparison
#<=> <=> other (Object) Integer Spaceship operator (-1, 0, 1, or nil)
#=== === other (Object) Boolean Case equality comparison

Bitwise Operators

Method Operator Parameters Returns Description
#& & other (Object) Object Bitwise AND operation
#| | other (Object) Object Bitwise OR operation
#^ ^ other (Object) Object Bitwise XOR operation
#~ ~obj None Object Bitwise NOT operation
#<< << other (Object) Object Left shift operation
#>> >> other (Object) Object Right shift operation

Index and Access Operators

Method Operator Parameters Returns Description
#[] obj[key] Variable arguments Object Element access operation
#[]= obj[key] = value Variable arguments, value Object Element assignment operation
#=~ =~ other (Object) Integer or nil Pattern matching operation

Special Method Behaviors

Method Name Purpose Common Usage Return Expectations
coerce Type conversion for mixed operations Called when left operand cannot handle right operand Array containing [converted_other, self]
to_int Integer conversion Implicit conversion contexts Integer
to_str String conversion String operations and interpolation String
to_ary Array conversion Array operations and assignments Array
respond_to? Method availability check Before calling operator methods Boolean

Operator Precedence (Highest to Lowest)

Precedence Operators Associativity Description
1 [] Left Index access
2 ** Right Exponentiation
3 +@ -@ Right Unary plus and minus
4 * / % Left Multiplication, division, modulo
5 + - Left Addition and subtraction
6 << >> Left Bitwise shift operators
7 & Left Bitwise AND
8 | ^ Left Bitwise OR and XOR
9 < <= > >= Left Comparison operators
10 <=> == === != =~ !~ Left Equality and pattern matching
11 && Left Logical AND
12 || Left Logical OR

Common Implementation Patterns

Pattern Use Case Example Implementation
Numeric Operations Mathematical objects Return same class with computed value
Collection Operations Container classes Modify or combine collection contents
Builder Pattern Fluent interfaces Return self or new builder instance
Comparison Chain Sortable objects Implement <=> and include Comparable
Type Dispatch Polymorphic operations Case statement on operand class/type
Coercion Protocol Mixed-type operations Implement coerce method for type conversion

Error Handling Patterns

Error Type When to Raise Recommended Message Format
TypeError Incompatible operand types "Cannot #{operator} #{self.class} with #{other.class}"
ArgumentError Invalid operand values "Invalid operand value: #{other.inspect}"
ZeroDivisionError Division by zero "Division by zero in #{self.class}##{operator}"
RangeError Result outside valid range "Result #{result} outside valid range"
NoMethodError Missing required methods Let Ruby raise automatically

Memory and Performance Considerations

Consideration Impact Mitigation Strategy
Object Creation High GC pressure Implement in-place variants when appropriate
Type Checking Method dispatch overhead Cache type information or use fast type detection
Operator Chaining Intermediate object creation Consider lazy evaluation or expression templates
Polymorphic Dispatch Runtime type resolution Implement efficient type dispatch mechanisms
Method Lookup Inheritance chain traversal Keep operator method definitions in immediate class