Ruby on Rails Zero to Advanced #2: Arrays, Hashes, Blocks & Methods
In part 1 we covered variables, strings and symbols. Now we move to the four things you will see in almost every Rails file: arrays, hashes, blocks and methods. A Rails controller action is a method, request parameters arrive as a hash, a list of records behaves like an array, and you loop over them with blocks. Master these and Rails code becomes much easier to read.
All examples run in irb. Hash output uses the Ruby 3.4 format ({name: "Budi"}); older Ruby versions print the same hash as {:name=>"Budi"}, which means exactly the same thing.
Arrays: ordered lists
An array holds an ordered list of values. Indexes start at 0, and negative indexes count from the end.
fruits = ["apple", "banana", "mango"]
fruits[0]
fruits[-1]
fruits[10]
fruits.first(2)
fruits << "durian"
fruits.length
fruits.include?("mango")
fruits.sort
fruits.join(", ")
Output:
=> ["apple", "banana", "mango"]
=> "apple"
=> "mango"
=> nil
=> ["apple", "banana"]
=> ["apple", "banana", "mango", "durian"]
=> 4
=> true
=> ["apple", "banana", "durian", "mango"]
=> "apple, banana, mango, durian"
Reading an index that does not exist returns nil instead of raising an error. The << operator ("shovel") appends to the array. Two handy shortcuts:
%w[budi siti andi]
[1, nil, 2, nil].compact
[3, 1, 2].sort.reverse
Output:
=> ["budi", "siti", "andi"]
=> [1, 2]
=> [3, 2, 1]
Hashes: key-value pairs
A hash stores values by key. In Rails you meet hashes constantly: params, options passed to methods, JSON payloads and query conditions such as where(status: "active").
user = { name: "Budi", email: "budi@example.com", age: 25 }
user[:name]
user[:phone]
user.fetch(:email)
user[:city] = "Surabaya"
user
user.keys
user.key?(:age)
user.except(:email)
Output:
=> {name: "Budi", email: "budi@example.com", age: 25}
=> "Budi"
=> nil
=> "budi@example.com"
=> "Surabaya"
=> {name: "Budi", email: "budi@example.com", age: 25, city: "Surabaya"}
=> [:name, :email, :age, :city]
=> true
=> {name: "Budi", age: 25, city: "Surabaya"}
{ name: "Budi" } is shorthand for { :name => "Budi" }: the keys are symbols. Use fetch when a key must exist; it raises an error instead of quietly returning nil:
user.fetch(:phone)
user.fetch(:phone, "not set")
Output:
(irb):1:in 'Hash#fetch': key not found: :phone (KeyError)
=> "not set"
Be careful: string keys and symbol keys are different keys.
data = { "name" => "Siti" }
data[:name]
data["name"]
Output:
=> {"name" => "Siti"}
=> nil
=> "Siti"
Rails' params object hides this difference for you (it accepts both), but plain Ruby hashes do not.
Blocks and iterators
A block is a chunk of code you pass to a method, written between { } for one-liners or do ... end for multiple lines. The values between pipes (|n|) are the block parameters. Iterators are methods that call the block once per element.
[1, 2, 3].each { |n| puts n * 10 }
%w[budi siti].each_with_index do |name, i|
puts "#{i + 1}. #{name.capitalize}"
end
Output:
10
20
30
=> [1, 2, 3]
1. Budi
2. Siti
=> ["budi", "siti"]
each returns the original array, so it is used for side effects (printing, saving). When you want a new collection, reach for the transforming iterators:
numbers = [1, 2, 3, 4, 5, 6]
numbers.map { |n| n * n }
numbers.select { |n| n.even? }
numbers.reject(&:even?)
numbers.find { |n| n > 3 }
numbers.sum
numbers.map(&:to_s)
numbers.reduce(1) { |product, n| product * n }
Output:
=> [1, 2, 3, 4, 5, 6]
=> [1, 4, 9, 16, 25, 36]
=> [2, 4, 6]
=> [1, 3, 5]
=> 4
=> 21
=> ["1", "2", "3", "4", "5", "6"]
=> 720
&:even? is shorthand for { |n| n.even? }. You will see it often in Rails code, for example users.map(&:email).
Hashes have iterators too; the block receives the key and the value:
prices = { "coffee" => 25_000, "tea" => 15_000, "juice" => 30_000 }
prices.each { |item, price| puts "#{item}: Rp#{price}" }
prices.select { |_, price| price > 20_000 }
prices.sum { |_, price| price }
prices.min_by { |_, price| price }
Output:
coffee: Rp25000
tea: Rp15000
juice: Rp30000
=> {"coffee" => 25000, "tea" => 15000, "juice" => 30000}
=> {"coffee" => 25000, "juice" => 30000}
=> 70000
=> ["tea", 15000]
An underscore (_) is the convention for a block parameter you do not use.
Methods
Methods are defined with def. Ruby returns the value of the last expression automatically, so you rarely write return.
def greet(name, greeting = "Hello")
"#{greeting}, #{name}!"
end
greet("Budi")
greet("Siti", "Hi")
Output:
=> :greet
=> "Hello, Budi!"
=> "Hi, Siti!"
Keyword arguments make calls self-describing and order-independent. Rails uses them everywhere (think validates :title, presence: true).
def create_user(name:, role: "member")
"#{name} (#{role})"
end
create_user(name: "Budi")
create_user(role: "admin", name: "Siti")
create_user(role: "admin")
Output:
=> :create_user
=> "Budi (member)"
=> "Siti (admin)"
(irb):1:in 'Object#create_user': missing keyword: :name (ArgumentError)
Your own methods can accept a block with yield. This is how Rails methods like transaction do ... end or respond_to do |format| work.
def with_logging
puts "Start"
result = yield
puts "Done"
result
end
with_logging { 2 + 3 }
Output:
Start
Done
=> 5
Tips and common mistakes
- Using each when you want map.
result = list.each { ... }gives you the original list back. Usemapto build a new one. - Mixing string and symbol keys.
{ "name" => "Siti" }[:name]isnil. Pick one style (symbols in Ruby code). - Braces vs do/end. Use
{ }for short one-line blocks anddo ... endfor multi-line blocks. It is the community convention and avoids precedence surprises. - Forgetting the implicit return. A
putson the last line makes your method returnnil. - Mutating while iterating. Do not add or delete elements of an array inside its own
each; build a new array withselect/rejectinstead.
Exercise
You receive a list of orders. Print the names of customers who paid and the total paid amount, using only select, map and sum:
orders = [
{ customer: "Budi", total: 120_000, paid: true },
{ customer: "Siti", total: 85_000, paid: false },
{ customer: "Andi", total: 200_000, paid: true }
]
paid = orders.select { |order| order[:paid] }
p paid.map { |order| order[:customer] }
p paid.sum { |order| order[:total] }
Output:
["Budi", "Andi"]
320000
Bonus: wrap the logic in a method paid_summary(orders) that returns a hash like {customers: [...], total: 320000}.
Summary
Arrays keep ordered lists, hashes store key-value pairs, blocks let you pass behaviour into methods, and iterators like map, select and sum replace most manual loops. Methods return their last expression and keyword arguments keep calls readable. In part 3 we will bundle data and behaviour together with classes, modules and mixins, the foundation of every Rails model.
Related articles
PostgreSQL Zero to Advanced #1: Introduction & Setup
Start the series by understanding what a relational database is, installing PostgreSQL locally, and getting comfortable with psql commands like \l, \c and \dt.
PHP Zero to Advanced #1: Introduction & Setup
Start your PHP journey: what PHP is, how to install PHP 8.2+, run the built-in development server, and write your very first script with echo.
Python Zero to Advanced #1: Introduction & Setup
Start your Python journey: what Python is, how to install it, how to use the REPL, print your first output, and run .py files from the terminal.