By now you can read basic Ruby and understand classes. Good. But to read real Ruby comfortably, you also need the patterns Ruby developers lean on all the time.
This means understanding a few core ideas:
- procs
- lambdas
- closures
Enumerable
These are not academic features. They show up everywhere in Ruby, Rails, and AI-adjacent code that transforms data, filters records, builds pipelines, and wraps behavior.
Let’s make them practical.
A proc is a chunk of callable behavior
A proc lets you store behavior in a variable and call it later.
upcase_text = Proc.new { |text| text.upcase }
puts upcase_text.call("ruby for ai")
Output:
RUBY FOR AI
That may look small, but it matters. You can pass behavior around just like data.
For example, maybe you want a reusable text post-processor:
clean_output = Proc.new do |text|
text.strip.gsub(/\s+/, " ")
end
raw = " hello from model "
puts clean_output.call(raw)
Output:
hello from model
Lambdas are stricter procs
Ruby has both Proc and lambda. They are similar, but lambdas behave more like regular methods.
formatter = lambda { |name| "Hello, #{name}" }
puts formatter.call("Advait")
You can also write it with stabby lambda syntax:
formatter = ->(name) { "Hello, #{name}" }
puts formatter.call("Ruby")
The important practical difference is argument handling.
loose_proc = Proc.new { |a, b| "#{a} #{b}" }
strict_lambda = ->(a, b) { "#{a} #{b}" }
puts loose_proc.call("hello")
# puts strict_lambda.call("hello")
The proc call works and silently sets b to nil.
The lambda version would raise an error because it expects exactly two arguments.
In real code, prefer lambdas when you want something method-like and predictable.
Closures remember their surrounding state
A closure is callable code that captures variables from the scope where it was created.
That sounds abstract. Here is the practical version.
def build_prompt_wrapper(prefix)
->(question) { "#{prefix}: #{question}" }
end
support_prompt = build_prompt_wrapper("Support assistant")
teaching_prompt = build_prompt_wrapper("Teaching assistant")
puts support_prompt.call("Reset my password")
puts teaching_prompt.call("Explain Ruby blocks")
Output:
Support assistant: Reset my password
Teaching assistant: Explain Ruby blocks
The lambda “remembers” prefix even after build_prompt_wrapper has finished.
That is a closure.
This is useful for AI code because you often want configurable behavior without creating a whole new class every time.
Blocks: Ruby’s built-in callback style
You already saw blocks in iterators. They are central to Ruby.
A method can yield to a block:
def with_timing
start = Time.now
result = yield
finish = Time.now
puts "Took #{(finish - start).round(3)}s"
result
end
response = with_timing do
sleep 0.2
"model response"
end
puts response
Output:
Took 0.2s
model response
This pattern is everywhere in Ruby libraries: open a file, wrap a database transaction, benchmark a call, retry a request.
If a method uses yield, it expects a block.
Turn a block into a proc with &
Sometimes you want to accept a block and store or forward it.
def run_pipeline(input, &step)
step.call(input)
end
result = run_pipeline(" hello ") { |text| text.strip.upcase }
puts result
The &step turns the block into a proc object.
You do not need this every day, but when you see &block in method definitions, now you know what is happening.
Enumerable is where Ruby gets fun
Enumerable gives collection classes a pile of useful methods like:
mapselectrejectfindreducegroup_byeach_with_object
You will use these constantly.
Let’s work with a realistic AI-flavored data set.
chunks = [
{ id: 1, text: "Ruby is elegant", score: 0.91 },
{ id: 2, text: "Rails ships fast", score: 0.87 },
{ id: 3, text: "Bananas are yellow", score: 0.21 }
]
map: transform each item
texts = chunks.map { |chunk| chunk[:text] }
p texts
select: keep matching items
relevant = chunks.select { |chunk| chunk[:score] > 0.8 }
p relevant
find: get the first match
best_match = chunks.find { |chunk| chunk[:id] == 2 }
p best_match
reduce: combine values
total_score = chunks.reduce(0) { |sum, chunk| sum + chunk[:score] }
puts total_score
These methods let you write concise code that still reads clearly.
each_with_object is cleaner than manual mutation
Beginners often do this:
lookup = {}
chunks.each do |chunk|
lookup[chunk[:id]] = chunk[:text]
end
p lookup
That works. But Ruby has a neater pattern:
lookup = chunks.each_with_object({}) do |chunk, hash|
hash[chunk[:id]] = chunk[:text]
end
p lookup
This shows up a lot in Ruby codebases.
Chain enumerable methods to build data pipelines
This is where Ruby becomes really pleasant for AI data work.
results = chunks
.select { |chunk| chunk[:score] > 0.5 }
.map { |chunk| chunk[:text].upcase }
.sort
p results
You can think of this as a small transformation pipeline.
That style is perfect for:
- preparing prompt context
- cleaning API responses
- ranking search hits
- formatting model output
- generating summaries from collections
Group data with group_by
Suppose you classify chunks by source type.
documents = [
{ source: "faq", text: "Reset password", score: 0.8 },
{ source: "faq", text: "Update email", score: 0.7 },
{ source: "docs", text: "Install Ruby", score: 0.95 }
]
grouped = documents.group_by { |doc| doc[:source] }
p grouped
This is handy when building retrieval pipelines or dashboards.
Use symbols for lightweight keys
You have already seen hashes with symbol keys:
{ score: 0.91, source: "docs" }
This is idiomatic Ruby. Most enumerable examples in real code will use symbol keys like :score and :source.
A practical example: rerank and format context
Let’s put the patterns together.
chunks = [
{ text: "Ruby blocks are closures", score: 0.82 },
{ text: "Rails uses MVC", score: 0.76 },
{ text: "Cats are animals", score: 0.12 }
]
format_chunk = ->(chunk) { "- #{chunk[:text]} (#{chunk[:score]})" }
context = chunks
.select { |chunk| chunk[:score] >= 0.75 }
.sort_by { |chunk| -chunk[:score] }
.map(&format_chunk)
.join("\n")
puts context
Output:
- Ruby blocks are closures (0.82)
- Rails uses MVC (0.76)
That is the kind of data shaping you do constantly in AI applications.
Proc or class?
A good rule of thumb:
Use a proc or lambda when:
- behavior is small
- it is local to one flow
- it does one simple transformation
Use a class when:
- behavior has multiple steps
- it needs configuration or dependencies
- it has state
- it deserves tests of its own
Do not turn every three-line transformation into a class. Do not turn every meaningful subsystem into a lambda either. Use judgment.
What to remember
Here is the short version:
- a proc is callable behavior stored in an object
- a lambda is a stricter, more method-like proc
- closures remember variables from surrounding scope
- blocks are Ruby’s built-in callback mechanism
-
Enumerablegives you powerful collection methods - chaining
select,map,find,reduce, and friends is normal Ruby
If you get comfortable with these, a lot of Ruby code stops looking magical and starts looking obvious.
Try this next
Save this as ruby_patterns.rb and run:
ruby ruby_patterns.rb
Then extend it:
- add
rejectto remove bad results - use
group_byon a larger document list - create a lambda that truncates long model outputs
- build a tiny prompt-cleaning pipeline with chained enumerable calls
Next, we’ll move into Rails and build your first app so all this Ruby stops living in toy scripts and starts powering a real web application.
Top comments (0)