Polyglot
Languages Ruby operators
Ruby § operators

Operators

Most Ruby operators are methods1 + 2 is sugar for 1.+(2). The principal exceptions (operators that are not methods): &&, ||, !, and, or, not, =, .., ..., ?:, defined?. The mechanism admits operator overloading — any class may define +, -, *, <=>, ==, etc. The combination — operators-as-methods, the spaceship <=> for ordering, the safe navigation &., the splat (*) and double splat (**) for unpacking — is the substance of Ruby’s expression surface.

Arithmetic

a + b                                             # addition
a - b                                             # subtraction
a * b                                             # multiplication
a / b                                             # division (integer for Integer/Integer)
a % b                                             # modulo
a ** b                                            # exponentiation
-a                                                # unary negation
+a                                                # unary plus

Integer division truncates toward negative infinity:

puts 7 / 2                                        # 3
puts -7 / 2                                       # -4 (not -3)
puts 7 % 2                                        # 1
puts -7 % 2                                       # 1 (not -1)

The behaviour differs from C-family languages where division truncates toward zero. The mathematical identity (a / b) * b + (a % b) == a always holds.

For exact rationals:

Rational(1, 3) + Rational(2, 3)                  # (1/1)

The arithmetic operators are methods; 1 + 2 is 1.+(2). Class authors may overload them:

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
end

a = Vector.new(1, 2)
b = Vector.new(3, 4)
(a + b).inspect                                   # #<Vector ...>

Comparison

a == b                                            # equality (compares values)
a != b
a < b
a > b
a <= b
a >= b
a <=> b                                           # spaceship: -1, 0, 1, or nil

The <=> returns -1 if less, 0 if equal, 1 if greater, nil if incomparable:

1 <=> 2                                           # -1
"a" <=> "b"                                       # -1
1 <=> "a"                                         # nil

The <=> is the conventional foundation for Comparable; defining it admits <, <=, >, >=, between?, etc. Treated in Modules and mixins.

Three equality methods

Ruby has three equality-related methods:

a == b                                            # value equality (the conventional default)
a.eql?(b)                                         # strict equality (no type coercion)
a.equal?(b)                                       # identity (same object)

The principal differences:

1 == 1.0                                          # true (value equality)
1.eql?(1.0)                                       # false (different types)
1.equal?(1.0)                                     # false

a = "hello"
b = "hello"
a == b                                            # true (same value)
a.equal?(b)                                       # false (different objects)
a.equal?(a)                                       # true (same object)

The conventional discipline:

  • == — value equality; the default.
  • eql? — used by Hash for key comparison.
  • equal? — never overridden; admits identity check.

=== (case equality)

The === operator is special — used by case/when:

case x
when Integer then "integer"
when /^[0-9]+$/ then "digits"
when 1..10 then "small"
when ->(n) { n > 100 } then "large"
end

The semantics depend on the receiver type:

  • Class === objobj.is_a?(Class).
  • Regex === str — regex match.
  • Range === valval is in range.
  • Proc === valproc.call(val) (since 1.9).
  • Otherwise: same as ==.

The mechanism admits substantial polymorphism in case expressions; treated in Conditionals.

Logical operators

Ruby has two sets of logical operators:

# Symbolic (high precedence):
a && b                                            # short-circuit AND
a || b                                            # short-circuit OR
!a                                                # NOT

# Word-form (low precedence):
a and b
a or b
not a

The two sets differ in precedence:

result = a || b                                   # parses as: result = (a || b)
result = a or b                                   # parses as: (result = a) or b

The conventional discipline:

  • &&, ||, ! for boolean expressions.
  • and, or, not for control flow (e.g., do_thing or fail).

Both forms return the operand value, not just true/false:

nil || "default"                                  # "default"
0 || "default"                                    # 0 (0 is truthy in Ruby)
false || "default"                                # "default"

"first" && "second"                               # "second" (returns last truthy)
nil && "ignored"                                  # nil

For “default value when nil”:

config.port || 8080                               # 8080 if port is nil OR false
config.port.nil? ? 8080 : config.port            # 8080 only if nil

Assignment operators

a = 5                                             # simple assignment
a += 1                                            # a = a + 1
a -= 1
a *= 2
a /= 2
a %= 3
a **= 2

a &= true
a |= false
a ^= b
a <<= 1
a >>= 1

a &&= b                                           # a = a && b
a ||= default                                     # a = a || default — set if nil/false

The ||= is the conventional Ruby memoization idiom:

def expensive_data
  @data ||= compute_data
end

The form admits “compute once, cache the result”.

The mass assignment:

a, b = 1, 2
a, b, c = [10, 20, 30]
a, *rest = [1, 2, 3, 4]                          # a=1, rest=[2,3,4]
a, b = b, a                                       # swap

Bitwise operators

a & b                                             # AND
a | b                                             # OR
a ^ b                                             # XOR
~a                                                # NOT
a << b                                            # left shift
a >> b                                            # right shift

The bitwise operators work on integers; for Array, &, |, ^ are set operations:

[1, 2, 3] & [2, 3, 4]                             # [2, 3] (intersection)
[1, 2, 3] | [3, 4, 5]                             # [1, 2, 3, 4, 5] (union)
[1, 2, 3] - [2]                                   # [1, 3] (difference)

For String, * repeats; + concatenates; << appends:

"ab" * 3                                          # "ababab"
"a" + "b"                                         # "ab"
s = "a"; s << "b"                                 # mutates: "ab"

For Hash, merge is the conventional combiner:

{ a: 1 }.merge({ b: 2 })                          # { a: 1, b: 2 }

Range operators

1..10                                             # inclusive
1...10                                            # exclusive (no 10)

Ranges are first-class objects; treated in Types.

Splat operators

The * (splat) and ** (double splat) admit unpacking and collecting:

# Splat in argument list (collect):
def sum(*args)
  args.sum
end

sum(1, 2, 3)                                      # 6

# Splat in call (spread):
nums = [1, 2, 3]
sum(*nums)                                        # 6

# Double splat (keyword):
def configure(**opts)
  opts.each { |k, v| puts "#{k}=#{v}" }
end

configure(host: "localhost", port: 8080)
opts = { host: "localhost", port: 8080 }
configure(**opts)

Treated in Methods.

Safe navigation &.

Since Ruby 2.3, the &. admits “call this method if the receiver is not nil”:

user&.name                                        # nil if user is nil
user&.address&.city                               # nil-safe chain

result = list.first&.upcase                       # nil if list is empty

The conventional alternative pre-2.3 was try:

user.try(:name)                                   # ActiveSupport (Rails)

The &. is the contemporary form.

Conditional and ternary

result = condition ? value_if_true : value_if_false

# Modifier forms:
puts "hello" if condition
puts "hello" unless condition

The ternary is admitted but conventional only for short value-returning conditionals; for substantial logic, if/else is conventional.

Range === and case

case n
when 1..10 then "small"
when 11..100 then "medium"
when 101.. then "large"                           # endless range
end

The mechanism admits range-based dispatch; treated in Pattern matching.

Defined?

The defined? operator returns a string describing the kind of expression — or nil if the expression is not defined:

defined?(x)                                       # nil if x undeclared, "local-variable" otherwise
defined?(@x)                                      # "instance-variable" or nil
defined?(puts)                                    # "method"
defined?(String)                                  # "constant"
defined?(self)                                    # "self"

The form is rare in idiomatic code; nil? and respond_to? are conventional alternatives.

Operator precedence

Ruby’s operator precedence (high to low):

Operators
!, ~, +@, -@
**
*, /, %
+, -
<<, >>
&
|, ^
<, <=, >, >=
==, !=, ===, =~, !~, <=>
&&
||
.., ...
?:
=, +=, etc.
not
and, or

The principal pitfall is not, and, or having very low precedence — even lower than =. The conventional defence:

result = a or b                                   # PROBABLY NOT what you want
                                                  # parses as: (result = a) or b
result = a || b                                   # what you want

Common patterns

||= for memoization

def cache_key
  @cache_key ||= compute_cache_key
end

Spaceship for sort

people.sort { |a, b| a.age <=> b.age }
people.sort_by(&:age)                             # cleaner

Splat for forwarding

def wrapper(*args, **opts, &block)
  log_call(*args, **opts)
  underlying_method(*args, **opts, &block)
end

Set operations on arrays

required = [:name, :email, :password]
provided = params.keys
missing = required - provided
extra = provided - required
intersection = required & provided

Safe navigation chain

city = user&.profile&.address&.city || "unknown"

Range iteration

(1..100).step(5) { |n| puts n }
(1..10).each_slice(3) { |slice| puts slice.inspect }

Multiple assignment

def divmod(a, b)
  [a / b, a % b]
end

quotient, remainder = divmod(17, 5)

Symbol-to-proc

[1, 2, 3].map(&:to_s)                             # ["1", "2", "3"]
people.sort_by(&:age)
strings.select(&:empty?)

The &:method_name admits substantial conciseness for “call this method on each item”.

A note on the conventional discipline

The contemporary Ruby operator advice:

  • Use == for value equality; rarely override.
  • Use <=> with Comparable for ordering.
  • Use ||= for memoization.
  • Use && and || (symbolic forms) over and/or.
  • Use &. for nil-safe access.
  • Use * and ** for splat unpacking.
  • Use Comparable mixin to derive comparisons from <=>.
  • Use parentheses freely — Ruby’s precedence has subtle pitfalls.

The combination — operators-as-methods, three equality methods (==, eql?, equal?), the spaceship <=>, the splat operators, the safe navigation &., the symbolic-vs-word logical operators — is the substance of Ruby’s expression surface. The discipline produces concise, expressive code with substantial flexibility.