Conditionals
Ruby’s conditional surface is expression-oriented — if, 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:
falseandnil. - 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
unlessfor simple negative conditions. - Use
if !for compound conditions whereunlessbecomes 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 === obj—obj.is_a?(Class).Range === val—valis in the range.Regexp === str— regex match.Proc === val—proc.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
whenbody runs once. - Polymorphic matching via
===. - Expression-form — produces a value.
- No
break— implicit at the end of eachwhen.
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/elsefor the conventional branching. - Use
unlessfor simple negative conditions. - Use modifier forms for short single-statement conditionals.
- Use the ternary sparingly — only for short value-returning expressions.
- Use
case/whenfor value dispatch on type, range, regex, or pattern. - Use guard clauses for precondition checks.
- Use
||=for memoization. - Use
&&and||; avoidandandor(low precedence). - Use
case/in(Ruby 3+) for pattern matching. - Trust expression orientation — conditionals can be assigned and returned.
- Avoid
unlesswithelse— 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.