Ruby LanguageControl Flow

if, elsif, else and end

Ruby offers the expected if and else expressions for branching logic, terminated by the end keyword:

# Simulate flipping a coin
result = [:heads, :tails].sample

if result == :heads
  puts 'The coin-toss came up "heads"'
else
  puts 'The coin-toss came up "tails"'
end

In Ruby, if statements are expressions that evaluate to a value, and the result can be assigned to a variable:

status = if age < 18
           :minor
         else
           :adult
         end

Ruby also offers C-style ternary operators (see here for details) that can be expressed as:

some_statement ? if_true : if_false  

This means the above example using if-else can also be written as

status = age < 18 ? :minor : :adult

Additionally, Ruby offers the elsif keyword which accepts an expression to enables additional branching logic:

label = if shirt_size == :s
          'small'
        elsif shirt_size == :m
          'medium'
        elsif shirt_size == :l
          'large'
        else
          'unknown size'
        end

If none of the conditions in an if/elsif chain are true, and there is no else clause, then the expression evaluates to nil. This can be useful inside string interpolation, since nil.to_s is the empty string:

"user#{'s' if @users.size != 1}"

Truthy and Falsy values

In Ruby, there are exactly two values which are considered "falsy", and will return false when tested as a condition for an if expression. They are:

  • nil
  • boolean false

All other values are considered "truthy", including:

  • 0 - numeric zero (Integer or otherwise)
  • "" - Empty strings
  • "\n" - Strings containing only whitespace
  • [] - Empty arrays
  • {} - Empty hashes

Take, for example, the following code:

def check_truthy(var_name, var)
  is_truthy = var ? "truthy" : "falsy"
  puts "#{var_name} is #{is_truthy}"
end

check_truthy("false", false)
check_truthy("nil", nil)
check_truthy("0", 0)
check_truthy("empty string", "")
check_truthy("\\n", "\n")
check_truthy("empty array", [])
check_truthy("empty hash", {})

Will output:

false is falsy
nil is falsy
0 is truthy
empty string is truthy
\n is truthy
empty array is truthy
empty hash is truthy

while, until

A while loop executes the block while the given condition is met:

i = 0
while i < 5
  puts "Iteration ##{i}"
  i +=1
end

An until loop executes the block while the conditional is false:

i = 0
until i == 5
  puts "Iteration ##{i}"
  i +=1
end

Inline if/unless

A common pattern is to use an inline, or trailing, if or unless:

puts "x is less than 5" if x < 5

This is known as a conditional modifier, and is a handy way of adding simple guard code and early returns:

def save_to_file(data, filename)
  raise "no filename given" if filename.empty?
  return false unless data.valid?

  File.write(filename, data)
end

It is not possible to add an else clause to these modifiers. Also it is generally not recommended to use conditional modifiers inside the main logic -- For complex code one should use normal if, elsif, else instead.

unless

A common statement is if !(some condition). Ruby offers the alternative of the unless statement.

The structure is exactly the same as an if statement, except the condition is negative. Also, the unless statement does not support elsif, but it does support else:

# Prints not inclusive
unless 'hellow'.include?('all')
  puts 'not inclusive'
end

Case statement

Ruby uses the case keyword for switch statements.

As per the Ruby Docs:

Case statements consist of an optional condition, which is in the position of an argument to case, and zero or more when clauses. The first when clause to match the condition (or to evaluate to Boolean truth, if the condition is null) “wins”, and its code stanza is executed. The value of the case statement is the value of the successful when clause, or nil if there is no such clause.

A case statement can end with an else clause. Each when a statement can have multiple candidate values, separated by commas.

Example:

case x
when 1,2,3
  puts "1, 2, or 3"
when 10
  puts "10"
else
  puts "Some other number"
end

Shorter version:

case x
when 1,2,3 then puts "1, 2, or 3"
when 10 then puts "10"
else puts "Some other number"
end

The value of the case clause is matched with each when clause using the === method (not ==). Therefore it can be used with a variety of different types of objects.

A case statement can be used with Ranges:

case 17
when 13..19
  puts "teenager"
end

A case statement can be used with a Regexp:

case "google"
when /oo/
  puts "word contains oo"
end

A case statement can be used with a Proc or lambda:

case 44
when -> (n) { n.even? or n < 0 }
  puts "even or less than zero"
end

A case statement can be used with Classes:

case x
when Integer
  puts "It's an integer"
when String
  puts "It's a string"
end

By implementing the === method you can create your own match classes:

class Empty
  def self.===(object)
    !object or "" == object
  end
end

case ""
when Empty
  puts "name was empty"
else
  puts "name is not empty"
end

A case statement can be used without a value to match against:

case
when ENV['A'] == 'Y'
  puts 'A'
when ENV['B'] == 'Y'
  puts 'B'
else
  puts 'Neither A nor B'
end

A case statement has a value, so you can use it as a method argument or in an assignment:

description = case 16
              when 13..19 then "teenager"
              else ""
              end

Loop control with break, next, and redo

The flow of execution of a Ruby block may be controlled with the break, next, and redo statements.

break

The break statement will exit the block immediately. Any remaining instructions in the block will be skipped, and the iteration will end:

actions = %w(run jump swim exit macarena)
index = 0

while index < actions.length
  action = actions[index]

  break if action == "exit"

  index += 1
  puts "Currently doing this action: #{action}"
end

# Currently doing this action: run
# Currently doing this action: jump
# Currently doing this action: swim

next

The next statement will return to the top of the block immediately, and proceed with the next iteration. Any remaining instructions in the block will be skipped:

actions = %w(run jump swim rest macarena)
index = 0

while index < actions.length
  action = actions[index]
  index += 1

  next if action == "rest"

  puts "Currently doing this action: #{action}"
end

# Currently doing this action: run
# Currently doing this action: jump
# Currently doing this action: swim
# Currently doing this action: macarena

redo

The redo statement will return to the top of the block immediately, and retry the same iteration. Any remaining instructions in the block will be skipped:

actions = %w(run jump swim sleep macarena)
index = 0
repeat_count = 0

while index < actions.length
  action = actions[index]
  puts "Currently doing this action: #{action}"

  if action == "sleep"
    repeat_count += 1
    redo if repeat_count < 3
  end

  index += 1
end

# Currently doing this action: run
# Currently doing this action: jump
# Currently doing this action: swim
# Currently doing this action: sleep
# Currently doing this action: sleep
# Currently doing this action: sleep
# Currently doing this action: macarena

Enumerable iteration

In addition to loops, these statements work with Enumerable iteration methods, such as each and map:

[1, 2, 3].each do |item|
  next if item.even?
  puts "Item: #{item}"
end

# Item: 1
# Item: 3

Block result values

In both the break and next statements, a value may be provided, and will be used as a block result value:

even_value = for value in [1, 2, 3]
  break value if value.even?
end

puts "The first even value is: #{even_value}"

# The first even value is: 2

throw, catch

Unlike many other programming languages, the throw and catch keywords are not related to exception handling in Ruby.

In Ruby, throw and catch act a bit like labels in other languages. They are used to change the control flow, but are not related to a concept of "error" like Exceptions are.

catch(:out) do
  catch(:nested) do
    puts "nested"
  end

  puts "before"
  throw :out
  puts "will not be executed"
end
puts "after"
# prints "nested", "before", "after"

Control flow with logic statements

While it might seem counterintuitive, you can use logical operators to determine whether or not a statement is run. For instance:

File.exist?(filename) or STDERR.puts "#{filename} does not exist!"

This will check to see if the file exists and only print the error message if it doesn't. The or statement is lazy, which means it'll stop executing once it's sure which whether it's value is true or false. As soon as the first term is found to be true, there's no need to check the value of the other term. But if the first term is false, it must check the second term.

A common use is to set a default value:

glass = glass or 'full' # Optimist! 

That sets the value of glass to 'full' if it's not already set. More concisely, you can use the symbolic version of or:

glass ||= 'empty' # Pessimist. 

It's also possible to run the second statement only if the first one is false:

File.exist?(filename) and puts "#{filename} found!"

Again, and is lazy so it will only execute the second statement if necessary to arrive at a value.

The or operator has lower precedence than and. Similarly, || has lower precedence than &&. The symbol forms have higher precedence than the word forms. This is handy to know when you want to mix this technique with assignment:

a = 1 and b = 2
#=> a==1
#=> b==2
a = 1 && b = 2; puts a, b
#=> a==2
#=> b==2

Note that the Ruby Style Guide recommends:

The and and or keywords are banned. The minimal added readability is just not worth the high probability of introducing subtle bugs. For boolean expressions, always use && and || instead. For flow control, use if and unless; && and || are also acceptable but less clear.

begin, end

The begin block is a control structure that groups together multiple statements.

begin
  a = 7
  b = 6
  a * b
end

A begin block will return the value of the last statement in the block. The following example will return 3.

begin
  1
  2
  3
end

The begin block is useful for conditional assignment using the ||= operator where multiple statements may be required to return a result.

circumference ||=
  begin
    radius = 7
    tau = Math::PI * 2
    tau * radius
  end

It can also be combined with other block structures such as rescue, ensure, while, if, unless, etc to provide greater control of program flow.

Begin blocks are not code blocks, like { ... } or do ... end; they cannot be passed to functions.

return vs. next: non-local return in a block

Consider this broken snippet:

def foo
  bar = [1, 2, 3, 4].map do |x|
    return 0 if x.even?
    x
  end
  puts 'baz'
  bar
end
foo # => 0

One might expect return to yield a value for map's array of block results. So the return value of foo would be [1, 0, 3, 0]. Instead, return returns a value from the method foo. Notice that baz isn't printed, which means execution never reached that line.

next with a value does the trick. It acts as a block-level return.

def foo
  bar = [1, 2, 3, 4].map do |x|
    next 0 if x.even?
    x
  end
  puts 'baz'
  bar
end
foo # baz
    # => [1, 0, 3, 0]

In the absence of a return, the value returned by the block is the value of its last expression.

Or-Equals/Conditional assignment operator (||=)

Ruby has an or-equals operator that allows a value to be assigned to a variable if and only if that variable evaluates to either nil or false.

 ||= # this is the operator that achieves this. 

this operator with the double pipes representing or and the equals sign representing assigning of a value. You may think it represents something like this:

 x = x || y

this above example is not correct. The or-equals operator actually represents this:

 x || x = y

If x evaluates to nil or false then x is assigned the value of y, and left unchanged otherwise.

Here is a practical use-case of the or-equals operator. Imagine you have a portion of your code that is expected to send an email to a user. What do you do if for what ever reason there is no email for this user. You might write something like this:

 if user_email.nil?
    user_email = "error@yourapp.com"
 end

Using the or-equals operator we can cut this entire chunk of code, providing clean, clear control and functionality.

 user_email ||= "error@yourapp.com"

In cases where false is a valid value, care must be taken to not override it accidentally:

has_been_run = false
has_been_run ||= true
#=> true

has_been_run = false
has_been_run = true if has_been_run.nil?
#=> false

Ternary operator

Ruby has a ternary operator (?:), which returns one of two value based on if a condition evaluates as truthy:

conditional ? value_if_truthy : value_if_falsy

value = true
value ? "true" : "false"
#=> "true"

value = false
value ? "true" : "false"
#=> "false"

it is the same as writing if a then b else c end, though the ternary is preferred

Examples:

puts (if 1 then 2 else 3 end) # => 2

puts 1 ? 2 : 3                # => 2

x = if 1 then 2 else 3 end
puts x                        # => 2

Flip-Flop operator

The flip flop operator .. is used between two conditions in a conditional statement:

(1..5).select do |e|
  e if (e == 2) .. (e == 4)
end
# => [2, 3, 4]

The condition evaluates to false until the first part becomes true. Then it evaluates to true until the second part becomes true. After that it switches to false again.

This example illustrates what is being selected:

[1, 2, 2, 3, 4, 4, 5].select do |e|
  e if (e == 2) .. (e == 4)
end
# => [2, 2, 3, 4]

The flip-flop operator only works inside ifs (including unless) and ternary operator. Otherwise it is being considered as the range operator.

(1..5).select do |e|
  (e == 2) .. (e == 4)
end
# => ArgumentError: bad value for range

It can switch from false to true and backwards multiple times:

((1..5).to_a * 2).select do |e|
  e if (e == 2) .. (e == 4)
end
# => [2, 3, 4, 2, 3, 4]