Functional-Style Ruby for Everyday Data Problems
Ruby source files are converted directly from the English source tree.
en/topic_14_functional_style_lambdas
Overview
Why this topic matters ๐ก
Students often see map, zip, and lambdas as clever syntax. That is the wrong framing. These are practical tools for writing code that transforms data clearly, passes behavior explicitly, and avoids unnecessary mutable setup.
This topic is about the real value of a functional style in Ruby:
- take data in;
- transform it in small steps;
- keep each step honest;
- pass rules as values when behavior should vary.
Learning outcomes ๐ฏ
By the end of this topic, students should be able to:
- explain why
mapis often clearer than building arrays manually witheach; - use
selectandzipto shape data before transforming it; - pass lambdas into objects as behavior values;
- compose unary functions into a readable transformation pipeline;
- compare imperative loop-based code with functional-style Ruby and justify the tradeoff.
Assessment focus โ
Students should be able to say what improved when the code moved from manual loops to collection transforms, and also when not to force a pipeline.
Short Note
Ruby is not asking students to become pure functional programmers. It is asking them to notice that many ordinary problems are easier to read when code is written as a series of transformations instead of a series of mutations.
Imperative style often looks like this:
rows = []
names.each_with_index do |name, index|
score = scores[index]
next if score < 60
rows << "#{name}: #{score}"
endruby
Functional-style Ruby often looks like this:
names.zip(scores)
.select { |_name, score| score >= 60 }
.map { |name, score| "#{name}: #{score}" }ruby
The second version is not better because it is shorter. It is better because each step answers one question:
- how do we align the data?
- which rows do we keep?
- how do we transform them?
Ruby beauty in this topic:
- the code often follows the shape of the data problem;
- lambdas make small rules easy to pass around;
- composition lets a sequence of small functions read like a pipeline.
Ruby caution in this topic:
- pipelines can become opaque if each step is not meaningful;
- over-abstracting tiny lambdas can make code harder to debug;
- imperative code is still fine when stateful flow is the real problem.
Reflection prompt:
- Which version would be easier for another developer to review in a pull request?
Worked Examples
Example 1: Manual loop vs transformation pipeline ๐ก
Old style:
rows = []
quantities.each_with_index do |qty, index|
price = prices[index]
rows << qty * price
endruby
Functional-style Ruby:
quantities.zip(prices).map { |qty, price| qty * price }ruby
Why the second version is better here:
- alignment is explicit with
zip; - transformation is explicit with
map; - there is no temporary mutable array outside the loop.
Example 2: Filtering before transforming ๐ก
Old style:
labels = []
scores.each do |score|
next if score < 60
labels << "PASS: #{score}"
endruby
Functional-style Ruby:
scores.select { |score| score >= 60 }
.map { |score| "PASS: #{score}" }ruby
Why this matters:
- the "keep" decision is separated from the "transform" decision;
- each step can be tested or changed independently.
Example 3: Function composition for business rules ๐ก
Suppose a score should be curved, capped, then converted to a label. A large method can do that, but a composition pipeline keeps the stages visible:
transform = FunctionTools.compose(to_letter, clamp, curve)ruby
This is a good real-world use of lambdas because each step has a meaningful name.
Cheatsheet
Transform
scores.map { |score| score + 5 }ruby
Filter
scores.select { |score| score >= 60 }ruby
Pair aligned collections
names.zip(scores)ruby
Lambda
classifier = ->(score) { score >= 60 }ruby
Composition
def self.compose(*functions)
->(input) { functions.reverse.reduce(input) { |value, fn| fn.call(value) } }
endruby
Comparison guide โ๏ธ
each: do something for side effectsmap: build a transformed collectionselect: keep matching itemszip: align related collections by position
Study Guide
Topic purpose ๐ฏ
Learn that functional-style Ruby is not a party trick. It is a practical way to write data-transformation code that is easier to review, test, and extend.
Study sequence ๐ช
- Read
overview.md. - Read
shortnote.mdand compare the old-style and functional-style examples. - Study
worked_examples.md. - Keep
cheatsheet.mdopen while readingexample.rb. - Complete the basic exercise.
- Complete the advanced exercise.
Sprint plan ๐
Sprint 1: Transform one collection
- use
mapwhen the output is a reshaped version of the input; - avoid building arrays manually if the operation is purely transformational.
Sprint 2: Pair and filter data
- use
zipwhen arrays are aligned by position; - use
selectbeforemapwhen a filtering step is part of the problem.
Sprint 3: Pass behavior as a value
- let a lambda hold the rule;
- keep the object focused on structure, not on every possible rule variation.
Sprint 4: Compose functions
- build a pipeline from small unary functions;
- prefer named transformation steps over one large branching method.
Bridge to Topic 15
This topic focuses on transforming collections. Topic 15 continues the style by teaching accumulator-driven problems with reduce and each_with_object.
Reflection questions ๐ค
- What mutable state disappeared when the code moved to
zipandmap? - Why is an injected lambda better than hard-coding the classification rule?
- At what point would a plain named method be simpler than a composition pipeline?
Source Files and Tests
Ruby source files are converted directly from the English source tree.
# EXAMPLE CODE
# Topic: topic_14_functional_style_lambdas
#
# Purpose:
# - This file demonstrates a small functional-style transform using `zip` and `map`.
# - It should pass tests from the beginning.
# - Read it before solving the reporting and composition exercises.
class VectorMath
def pairwise_sum(left, right)
left.zip(right).map { |a, b| a + b }
end
end
Ruby course source
# STUDENT TASK (BASIC)
# Topic: topic_14_functional_style_lambdas
#
# What to do:
# - Pair names and scores with `zip`.
# - Use an injected lambda to decide whether each score passes.
# - Return structured rows instead of printing directly.
#
# Expected outcome:
# - You can solve a small reporting task with `zip`, `map`, and a lambda.
class StudentRoster
def initialize(classifier:)
@classifier = classifier
end
def rows(names, scores)
names.zip(scores).map do |name, score|
{
name: name,
score: score,
passed: @classifier.call(score)
}
end
end
end
Ruby course source
# STUDENT TASK (ADVANCED)
# Topic: topic_14_functional_style_lambdas
#
# Academic purpose:
# - Practice function composition as a practical Ruby technique.
# - Show how a sequence of lambdas can model a transformation pipeline.
#
# Real-world use case:
# - Reporting and normalization code often applies several steps in order:
# curve a score, clamp it, then convert it into a label.
# - Composition lets each step stay small while the pipeline remains reusable.
#
# Why Ruby is beautiful here:
# - Lambdas are lightweight enough to use as ordinary values.
# - Composition keeps each transformation step honest and testable.
# - The final pipeline reads like a sequence of meaning, not a long method body.
#
# What to do:
# - Build one composition helper.
# - Apply the composed transformation to a zipped list of names and scores.
# - Return user-facing labels as strings.
#
# Expected outcome:
# - Advanced tests pass and you can explain how the pipeline is assembled.
module FunctionTools
def self.compose(*functions)
->(input) { functions.reverse.reduce(input) { |value, fn| fn.call(value) } }
end
end
class GradePipeline
def initialize(transform:)
@transform = transform
end
def labels(names, scores)
names.zip(scores).map do |name, score|
"#{name}: #{@transform.call(score)}"
end
end
end
Ruby course source
# ANSWER KEY (BASIC)
# Topic: topic_14_functional_style_lambdas
#
# Solution idea:
# - Use `zip` to align names and scores by position.
# - Map over the paired values to build structured rows.
# - Let the lambda decide the pass/fail rule so the object stays generic.
class StudentRoster
def initialize(classifier:)
@classifier = classifier
end
def rows(names, scores)
names.zip(scores).map do |name, score|
{
name: name,
score: score,
passed: @classifier.call(score)
}
end
end
end
Ruby course source
# ANSWER KEY (ADVANCED)
# Topic: topic_14_functional_style_lambdas
#
# Solution idea:
# - Composition applies small functions from right to left.
# - The pipeline turns a raw numeric score into a final display label.
# - `GradePipeline` does not know the transformation details; it only applies the function.
module FunctionTools
def self.compose(*functions)
->(input) { functions.reverse.reduce(input) { |value, fn| fn.call(value) } }
end
end
class GradePipeline
def initialize(transform:)
@transform = transform
end
def labels(names, scores)
names.zip(scores).map do |name, score|
"#{name}: #{@transform.call(score)}"
end
end
end
Ruby course source
# This spec is your learning companion for topic_14_functional_style_lambdas.
#
# How to use this file:
# 1) Run tests and observe failures or successes.
# 2) Keep the EXAMPLE specs green from the beginning.
# 3) Implement the BASIC exercise as sprint 2 and 3: pairing data and injecting a lambda.
# 4) Implement the ADVANCED exercise as sprint 4: composing functions into a pipeline.
#
# Expected final result:
# - All examples in this file pass.
# - You understand how functional-style Ruby helps solve transformation problems.
require_relative "../example"
require_relative "../basic_exercise"
require_relative "../adv_exercise"
RSpec.describe "topic_14_functional_style_lambdas" do
describe "EXAMPLE purpose: understand a small data transform with zip and map" do
it "sums aligned vectors pairwise" do
math = VectorMath.new
expect(math.pairwise_sum([1, 2, 3], [10, 20, 30])).to eq([11, 22, 33])
end
end
describe "BASIC EXERCISE purpose: solve a reporting task with zip and a lambda rule" do
it "builds roster rows with pass/fail status" do
classifier = ->(score) { score >= 60 }
roster = StudentRoster.new(classifier: classifier)
expect(roster.rows(%w[Ana Bob], [88, 52])).to eq(
[
{ name: "Ana", score: 88, passed: true },
{ name: "Bob", score: 52, passed: false }
]
)
end
end
describe "ADVANCED EXERCISE purpose: compose lambdas into a reusable transformation pipeline" do
it "curves, clamps, and converts scores into labels" do
curve = ->(score) { score + 5 }
clamp = ->(score) { [score, 100].min }
to_letter = lambda do |score|
case score
when 90..100 then "A"
when 80...90 then "B"
when 70...80 then "C"
else "D"
end
end
transform = FunctionTools.compose(to_letter, clamp, curve)
pipeline = GradePipeline.new(transform: transform)
expect(pipeline.labels(%w[Ana Bob], [88, 76])).to eq(
["Ana: A", "Bob: B"]
)
end
end
end
Ruby course source
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
exec "${ROOT_DIR}/run_tests.sh" "$(basename "${SCRIPT_DIR}")"
Ruby course source
Study Prompts
Read the spec first and identify which behaviors belong to the example, basic exercise, and advanced exercise.
Attempt the exercises before opening the answer files, then compare the design choices.