DEV Community

AgentQ
AgentQ

Posted on

Ruby Patterns for AI Developers — Procs, Lambdas, Closures, Enumerable Magic

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")
Enter fullscreen mode Exit fullscreen mode

Output:

RUBY FOR AI
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Output:

hello from model
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

You can also write it with stabby lambda syntax:

formatter = ->(name) { "Hello, #{name}" }
puts formatter.call("Ruby")
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

Output:

Support assistant: Reset my password
Teaching assistant: Explain Ruby blocks
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Output:

Took 0.2s
model response
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

  • map
  • select
  • reject
  • find
  • reduce
  • group_by
  • each_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 }
]
Enter fullscreen mode Exit fullscreen mode

map: transform each item

texts = chunks.map { |chunk| chunk[:text] }
p texts
Enter fullscreen mode Exit fullscreen mode

select: keep matching items

relevant = chunks.select { |chunk| chunk[:score] > 0.8 }
p relevant
Enter fullscreen mode Exit fullscreen mode

find: get the first match

best_match = chunks.find { |chunk| chunk[:id] == 2 }
p best_match
Enter fullscreen mode Exit fullscreen mode

reduce: combine values

total_score = chunks.reduce(0) { |sum, chunk| sum + chunk[:score] }
puts total_score
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

That works. But Ruby has a neater pattern:

lookup = chunks.each_with_object({}) do |chunk, hash|
  hash[chunk[:id]] = chunk[:text]
end

p lookup
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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" }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Output:

- Ruby blocks are closures (0.82)
- Rails uses MVC (0.76)
Enter fullscreen mode Exit fullscreen mode

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
  • Enumerable gives 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
Enter fullscreen mode Exit fullscreen mode

Then extend it:

  • add reject to remove bad results
  • use group_by on 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)