14

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 map is often clearer than building arrays manually with each;
  • use select and zip to 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}"
end
shortnote.md
ruby

Functional-style Ruby often looks like this:

names.zip(scores)
     .select { |_name, score| score >= 60 }
     .map { |name, score| "#{name}: #{score}" }
shortnote.md
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
end
worked_examples.md
ruby

Functional-style Ruby:

quantities.zip(prices).map { |qty, price| qty * price }
worked_examples.md
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}"
end
worked_examples.md
ruby

Functional-style Ruby:

scores.select { |score| score >= 60 }
      .map { |score| "PASS: #{score}" }
worked_examples.md
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)
worked_examples.md
ruby

This is a good real-world use of lambdas because each step has a meaningful name.

Cheatsheet

Transform

scores.map { |score| score + 5 }
cheatsheet.md
ruby

Filter

scores.select { |score| score >= 60 }
cheatsheet.md
ruby

Pair aligned collections

names.zip(scores)
cheatsheet.md
ruby

Lambda

classifier = ->(score) { score >= 60 }
cheatsheet.md
ruby

Composition

def self.compose(*functions)
  ->(input) { functions.reverse.reduce(input) { |value, fn| fn.call(value) } }
end
cheatsheet.md
ruby

Comparison guide โš–๏ธ

  • each: do something for side effects
  • map: build a transformed collection
  • select: keep matching items
  • zip: 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 ๐Ÿชœ

  1. Read overview.md.
  2. Read shortnote.md and compare the old-style and functional-style examples.
  3. Study worked_examples.md.
  4. Keep cheatsheet.md open while reading example.rb.
  5. Complete the basic exercise.
  6. Complete the advanced exercise.

Sprint plan ๐Ÿƒ

Sprint 1: Transform one collection

  • use map when 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 zip when arrays are aligned by position;
  • use select before map when 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 zip and map?
  • 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
en/topic_14_functional_style_lambdas/example.rb
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
en/topic_14_functional_style_lambdas/basic_exercise.rb
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
en/topic_14_functional_style_lambdas/adv_exercise.rb
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
en/topic_14_functional_style_lambdas/answer_basic_exercise.rb
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
en/topic_14_functional_style_lambdas/answer_adv_exercise.rb
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
en/topic_14_functional_style_lambdas/tests/topic_14_functional_style_lambdas_spec.rb
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}")"
en/topic_14_functional_style_lambdas/run_topic_tests.sh
Ruby course source

Study Prompts

  1. Read the spec first and identify which behaviors belong to the example, basic exercise, and advanced exercise.

  2. Attempt the exercises before opening the answer files, then compare the design choices.