The letter A styled as Alchemists logo. lchemists

Putin's War on Ukraine - Watch President Zelenskyy's speech and help Ukraine fight against the senseless cruelty of a dictator!

Published May 1, 2023 Updated May 22, 2023
Cover
Ruby Keywords

The Ruby languages consists of only 40 keywords which provides a lot of power with minimal syntax. This article will dive into these core language keywords, provide a reference for quick lookup, and examples for use within your own code.

ENCODING

The ENCODING keyword answers the encoding of the current file. By default, this is generally UTF-8. Example:

__ENCODING__  # UTF-8

This can be changed with pragmas — also known as magic comments — as follows:

# encoding: ISO-8859-1

__ENCODING__  # ISO-8859-1

💡 You can use the Pragmater gem to learn about and manage pragmas.

LINE

The LINE keyword answers the current line of the file in which it is defined. For example, if defined on the third line of a file, you’d get a value of 3:

# One
# Two
__LINE__  # 3

There are multiple use cases for LINE and is commonly used in scripting and metaprogramming. Here’s an example where LINE information is used when dynamically defining a module method so that this information can be used for debugging purposes:

Demo = Module.new do
  module_eval <<~DEFINITION, __FILE__, __LINE__ + 1
    def self.say(text = "A demo.") = puts text
  DEFINITION
end

puts Demo.method(:say).source_location  # "demo.rb", 3

Knowing the .say method was defined on Line 3 is valuable when wanting to find the source code of the dynamic method. Otherwise, without this information, you’d get back a line number of 1 which isn’t accurate:

Demo = Module.new do
  module_eval <<~DEFINITION, __FILE__
    def self.say(text = "A demo.") = puts text
  DEFINITION
end

puts Demo.method(:say).source_location  # "demo.rb", 1

FILE

The FILE keyword answers the current file in which it is defined. Building upon the LINE example from above, this is how we know the Demo.say method was defined in the demo.rb file:

Demo = Module.new do
  module_eval <<~DEFINITION, __FILE__, __LINE__ + 1
    def self.say(text = "A demo.") = puts text
  DEFINITION
end

puts Demo.method(:say).source_location  # "demo.rb", 3

Otherwise, removing this information entirely means we don’t know where this method is defined:

Demo = Module.new do
  module_eval <<~DEFINITION
    def self.say(text = "A demo.") = puts text
  DEFINITION
end

puts Demo.method(:say).source_location  # "(eval)", 1

BEGIN

The BEGIN keyword defines a block — which can only be used with brackets ({}) — for code that is meant to be run before any other code within the file. This is most useful for one-line scripts:

ruby -e 'BEGIN { text = "Demo." }; puts text'
# "Demo."

Here’s an example when used in a script which isn’t as useful as the one-liner shown above:

BEGIN { puts "Start" }
# "Start"

END

The END keyword defines a block — which can only be used with brackets ({}) — for code that is meant to be run after any other code within the file. Building upon the BEGIN examples from above, this is meant for one-line scripts:

ruby -e 'text = "A demo."; END { puts text }'
# "A demo."

Here’s an example when used in a script which isn’t as useful as the one-liner shown above:

END { puts "Finish" }
# "Finish"

alias

The alias keyword allows you to define a new method which is an alias to an existing method:

class Demo
  def say = puts "An example."

  alias speak say
end

demo = Demo.new

demo.say    # "An example."
demo.speak  # "An example."

This keyword is great for renaming methods or wanting to provide a reference to the original method so you can change behavior. You can also use Module#alias_method but alias is preferred since the syntax is more concise.

and

The and keyword is a short-circuit boolean with lower precedence than && and is best used for control flow instead of yielding a boolean. In most cases, you want to use && but and improves readability in situations where you want combine an operation with another operation and don’t care if the combination of both operations answers a boolean or not. Example:

class Collector
  def initialize
    @collection = Set.new
  end

  def add(item) = collection.add(item) and self

  def to_a = collection.to_a

  private

  attr_reader :collection
end

Collector.new.add("one").add("two").to_a
# ["one", "two"]

With the above Collector#add method, the and keyword allows you to chain multiple #add messages together.

begin

The begin keyword defines the start of an exception block. Example:

begin
  require "some/file"
rescue LoadError => error
  puts error.message
end

# "cannot load such file -- some/file"

The above is great for inline scripting purposes but can be avoided when used in a method:

module Requirer
  def self.call path
    require path
  rescue LoadError => error
    puts error.message
  end
end

Requirer.call "some/file"
# "cannot load such file -- some/file"

Both examples are equivalent but when used in a method and/or block, use of the begin keyword is unnecessary.

break

The break keyword is useful when needing to exit from a block early. Example:

function = lambda do
  break
  "Unreachable"
end

puts function.call  # nil

With the above, "Unreachable" is never reached due to hitting break. Lesser known is that break can accept an argument. Example:

function = lambda do
  break "Exited early."
  "Unreachable"
end

puts function.call  # "Exited early."

While "Unreachable" is still never reached, you can see that the "Exited early." string was answered instead. This is powerful when needing to exit early while providing a custom value at the same time.

Keep in mind that break will return from an entire block (even when nested). Example:

("a".."e").each do |letter|
  5.times do |number|
    puts "#{letter}-#{number}"
    break
  end
end

# a-0
# b-0
# c-0
# d-0
# e-0

Notice only the first iteration of both blocks is executed because, once break is hit, the entire inner and outer blocks are exited.

case

The case keyword is used for control flow and — more recently — pattern matching which makes it a highly versatile keyword. The following is an example of control flow:

def demo object
  case object
    when 1..10 then puts "Is within range."
    when /blue/ then puts "Matches color."
    when String then puts "Is a string."
    when :demo then puts "Matches symbol."
    else puts "Unknown: #{object}."
  end
end

demo 5                   # "Is within range."
demo "The sky is blue."  # "Matches color."
demo "Hello."            # "Is a string."
demo :demo               # "Matches symbol."
demo Object.new          # "Unknown: #<Object:0x0000000112f68908>."

The following demonstrates pattern matching:

def demo(**attributes)
  case attributes
    in name:, **nil then puts "Name: #{name}."
    in name:, label: String => label then puts "Name: #{name}, Label: #{label}."
    in {} then puts "No attributes found."
    else puts "Unable to match: #{attributes}."
  end
end

demo name: "demo"                 # "Name: demo."
demo name: "demo", label: "Demo"  # "Name: demo, Label: Demo."
demo                              # "No attributes found."
demo x: 1, y: 2                   # "Unable to match: {:x=>1, :y=>2}."

In each of the above examples, there is a lot more to unpack and explore further so recommend you check out these articles for a deeper dive: function composition and pattern matching.

class

The class keyword is one of those most common keywords which allows you to create a class with optional support for subclassing:

class Demo
end

# Inherits from Object.
Demo.ancestors  # [Demo, Object, ...]

class Sub < BasicObject
end

# Inherits from BasicObject.
Sub.ancestors  # Sub, BasicObject]

def

The def keyword allows you to define a method and comes in two forms:

# Endless.
def say = "Hello"

# Standard
def say
  "Hello"
end

Endless methods are great for short one-liners while standard methods can use a few more lines of code to implement behavior. Be aware of these antipatterns when using both.

defined?

The defined? keyword is a useful — but odd keyword — in that it breaks convention by ending in a question mark and doesn’t return either true or false. Although, it can distinguish the use of nil, a variable that has never been set, expressions, methods, and constants. Example:

demo = "A demo."

defined? demo    # "local-variable"
defined? bogus   # nil
defined? @demo   # nil
defined? :puts   # "expression"
defined? puts    # "method"
defined? Object  # "constant"

Despite what is shown above, there are more robust alternatives:

  • Use local_variables.include? :demo for local variable checks or — even better — a nil check in the form of demo && demo == "<some_value>".

  • Use instance_variable_defined("@demo") for instance variable checks or — even better — a nil check in the form of @demo && @demo == "<some_value>".

  • Use object.respond_to? :example for method checks.

  • Use Object.const_defined? :Example for constant checks.

do

The do keyword indicates the start of a block — one of the most powerful features in the language — and is best used for multi-line expressions. Example:

demo = lambda do |text|
  puts "Got: #{text}."
  puts "End of block."
end

demo.call "A demo."

# "Got: A demo.."
# "End of block."

In situations where blocks only require a single line, you are better off using brackets (i.e. {}):

demo = -> text { puts "Got: #{text}." }
demo.call "A demo."

# "Got: A demo."

else

The else keyword provides an alternate branch of control flow logic when used in a if or case expression (can be used with unless but is not recommended because it forces the reader to invert the logic). Example:

if true == false
  puts "True."
else
  puts "False."
end

# "False."

elsif

The elsif keyword provides multiple alternate branches of control flow logic when used in an if expression (can be used with unless but is not recommended). Example:

if "blue" == "red"
  puts "True."
elsif "blue".match?(/blue/)
  puts "A match."
else
  puts "Unknown"
end

# "A match."

Use of elsif also allows you to add multiple branches of control flow logic much like a case…​when or case…​in expression and is best used when each logic branch requires non-uniform logic versus the more uniform logic you get with case expressions.

end

The end keyword denotes the end of a block, class, module, method, exception handling, or control expression. Examples:

proc do
  "A demo."
end

class Demo
end

def demo
end

ensure

The ensure keyword is used in exception handling to ensure behavior always executes after an exception has been raised. Example:

begin
  demo.to_s
rescue NameError => error
  puts "ERROR: #{error.message}"
ensure
  puts "End of line."
end

# ERROR: undefined local variable or method `demo' for main:Object
# End of line.

Of course, when adding ensure behavior, the logic should be safe in order to not trigger another exception.

false

The false keyword is the companion to the true keyword and both represent boolean logic even though a boolean type doesn’t exist in Ruby. In fact, the false keyword is an object:

false.class  # FalseClass

for

The for keyword is meant for control expressions but use is discouraged since the Enumerator#each method is infinitely more powerful and expressive. Example:

for number in 1..10 do
  puts "#{number}"
end

The better way to write the above is:

(1..10).each { |number| puts "#{number}" }

if

The if keyword is a control flow keyword that is the opposite of the unless keyword and is best used as a multi-line expression or as a guard clause. Example:

# Multi-line.
if "The sky is blue".match?(/blue/)
  puts "Detected blue color."
else
 puts "A blue color wasn't detected."
end

# "Detected blue color."
# Guard clause.
puts "Detected blue color." if "The sky is blue".match?(/blue/)
# "Detected blue color."

in

The in keyword has, historically, been used in conjunction with for loops but, recently, gained use in pattern matching. Here’s an example of the in keyword when used with for and case:

# Loops: Not recommended.
for number in 1..10 do
  puts "#{number}"
end
# Pattern Matching: Recommended.
case attributes
  in name:, label: String => label then puts "Name: #{name}, Label: #{label}."
  in {} then puts "No attributes found."
  else puts "Unable to match: #{attributes}."
end

module

The module keyword is used for defining namespaces or mixins (i.e. multiple inheritance). Example:

module Demo
end

💡 Ensure you are aware of antipatterns when using modules, nested modules, and use of proper namespaces in general.

In terms of mixins, modules are quite flexible in that they can be used in various ways:

# Adds class methods.
extend Demo

# Adds instance methods.
include Demo

# Prepends instance methods.
prepend Demo

# Adds refinements.
using Demo

💡 Check out the Refinements article for a deeper dive on how to leverage refinements.

next

The next keyword allows you to skip the rest of the block logic when used in an enumerable. Here’s an example where if the current number is equal to 2, then it is skipped entirely:

(1..3).each { |number| number == 2 ? next : puts(number) }

# 1
# 3

Lesser known is next can accept an argument. Here’s a modified example of the above where if 2 is detected, we answer Two! instead:

(1..3).each do |number|
  next puts("Two!") if number == 2

  puts number
end

# 1
# Two!
# 3

nil

The nil keyword — also known as the Billion Dollar Mistake — is what you get when something isn’t defined. Example:

@demo  # nil

It is also an object:

nil.class  # NilClass

Because nil exists in the language, you can end up in hard to debug situations and is commonly the cause of NoMethodError exceptions. A better way to avoid nil — or at least significantly reduce its use — is to use monads as provided via the Dry Monads gem.

not

The not keyword is used to invert a boolean expression and is not commonly used since it has a lower precedence than ! but can be useful in situations where readability is a concern. Example:

# Lower precedence.
puts "demo" if not false  # "demo"

# Higher precedence.
puts "demo" if !false     # "demo"

or

The or keyword is a short-circuit boolean with lower precedence than || and is best used for control flow instead of yielding a boolean. In most cases, you want to use || but or improves readability in situations where you want to do one operation or another and don’t care if the combination of both operations answers a boolean or not. Example:

questionable_authorizer = lambda do |login, password|
  (login == "admin" or password == "open_sesame") ? "Allowed" : "Denied"
end

puts questionable_authorizer.call("admin", "hacked")        # Allowed
puts questionable_authorizer.call("hacked", "open_sesame")  # Allowed
puts questionable_authorizer.call("invalid", "password")    # Denied

redo

The redo keyword is a less commonly used keyword that is best used to restart execution within a block. Here’s a powerful, but abstract, example where you redo asynchronous processes (i.e. jobs) if you detect a failure when calling a job that answers Dry Monads:

require "dry/monads"

include Dry::Monads[:result]

jobs.each do |job| redo if job.call in Failure }

Alternatively, here’s a more concrete example where random numbers are generated, checked if equal to a random number within the block, and the block is restarted only if the current number and the random number match:

numbers = Array.new(3) { rand(1..10) }

numbers.each do |number|
  puts "Number: #{number}."
  redo if number == rand(1..3)
end

⚠️ As powerful as redo is, you must ensure the number of attempts is capped or you can easily end up in an infinite loop.

rescue

The rescue keyword defines how to handle an exception within an exception block. Example:

begin
  require "some/file"
rescue LoadError => error
  puts error.message
end

# "cannot load such file -- some/file"

With the above, we print the error message when the LoadError is encountered. The rescue keyword can also accept a list of exceptions:

require "json"

def load path
  JSON.load_file path
rescue TypeError, Errno::ENOENT, JSON::ParserError => error
  puts error.message
end

The above is handy when needing to capture a series of exceptions and deal with the error in the same manner. This might not always be applicable but can tighten up the logic.

retry

The retry keyword is similar in nature to the redo keyword and is used to restart execution when an exception is encountered. Here’s an example where a URL is pinged, up to three times, before giving up:

require "http"

module Pinger
  def self.call url, count: 1, max: 3
    count += 1
    puts "Pinging #{url}..."
    HTTP.timeout(1).get url
  rescue HTTP::ConnectTimeoutError
    count <= max ? retry : puts("Unable to reach: #{url}.")
  end
end

Pinger.call "https://abc123.com"

# Pinging https://abc123.com...
# Pinging https://abc123.com...
# Pinging https://abc123.com...
# Unable to reach: https://abc123.com.

⚠️ As powerful as retry is, you must ensure the number of attempts is capped or you can easily end up in an infinite loop.

return

The return keyword allows you to explicitly exit a method or exit a method early when used with a guard clause. Examples:

# Implicit.
def demo
  1 + 2
end

# 3

# Explicit.
def demo
  1 + 2
  return "End"
end

# "End"

# Guard clause.
def demo text
  return if text == "hello"
  text
end

# nil

self

The self keyword answers the object that the current method is attached to and dynamically changes relative to the current scope. Example:

puts self.inspect
# main

demo = Module.new { def self.spy = puts self.inspect }
demo.spy
# #<Module:0x00000001158717c8>

class Demo
  def self.spy = puts self.inspect
end

Demo.spy
# Demo

super

The super keyword allows you to message the current method in a superclass and is primarily used in conjunction with inheritance. Example:

Parent = Class.new { def demo = puts "I'm the parent." }

class Child < Parent
  def demo
    super
    puts "I'm the child."
  end
end

Child.new.demo

# "I'm the parent."
# "I'm the child."

💡 For more sophisticated examples of the super keyword, see the Infusible and/or Transactable gems.

then

The then keyword is primarily used with case expressions although can be used with other control structures such as with the if keyword but is not recommended since it’s extraneous. The best use of then is with one-line conditional logic in a case expression:

case object
  when Symbol then puts "Is a symbol."
  when String then puts "Is a string."
  else puts "Unknown: #{object}."
end

With the above, you can see then allows you to define branch logic on a single line for concise case expressions. Otherwise, you can avoid then by using multiple lines to define your case expression (at the cost of being more verbose):

case object
  when Symbol
    puts "Is a symbol."
  when String
    puts "Is a string."
  else
    puts "Unknown: #{object}."
end

true

The true keyword is the companion to the false keyword as both represent boolean logic even though a boolean type doesn’t exist in Ruby. In fact, the true keyword is an object:

true.class  # TrueClass

undef

The undef keyword is the opposite of the def keyword in that it allows you to undefine a previously defined method:

def demo = "This is a demo."

demo  # "This is a demo."

undef :demo

demo
# undefined local variable or method `demo' for main:Object (NameError)

You can also undefine multiple methods at once using either symbols or the actual method name. Example:

undef one, :two, three

unless

The unless keyword is a control flow keyword that is the opposite of the if keyword and is best used as a multi-line expression or as a guard clause. It can be used with else but is strongly discouraged since it leads to hard to read code that should be rewritten to use the if keyword instead. Example:

# Multi-line.
unless "blue" == "green"
  puts "Blue is not green."
end

# "Blue is not green."
# Guard clause.
puts "Blue is not green." unless "blue" == "green"
# "Blue is not green."

until

The until keyword is meant for loops which iterate until a certain condition is met. Example:

count = 1

until count > 3
  puts count
  count += 1
end

# 1
# 2
# 3

You can optionally add the do keyword after the until condition but is not recommended since it’s extraneous. You can also use the break keyword to exit the loop early.

when

The when keyword is used in conjunction with case expressions for control flow by allowing you to define multiple branches of conditional logic:

case object
  when Symbol then puts "Is a symbol."
  when String then puts "Is a string."
  else puts "Unknown: #{object}."
end

while

The while keyword is meant for loops which iterate until a certain condition is met. Example:

count = 1

while count <= 3
  puts count
  count += 1
end

# 1
# 2
# 3

You can optionally add the do keyword after the while condition but is not recommended since it’s extraneous. You can also use the break keyword to exit the loop early.

yield

The yield keyword allows you to hand over control to the current method’s block. Example:

def demo
  puts yield if block_given?
end

demo           # nil
demo { "Hi" }  # "Hi"

The yield keyword can also accept an argument:

def demo text = "This is a demo."
  puts yield(text) if block_given?
end

demo                                     # nil
demo { |text| text.tr ".", "!" }         # "This is a demo!"
demo("Hi.") { |text| text.tr ".", "!" }  # "Hi!"