Last post covered variables, methods, blocks, and classes. Now let's talk about the data structures you'll actually use every day: arrays, hashes, and the iterators that make them powerful.
Arrays: Ordered Lists
models = ["gpt-4", "claude-3", "gemini"]
models[0] # => "gpt-4"
models[-1] # => "gemini"
models.length # => 3
models.first # => "gpt-4"
models.last # => "gemini"
Adding and removing:
models << "llama-3" # append
models.push("mistral") # same thing
models.delete("gemini") # remove by value
models.pop # remove and return last
The << shovel operator is idiomatic Ruby. You'll see it everywhere.
Hashes: Key-Value Pairs
Hashes are Ruby's dictionaries. If you're building AI apps, you'll live in hashes — they map directly to JSON:
config = {
model: "gpt-4",
temperature: 0.7,
max_tokens: 1000,
stream: true
}
config[:model] # => "gpt-4"
config[:temperature] # => 0.7
config[:missing] # => nil (no error!)
Those :model, :temperature things are symbols — lightweight, immutable strings. Ruby uses them as hash keys by convention because they're faster than strings.
Older syntax you'll still see in codebases:
# Hashrocket style (equivalent)
config = {
:model => "gpt-4",
:temperature => 0.7
}
Both work. The key: value style is modern and preferred.
Iterators: Where Ruby Shines
This is Ruby's killer feature for data processing. Forget for loops.
each — Do something with every element
responses = ["Hello!", "How can I help?", "Goodbye."]
responses.each do |response|
puts "AI said: #{response}"
end
map — Transform every element
prompts = ["explain ruby", "what is rails", "how do blocks work"]
uppered = prompts.map { |p| p.upcase }
# => ["EXPLAIN RUBY", "WHAT IS RAILS", "HOW DO BLOCKS WORK"]
# Shorter: map with symbol-to-proc
uppered = prompts.map(&:upcase)
That &:upcase shorthand calls .upcase on each element. Elegant.
select and reject — Filter
scores = [0.92, 0.45, 0.87, 0.31, 0.95]
passing = scores.select { |s| s > 0.8 }
# => [0.92, 0.87, 0.95]
failing = scores.reject { |s| s > 0.8 }
# => [0.45, 0.31]
reduce — Accumulate into a single value
token_counts = [150, 230, 180, 95]
total = token_counts.reduce(0) { |sum, count| sum + count }
# => 655
# Shorthand
total = token_counts.sum # => 655 (Ruby knows)
find — First match
messages = [
{ role: "system", content: "You are helpful." },
{ role: "user", content: "Hello" },
{ role: "assistant", content: "Hi there!" }
]
first_user = messages.find { |m| m[:role] == "user" }
# => { role: "user", content: "Hello" }
Chaining: The Ruby Way
The real power is chaining these together:
raw_inputs = [" hello ", "", "WHAT IS AI? ", nil, " ", "explain ruby"]
cleaned = raw_inputs
.compact # remove nils
.map(&:strip) # trim whitespace
.reject(&:empty?) # remove blanks
.map(&:downcase) # normalize case
# => ["hello", "what is ai?", "explain ruby"]
Each method returns a new array, so you chain the next operation. No temp variables, no mutation, just a pipeline.
String Manipulation
Strings come up constantly when building prompts and parsing responses:
prompt = " What is machine learning? "
prompt.strip # => "What is machine learning?"
prompt.downcase # => " what is machine learning? "
prompt.include?("machine") # => true
prompt.gsub("machine", "deep") # => " What is deep learning? "
prompt.split(" ") # => ["What", "is", "machine", "learning?"]
Building prompts with heredocs:
context = "Ruby programming"
question = "How do blocks work?"
prompt = <<~PROMPT
You are an expert in #{context}.
Answer the following question concisely.
Question: #{question}
Provide a code example in your answer.
PROMPT
The <<~PROMPT syntax creates a multi-line string with interpolation and strips leading indentation. You'll use this for every prompt template.
Joining arrays into strings:
tags = ["ruby", "ai", "tutorial"]
tags.join(", ") # => "ruby, ai, tutorial"
tags.join("\n") # => each on its own line
A Practical Example
Let's combine everything into something real — processing a batch of API responses:
responses = [
{ model: "gpt-4", tokens: 150, score: 0.92, content: "Ruby is great." },
{ model: "claude-3", tokens: 230, score: 0.45, content: "" },
{ model: "gpt-4", tokens: 180, score: 0.87, content: "Blocks are closures." },
{ model: "gemini", tokens: 95, score: 0.31, content: nil }
]
good_responses = responses
.select { |r| r[:score] > 0.8 }
.reject { |r| r[:content].nil? || r[:content].empty? }
.map { |r| "#{r[:model]}: #{r[:content]}" }
puts good_responses
# gpt-4: Ruby is great.
# gpt-4: Blocks are closures.
total_tokens = responses.sum { |r| r[:tokens] }
puts "Total tokens used: #{total_tokens}" # => 655
Filter, transform, aggregate. Three lines. No loops, no index tracking, no off-by-one errors.
What's Next
Next up: object-oriented Ruby — classes, modules, inheritance, and mixins. The patterns that make Rails (and every Ruby gem) tick.
You now have enough data structure knowledge to read most Ruby code. The rest is practice.
Top comments (0)