Polyglot
Languages Ruby conditionals
Ruby § conditionals

Conditionals

Ruby’s conditional surface is expression-orientedif, unless, case, and the ternary ?: all produce values. The principal forms are if/elsif/else, unless (the inverted form), the ternary, and case/when for value dispatch. Each of if, unless, while, and until admits a modifier (postfix) form for short single-statement conditionals. The case admits substantial polymorphism through the === operator (equality for ranges and regexes; type-check for classes; == otherwise). The combination — expression-orientation, the unless inversion, modifier forms, the polymorphic case/when — is the substance of Ruby’s conditional surface.

if/elsif/else

The principal form:

if condition
  body
elsif other
  body
else
  body
end

Examples:

if x > 0
  puts "positive"
elsif x < 0
  puts "negative"
else
  puts "zero"
end

The elsif is a single keyword (not else if). The then keyword is admitted (if x > 0 then ...) but typically omitted on multi-line conditionals; conventional on one-liners.

The braces of C-family languages are not admitted — the body is delimited by the end keyword.

Truthiness

Ruby’s truthiness is strict:

  • Falsy: false and nil.
  • Truthy: everything else (including 0, "", [], 0.0).
if 0 then puts "yes" end                          # prints "yes"
if "" then puts "yes" end                         # prints "yes"
if [] then puts "yes" end                         # prints "yes"
if nil then puts "yes" end                        # nothing
if false then puts "yes" end                      # nothing

The conventional discipline produces clear code; checking nil?, empty?, or specific values is conventionally explicit:

if items.empty? then puts "empty" end
if name.nil? then return end

unless

The unless is if not:

unless condition
  body
end

# Equivalent:
if !condition
  body
end

The conventional discipline:

  • Use unless for simple negative conditions.
  • Use if ! for compound conditions where unless becomes confusing.
  • Avoid unless ... else — the inversion is conventionally hard to read.
# Acceptable:
unless valid?
  return error
end

# Avoid:
unless valid? && complete?                        # confusing
  return error
end

# Better:
if !valid? || !complete?
  return error
end

Modifier forms

if and unless admit postfix forms:

puts "found" if found?
return unless valid?
log_warning if defined?(WARNINGS_ENABLED)

# Same as:
puts "found" if found?
return if !valid?

The conventional discipline uses modifier forms for short, single-statement conditionals; multi-statement bodies use the regular form.

if as expression

if produces a value:

max = if a > b then a else b end

# More commonly, the multi-line form:
status = if user.active?
           "active"
         elsif user.pending?
           "pending"
         else
           "unknown"
         end

# Or with implicit return:
def status_for(user)
  if user.active?
    "active"
  elsif user.pending?
    "pending"
  else
    "unknown"
  end
end

The expression-orientation is a substantial feature; the conventional Ruby discipline admits if in any expression position.

Ternary

The C-style ternary:

result = condition ? value_if_true : value_if_false

max = a > b ? a : b
status = user.active? ? "active" : "inactive"

The ternary is admitted but conventional only for short, single-expression conditionals. For substantial logic, if/else is conventionally clearer.

case/when

The case expression admits multi-way dispatch:

case n
when 0
  "zero"
when 1
  "one"
when 2, 3
  "two or three"                                  # multiple values
when 4..10
  "small"                                          # range
when Integer
  "an integer"                                     # class match
else
  "other"
end

The case/when uses the === operator (the case equality):

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

The mechanism admits substantial polymorphism:

case input
when Integer then "got an int: #{input}"
when /^\d+$/ then "got numeric string"
when 1..100 then "got a small number"
when ->(x) { x.respond_to?(:call) } then "got a callable"
end

Each when admits multiple comma-separated values; the first match wins. There is no fallthrough — each when body runs to completion and exits the case.

Conditionless case

case with no expression admits arbitrary boolean conditions:

case
when n < 0 then "negative"
when n == 0 then "zero"
when n < 100 then "small"
else "large"
end

The form is equivalent to if/elsif/else but conventionally clearer for substantial value-driven branching.

Expressional case

Like if, case produces a value:

greeting = case time.hour
           when 0..11 then "Good morning"
           when 12..17 then "Good afternoon"
           else "Good evening"
           end

then keyword

The then keyword is optional — admits separating the condition from the body on the same line:

if x > 0 then puts "positive" end                 # one-liner with then
if x > 0; puts "positive" end                     # one-liner with semicolon
if x > 0 then                                     # multi-line with then (rare)
  puts "positive"
end

The conventional discipline omits then on multi-line forms; admits it on single-line forms.

Pattern matching case/in (Ruby 3.0+)

Ruby 3.0 introduced pattern matching with the in keyword:

case point
in [0, 0]
  "origin"
in [x, 0]
  "on x-axis at #{x}"
in [0, y]
  "on y-axis at #{y}"
in [x, y]
  "at (#{x}, #{y})"
end

Treated separately in Pattern matching.

Boolean operators in conditions

The &&, ||, ! (or word forms and, or, not) for compound conditions:

if user && user.active? && user.has_role?(:admin)
  grant_admin
end

# With safe navigation:
if user&.active? && user&.admin?
  # ...
end

The conventional symbolic forms (&&, ||, !) are preferred for boolean expressions; the word forms have very low precedence and are conventional only for control flow:

do_thing or fail                                  # OK; "or" gates the failure
result = compute() or default                     # SURPRISING — assigns compute(), then 'or default' is discarded

Pattern: Guard clauses

The conventional Ruby style favours guard clauses for early returns:

def process(input)
  return error("empty") if input.nil? || input.empty?
  return error("too long") if input.length > 100
  return error("invalid") unless input.match?(/^[a-z]+$/)

  # main body
  do_work(input)
end

The pattern reduces nesting and makes preconditions explicit.

Pattern: Conditional assignment

# Set if not already set:
@cache ||= {}

# Set conditionally:
options[:timeout] = options[:timeout] || DEFAULT_TIMEOUT
options[:timeout] ||= DEFAULT_TIMEOUT             # equivalent

# Conditional update:
status = if user.active? then "online" else "offline" end

Pattern: Memoization

The ||= is the canonical Ruby memoization idiom:

def expensive_data
  @expensive_data ||= compute_expensive
end

The form admits “compute once on first access; cache for subsequent calls”.

Pattern: Multi-condition value dispatch

def category(age)
  case age
  when 0..12 then "child"
  when 13..19 then "teen"
  when 20..64 then "adult"
  else "senior"
  end
end

For substantially elaborate dispatch, a hash lookup or pattern matching is conventionally clearer.

Pattern: Type dispatch

def describe(value)
  case value
  when Integer then "integer #{value}"
  when Float then "float #{value}"
  when String then "string #{value.inspect}"
  when Array then "array of #{value.length}"
  when Hash then "hash of #{value.size} keys"
  when nil then "nothing"
  else "unknown"
  end
end

The mechanism admits substantial discrimination on dynamic types.

Pattern: Boolean validation chain

def valid_user?(user)
  return false unless user
  return false if user.email.nil? || user.email.empty?
  return false unless user.email.match?(/\A[^@]+@[^@]+\z/)
  return false if user.age && user.age < 0
  true
end

Pattern: Conditional method definition

class Service
  if Rails.env.development?
    def debug_info
      # ...
    end
  end
end

Admitted but rare; the conventional discipline keeps method definitions consistent.

Pattern: case with then for compactness

def http_status(code)
  case code
  when 200 then "OK"
  when 201 then "Created"
  when 301..302 then "Redirect"
  when 400 then "Bad Request"
  when 401 then "Unauthorized"
  when 404 then "Not Found"
  when 500..599 then "Server Error"
  else "Unknown"
  end
end

The when ... then ... form is conventional for compact value dispatch.

Pattern: Exception-based conditional

def load_or_default(path)
  File.read(path)
rescue Errno::ENOENT
  default_content
end

Method-level rescue admits substantial conditional control through exceptions; treated in Error handling.

A note on the absence of switch

Ruby’s case/when is the conventional substitute for C-family switch. The principal differences:

  • No fallthrough — each when body runs once.
  • Polymorphic matching via ===.
  • Expression-form — produces a value.
  • No break — implicit at the end of each when.

The mechanism is more expressive than C-family switch while admitting similar code structure.

A note on the conventional discipline

The contemporary Ruby conditional advice:

  • Use if/elsif/else for the conventional branching.
  • Use unless for simple negative conditions.
  • Use modifier forms for short single-statement conditionals.
  • Use the ternary sparingly — only for short value-returning expressions.
  • Use case/when for value dispatch on type, range, regex, or pattern.
  • Use guard clauses for precondition checks.
  • Use ||= for memoization.
  • Use && and ||; avoid and and or (low precedence).
  • Use case/in (Ruby 3+) for pattern matching.
  • Trust expression orientation — conditionals can be assigned and returned.
  • Avoid unless with else — the inversion is conventionally hard to read.

The combination — expression-oriented if/unless/case, modifier forms, the polymorphic === in case/when, the strict truthiness rule, the conventional guard-clause pattern — is the substance of Ruby’s conditional surface. The discipline produces concise, expressive conditional code with substantial flexibility.