CrackedRuby logo

CrackedRuby

Operator Precedence and Associativity

Overview

Operator precedence and associativity control how Ruby parses and evaluates expressions containing multiple operators. Precedence determines which operators bind more tightly than others, while associativity determines the evaluation order for operators of equal precedence.

Ruby implements a fixed precedence hierarchy where operators like ** (exponentiation) have higher precedence than * (multiplication), which has higher precedence than + (addition). When operators have equal precedence, associativity rules determine evaluation order - most operators associate left-to-right, but some like ** and assignment operators associate right-to-left.

result = 2 + 3 * 4
# => 14 (not 20, because * has higher precedence than +)

result = 2 ** 3 ** 2
# => 512 (right associative: 2 ** (3 ** 2) = 2 ** 9)

result = 10 - 5 - 2
# => 3 (left associative: (10 - 5) - 2)

Ruby treats many method calls as operators, including comparison methods (<, >), equality methods (==, !=), and custom operators defined on objects. The precedence system applies consistently across built-in operators and method-based operators.

Understanding precedence prevents subtle bugs in complex expressions and clarifies when parentheses are necessary versus optional. Ruby's precedence closely follows mathematical conventions but includes additional rules for logical operators, assignment, and method calls that don't exist in pure mathematics.

Basic Usage

The most commonly encountered precedence relationships involve arithmetic, comparison, and logical operators. Arithmetic operators follow mathematical precedence with multiplication and division binding tighter than addition and subtraction.

# Arithmetic precedence
result = 5 + 2 * 3
# => 11 (evaluated as 5 + (2 * 3))

result = 20 / 4 + 3
# => 8 (evaluated as (20 / 4) + 3)

result = 2 + 3 * 4 - 1
# => 13 (evaluated as 2 + (3 * 4) - 1 = 2 + 12 - 1)

Comparison operators have lower precedence than arithmetic but higher than logical operators. This allows natural expression of numeric comparisons within logical contexts.

# Comparison and logical precedence
result = 5 + 3 > 2 * 4 && true
# => false (evaluated as (5 + 3) > (2 * 4) && true = 8 > 8 && true)

result = 10 % 3 == 1 || false
# => true (evaluated as (10 % 3) == 1 || false = 1 == 1 || false)

Assignment operators have very low precedence, ensuring the right side evaluates completely before assignment occurs. Multiple assignments associate right-to-left.

# Assignment precedence and associativity
x = y = 5 + 3
# Evaluated as x = (y = (5 + 3))
# Both x and y equal 8

a = b = c = [1, 2, 3]
# All variables reference the same array object

Method calls that use operator syntax follow the same precedence rules as built-in operators. Array indexing ([]) and method calls have high precedence.

array = [10, 20, 30]
result = array[1] + 5
# => 25 (array[1] evaluated first, then added to 5)

string = "hello"
result = string.length * 2 + 1
# => 11 (string.length evaluated first: 5 * 2 + 1)

Parentheses override precedence rules and create explicit evaluation order. Use parentheses when the natural precedence doesn't match intended logic or when clarifying complex expressions.

# Parentheses override precedence
result = (5 + 3) * 2
# => 16 (addition performed before multiplication)

result = 2 ** (3 + 1)
# => 16 (addition performed before exponentiation)

# Complex expression with explicit grouping
result = (a + b) * (c - d) / (e + f)

Advanced Usage

Complex expressions often involve multiple precedence levels, method chaining, and custom operators. Understanding how Ruby parses these constructs prevents unexpected behavior in sophisticated code.

Range operators have unique precedence characteristics. The inclusive range (..) and exclusive range (...) bind tighter than most operators but looser than method calls.

# Range operator precedence
numbers = (1..5).to_a
# => [1, 2, 3, 4, 5]

# Range with arithmetic requires parentheses
range = (0..10-1).to_a
# => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

# Without parentheses, binds differently
range = 0..10-1.to_a
# Equivalent to: 0..(10 - (1.to_a))
# Raises NoMethodError: undefined method `-' for [1]:Array

Conditional assignment operators (||=, &&=) combine logical operators with assignment. These have assignment precedence but use the logical operator for evaluation.

class Configuration
  attr_accessor :timeout, :retries, :debug

  def initialize
    @timeout ||= 30
    @retries ||= 3
    @debug &&= ENV['DEBUG'] == 'true'
  end
end

# Method chaining with mixed precedence
result = [1, 2, 3, 4, 5]
  .select { |n| n.even? }
  .map { |n| n * 2 }
  .reduce(:+) || 0
# => 12 (method calls have higher precedence than ||)

Custom operators defined through method definitions follow the same precedence rules as their symbolic equivalents. The operator symbol determines precedence, not the method implementation.

class Vector
  attr_reader :x, :y

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

  def +(other)
    Vector.new(@x + other.x, @y + other.y)
  end

  def *(scalar)
    Vector.new(@x * scalar, @y * scalar)
  end

  def magnitude
    Math.sqrt(@x ** 2 + @y ** 2)
  end
end

v1 = Vector.new(1, 2)
v2 = Vector.new(3, 4)

# Custom operators follow standard precedence
result = v1 + v2 * 2
# Equivalent to: v1 + (v2 * 2)
# Vector.new(1, 2) + Vector.new(6, 8) = Vector.new(7, 10)

magnitude = (v1 + v2).magnitude
# Parentheses required to add vectors before calculating magnitude

The splat operator (*) and double splat (**) have special precedence in method calls and array/hash contexts. They bind tightly to their operands but interact complexly with other operators.

def process_values(*args, **kwargs)
  puts "Args: #{args.inspect}"
  puts "Kwargs: #{kwargs.inspect}"
end

# Splat operator precedence in calls
values = [1, 2, 3]
options = { debug: true, verbose: false }

process_values(*values, **options, extra: "value")
# Splat operators evaluate before method call

# Complex expression with splatting
arrays = [[1, 2], [3, 4], [5, 6]]
flattened = [*arrays[0], *arrays[1], *arrays[2]]
# => [1, 2, 3, 4, 5, 6]

Common Pitfalls

Operator precedence creates subtle bugs when developers rely on intuition rather than Ruby's actual precedence rules. The most common mistakes involve logical operators, assignment, and method calls.

Logical operators && and || have much lower precedence than comparison operators, leading to unexpected grouping in conditional expressions.

# Common mistake with logical operator precedence
user_active = true
user_admin = false

# WRONG: Intended to check (user_active && user_admin) || user_admin
if user_active && user_admin || user_admin
  puts "Access granted"
end
# Evaluated as: user_active && (user_admin || user_admin)
# => false && (false || false) = false

# CORRECT: Use parentheses for intended logic
if (user_active && user_admin) || user_admin
  puts "Access granted"
end
# => (true && false) || false = false || false = false

# Another common pattern
status = "active"
role = "user"

# WRONG: Comparison operators bind tighter than &&
if status == "active" && role == "admin" || role == "user"
  puts "Valid user"
end
# Evaluated as: ((status == "active") && (role == "admin")) || (role == "user")
# => (true && false) || true = true

# Intended logic might have been different
if status == "active" && (role == "admin" || role == "user")
  puts "Valid user"
end

Assignment in conditional expressions often catches developers off-guard because assignment has very low precedence.

# Assignment precedence pitfall
x = 5
y = 10

# WRONG: Assignment happens last
result = x + y = 15
# Ruby interprets as: result = (x + (y = 15))
# Assigns 15 to y, then adds x + 15, then assigns to result
# result => 20, y => 15

# CORRECT: Use parentheses or separate statements
result = (x + y) == 15
y = x + y if condition

# Multiple assignment confusion
a = b = c = 1 + 2 * 3
# All variables get 7, not a=1, b=2, c=3 as might be expected
# Evaluated right-to-left: c = 7, then b = c, then a = b

Method calls that look like operators can create precedence confusion, especially with array indexing and attribute access.

class DataProcessor
  def initialize(data)
    @data = data
  end

  def [](index)
    @data[index]
  end

  def process
    @data.map(&:upcase)
  end
end

processor = DataProcessor.new(["hello", "world"])

# Method call precedence confusion
# WRONG assumption about precedence
result = processor.process[0].length + 1
# Evaluated as: ((processor.process)[0]).length + 1
# Not: processor.process([0].length + 1)

# Array indexing with arithmetic
array = [1, 2, 3, 4, 5]

# WRONG: Index calculation precedence
value = array[2 + 3 - 1]
# Evaluated as: array[(2 + 3) - 1] = array[4] = 5
# Index arithmetic happens before array access

# String interpolation precedence gotchas
name = "Ruby"
version = 3.1

# WRONG: Method calls in interpolation
puts "Running #{name.downcase} version #{version.to_s + " stable"}"
# String concatenation happens before interpolation ends
# Causes syntax error

# CORRECT: Complete expressions within interpolation
puts "Running #{name.downcase} version #{version.to_s + ' stable'}"

Range operators create particularly subtle precedence issues because they bind differently than arithmetic operators.

# Range precedence confusion
start = 1
finish = 10

# WRONG: Arithmetic doesn't bind tighter than range
numbers = start..finish - 1
# Evaluated as: start..(finish - 1) = 1..9
# Creates range from start to (finish-1)

# WRONG assumption about range binding
letters = 'a'..'z' - 'x'
# Syntax error: undefined method `-' for "a".."z":Range

# CORRECT: Use parentheses for arithmetic first
numbers = start..(finish - 1)
letters = ('a'..'z').to_a - ['x']

# Stepping through ranges with arithmetic
(0..9).step(2 + 1) { |n| puts n }
# Evaluated as: (0..9).step((2 + 1))
# Steps by 3, not by 2 then adds 1 to each result

Reference

Operator Precedence Table (Highest to Lowest)

Level Operators Associativity Description
1 [] Left Array/hash indexing, method calls
2 ** Right Exponentiation
3 + - (unary) Right Unary plus, minus
4 ! ~ Right Logical NOT, bitwise complement
5 * / % Left Multiplication, division, modulo
6 + - (binary) Left Addition, subtraction
7 << >> Left Bitwise shift left, right
8 & Left Bitwise AND
9 | ^ Left Bitwise OR, XOR
10 > >= < <= Left Comparison operators
11 <=> == === != =~ !~ Left Equality, pattern matching
12 && Left Logical AND
13 || Left Logical OR
14 .. ... Left Range operators (inclusive, exclusive)
15 ? : Right Ternary conditional
16 = += -= *= /= %= **= &= |= ^= <<= >>= ||= &&= Right Assignment operators
17 not Right Logical NOT (keyword form)
18 or and Left Logical OR, AND (keyword forms)
19 if unless while until Right Modifier conditionals and loops

Associativity Rules

Type Order Example Operators
Left ((a op b) op c) op d 10 - 5 - 2 = (10 - 5) - 2 = 3 Most arithmetic, comparison, logical
Right a op (b op (c op d)) 2 ** 3 ** 2 = 2 ** (3 ** 2) = 512 **, assignment, ternary, unary

Method Operators

Method Syntax Level Example
#[] obj[key] 1 array[0]
#[]= obj[key] = value 16 array[0] = 1
#+ a + b 6 "hello" + " world"
#- a - b 6 Time.now - 3600
#* a * b 5 "x" * 3
#/ a / b 5 Rational(1) / 3
#% a % b 5 "template %s" % "value"
#** a ** b 2 Complex(1, 1) ** 2
#<< a << b 7 array << item
#>> a >> b 7 8 >> 2
#& a & b 8 [1,2] & [2,3]
#| a | b 9 [1,2] | [2,3]
#^ a ^ b 9 5 ^ 3
#< a < b 10 obj1 < obj2
#<= a <= b 10 version <= required
#> a > b 10 score > threshold
#>= a >= b 10 age >= minimum
#<=> a <=> b 11 "a" <=> "b"
#== a == b 11 obj1 == obj2
#=== a === b 11 (1..10) === 5
#!= a != b 11 value != expected
#=~ a =~ b 11 string =~ /pattern/
#!~ a !~ b 11 string !~ /pattern/

Special Cases and Edge Behaviors

Pattern Behavior Example
Chained comparisons Each pair evaluated separately a < b < c means (a < b) < c
Assignment chains Right associative, shared references a = b = [] creates shared array
Splat in calls Binds to immediate argument method(*array + [item])
Block precedence Lower than all operators array.map { }.length
Keyword conditionals Lowest precedence return x if condition
Parentheses Override all precedence (a + b) * c

Common Precedence Patterns

# Pattern: Arithmetic in comparisons
result = value * 2 > threshold + margin
# Evaluates as: (value * 2) > (threshold + margin)

# Pattern: Logical operators with comparisons
valid = age >= 18 && status == "active" || role == "admin"
# Evaluates as: ((age >= 18) && (status == "active")) || (role == "admin")

# Pattern: Assignment with conditional
counter ||= items.count * 2 + base_value
# Evaluates as: counter ||= ((items.count * 2) + base_value)

# Pattern: Range with arithmetic
page_range = current_page - 2..current_page + 2
# Evaluates as: (current_page - 2)..(current_page + 2)

# Pattern: Method chaining with operators
result = data.map(&:to_i).select(&:positive?).sum > 0
# Method calls bind tighter than comparison