diff --git a/ideas/split-loop-with-lambda/split-loop-with-lambda-learning-hour.md b/ideas/split-loop-with-lambda/split-loop-with-lambda-learning-hour.md index 241c4bc..d95e1bb 100644 --- a/ideas/split-loop-with-lambda/split-loop-with-lambda-learning-hour.md +++ b/ideas/split-loop-with-lambda/split-loop-with-lambda-learning-hour.md @@ -84,6 +84,9 @@ If there is time, show one slide: the slice graphic from the ## Demo - The weather slice (10 min) +Full script for the *complex* variant of this demo — on `DailyForecasts.printSalesForecasts()`, +with SVGs and narration beats — is in [`storyboard/daily-forecasts-demo.md`](storyboard/daily-forecasts-demo.md). + Do the six mechanics on a whiteboard-projection slice of `LongRangeForecast` — only the `DATES` loop: diff --git a/ideas/split-loop-with-lambda/storyboard/daily-forecasts-demo.md b/ideas/split-loop-with-lambda/storyboard/daily-forecasts-demo.md new file mode 100644 index 0000000..bc43dd8 --- /dev/null +++ b/ideas/split-loop-with-lambda/storyboard/daily-forecasts-demo.md @@ -0,0 +1,241 @@ +# Demo script — printSalesForecasts + +This is what I demo in the Split Loop with Lambda learning hour. I demo on +`DailyForecasts.printSalesForecasts()` — the messier sibling of the method the pairs +will refactor in the exercise. Same pattern, but this loop has three unwanted +dependencies: `Scorer` (with a static field for extra credit), `System.out`, and the +clock. + +I remove the clock before we start. The rest stays in. Ten minutes, six steps. + +## Before we start (2 min) + +Strip the two header lines: + +```java +// var formatter = DateTimeFormatter.ISO_LOCAL_DATE.withZone(ZoneId.from(ZoneOffset.UTC)); +// System.out.println("Forecast at: " + formatter.format(Instant.now())); +``` + +I say this out loud while I delete them: *the clock is a third dependency, and we do +one split at a time.* + +Then add the test from step 1 and run it once, so the flakiness is a memory before I +start typing. + +--- + +## Step 0 — count the concerns + +![](svg/d0-start.svg) + +Mutation and value read, a conditional, IO — in twelve lines. I ask: *what does this +loop need from the outside?* Answer: *given a name, what is the sales score?* That +question is the lambda we are about to extract. The rest — printing, the clock — is +not the loop's business. + +--- + +## Step 1 — an approval test: red & flaky + +![](svg/d1-test.svg) + +```java +@Test +void salesForecasts() { + var out = new ByteArrayOutputStream(); + var originalOut = System.out; + System.setOut(new PrintStream(out)); + try { + new DailyForecasts().printSalesForecasts(); + } finally { + System.setOut(originalOut); + } + Approvals.verify(out.toString()); +} +``` + +Run it. Red, and every run disagrees with the last. Why: `Scorer` re-rolls `flavour` +with a `Random`. Same situation as the exercise method — that is the point of +demoing this one. + +--- + +## Step 2 — extract the dependency-using code into a lambda + +![](svg/d2-extract-lambda.svg) + +**After:** + +```java +public void printSalesForecasts() { + var names = List.of("Steve", "Julie", "Francis"); + Function nameToSalesScore = name -> { // ← NEW: the lambda... + if (Objects.equals(name, "Steve")) { + Scorer.flavour = IceCream.Strawberry; // ← ...holds BOTH scorer touches + } else { + scorer.updateSelection(); + } + return scorer.getSalesForecast(); + }; + for (String name : names) { + System.out.println(name + " score: " + nameToSalesScore.apply(name)); // ← invocation replaced getSalesForecast + } +} +``` + +I say: *watch what goes inside — all of it. The static write too. If you leave the +`Scorer.flavour = ...` behind in the loop, the extraction later drags the whole class +in with it.* (The exercise pairs will meet this as the drift; here I show it before +it can happen.) + +**Checkpoint:** compiles; behaviour unchanged; test still red & flaky. Also true and +worth saying: *this lambda is not pure. The unwanted dependency lives inside it — +that is exactly what we want. It is an adapter.* + +--- + +## Step 3 — slide the lambda definition to the top of the method + +![](svg/d3-slide.svg) + +**After:** + +```java +public void printSalesForecasts() { + Function nameToSalesScore = name -> { // ← SLID to the top... + if (Objects.equals(name, "Steve")) { + Scorer.flavour = IceCream.Strawberry; + } else { + scorer.updateSelection(); + } + return scorer.getSalesForecast(); + }; + var names = List.of("Steve", "Julie", "Francis"); + for (String name : names) { + System.out.println(name + " score: " + nameToSalesScore.apply(name)); // ← ...invocation stays here + } +} +``` + +Slide, don't move. Watch the invocation — it stays exactly where the code used to +be. + +**Checkpoint:** compiles; test still red & flaky (unchanged behaviour). + +--- + +## Step 4 — extract the loop; the lambda becomes a parameter + +![](svg/d4-extract.svg) + +Select from the loop through its closing brace, Extract Method. + +**After:** + +```java +public void printSalesForecasts() { + Function nameToSalesScore = name -> { + if (Objects.equals(name, "Steve")) { + Scorer.flavour = IceCream.Strawberry; + } else { + scorer.updateSelection(); + } + return scorer.getSalesForecast(); + }; + var names = List.of("Steve", "Julie", "Francis"); + var scores = collectSalesScores(names, nameToSalesScore); // ← EXTRACTED + for (var entry : scores.entrySet()) { + System.out.println(entry.getKey() + " score: " + entry.getValue()); + } +} + +private static LinkedHashMap collectSalesScores( // ← NEW — and already static + List names, Function nameToSalesScore) { + var scores = new LinkedHashMap(); + for (String name : names) { + scores.put(name, nameToSalesScore.apply(name)); + } + return scores; +} +``` + +(`LinkedHashMap` because the demo prints names in order; a plain `HashMap` would +shuffle them and confuse everyone. I say that out loud too.) + +**Checkpoint — the drift test:** + +- `grep -n scorer src/main/java/codingdojo/DailyForecasts.java` — nothing in the new + method. +- Try to make it `static` (it already is). If the static write had stayed in the + loop, this would not compile. +- `System.out` is still in `printSalesForecasts` — that is fine. One split at a + time; the printing loop is next time's material. + +--- + +## Step 5 — the overload, and the test goes green + +![](svg/d5-overload-test.svg) + +Change Signature on the big method: the lambda becomes a parameter. A no-arg overload +bakes in the real lambda, so callers are unchanged. + +```java +public void printSalesForecasts() { // ← no-arg overload: callers unchanged + printSalesForecasts(name -> { + if (Objects.equals(name, "Steve")) { + Scorer.flavour = IceCream.Strawberry; + } else { + scorer.updateSelection(); + } + return scorer.getSalesForecast(); + }); +} + +public void printSalesForecasts( // ← the seam: λ is a parameter + Function nameToSalesScore) { + var names = List.of("Steve", "Julie", "Francis"); + var scores = collectSalesScores(names, nameToSalesScore); + for (var entry : scores.entrySet()) { + System.out.println(entry.getKey() + " score: " + entry.getValue()); + } +} +``` + +The test goes through the seam with a pure lambda: + +```java +@Test +void salesForecasts() { + var out = new ByteArrayOutputStream(); + var originalOut = System.out; + System.setOut(new PrintStream(out)); + try { + new DailyForecasts().printSalesForecasts( + name -> Objects.equals(name, "Steve") ? 9 : 11); + } finally { + System.setOut(originalOut); + } + Approvals.verify(out.toString()); +} +``` + +Approved output: + +``` +Steve score: 9 +Julie score: 11 +Francis score: 11 +``` + +**Checkpoint:** green — deterministically. `Scorer` is never touched by the test. + +--- + +## End of demo + +Same three moves as the exercise: extract, slide, extract. `Scorer` is out of the +loop, and the lambda made it visible that the whole question is just "given a name, +what is the sales score?" `System.out` and the clock are still in +`printSalesForecasts` — that is the next hour, not this one. diff --git a/ideas/split-loop-with-lambda/storyboard/svg/d0-start.svg b/ideas/split-loop-with-lambda/storyboard/svg/d0-start.svg new file mode 100644 index 0000000..5be87aa --- /dev/null +++ b/ideas/split-loop-with-lambda/storyboard/svg/d0-start.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + Demo — printSalesForecasts: one loop, three unwanted dependencies + The clock is already out (prep). Count the rest: a static side-channel write, a random re-roll, a value read, and IO. + + + + printSalesForecasts() + + + + for (String name : names) + if (name.equals("Steve")) + Scorer.flavour = Strawberry; + else + scorer.updateSelection(); + score = scorer.getSalesForecast(); + System.out.println(name + " score: " + score); + the weather/sales loop + + + + Scorer + Random + a static + field (flavour) + + + + + + + + System.out + IO — next time's split + + diff --git a/ideas/split-loop-with-lambda/storyboard/svg/d1-test.svg b/ideas/split-loop-with-lambda/storyboard/svg/d1-test.svg new file mode 100644 index 0000000..7886991 --- /dev/null +++ b/ideas/split-loop-with-lambda/storyboard/svg/d1-test.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + Step 1 — an approval test: red & flaky + Capture System.out, Approvals.verify it. Every run disagrees — Scorer re-rolls with a Random. + + + + DailyForecastsTest + capture System.out + Approvals.verify(...) + red & flaky ✗ + + + + printSalesForecasts() + + for (String name : names) + Scorer.flavour = ... / updateSelection() + score = scorer.getSalesForecast(); + System.out.println(...) + + + + + + Scorer + Random → different every run + + diff --git a/ideas/split-loop-with-lambda/storyboard/svg/d2-extract-lambda.svg b/ideas/split-loop-with-lambda/storyboard/svg/d2-extract-lambda.svg new file mode 100644 index 0000000..38dac27 --- /dev/null +++ b/ideas/split-loop-with-lambda/storyboard/svg/d2-extract-lambda.svg @@ -0,0 +1,40 @@ + + + + + + + + Step 2 — extract the dependency-using code into a lambda + ALL of it goes inside — the static write too. The lambda is the adapter; it is not supposed to be pure. + + + + printSalesForecasts() + + + + for (String name : names) + + + + λ nameToSalesScore + if (Steve) Scorer.flavour = Strawberry; + else scorer.updateSelection(); + return scorer.getSalesForecast(); + + + println(name + " score: " + λ.apply(name)); + + System.out.println stays in the loop — next time's split + names list lives above the loop, unchanged + + + + Scorer + static field + Random + both touches in the λ now + + + + diff --git a/ideas/split-loop-with-lambda/storyboard/svg/d3-slide.svg b/ideas/split-loop-with-lambda/storyboard/svg/d3-slide.svg new file mode 100644 index 0000000..4474c61 --- /dev/null +++ b/ideas/split-loop-with-lambda/storyboard/svg/d3-slide.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + Step 3 — slide the lambda definition to the top of the method + Slide, don't move: the definition slides up, the invocation stays exactly where it was. + + + + printSalesForecasts() + + + + λ nameToSalesScore + if (Steve) Scorer.flavour = Strawberry; + else scorer.updateSelection(); + return scorer.getSalesForecast(); + + + + for (String name : names) + + + + λ used to be defined here + + + println(name + " score: " + λ.apply(name)); + + + + slide + + + + Scorer + static field + Random + both touches in the λ now + + + + diff --git a/ideas/split-loop-with-lambda/storyboard/svg/d4-extract.svg b/ideas/split-loop-with-lambda/storyboard/svg/d4-extract.svg new file mode 100644 index 0000000..ff5a77e --- /dev/null +++ b/ideas/split-loop-with-lambda/storyboard/svg/d4-extract.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + Step 4 — extract the loop; the lambda becomes a parameter + collectSalesScores is static and scorer-free. The printing loop stays behind — one split at a time. + + + + printSalesForecasts() + + + + λ nameToSalesScore + static write + updateSelection + + getSalesForecast() + + + + var scores = + collectSalesScores(names, λ) + + + + for (entry : scores) + System.out.println(...) + IO stays — next time + + + + λ becomes a parameter + + + + collectSalesScores + static — no fields, all inputs are parameters + + + λ Function<String, Integer> nameToSalesScore + + + for (String name : names) + scores.put(name, + λ.apply(name)) + LinkedHashMap — keeps the order + + return scores; + + + + Scorer + out of reach from in here + + ✗ cannot reach in + + + grep scorer → no hits in here + …but System.out is still on this side + + diff --git a/ideas/split-loop-with-lambda/storyboard/svg/d5-overload-test.svg b/ideas/split-loop-with-lambda/storyboard/svg/d5-overload-test.svg new file mode 100644 index 0000000..4fd5332 --- /dev/null +++ b/ideas/split-loop-with-lambda/storyboard/svg/d5-overload-test.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + Step 5 — the overload, and the test goes green + The test passes nameToSalesScore through the seam. Scorer is never touched: deterministic, glanceable. + + + + DailyForecastsTest + capture System.out + name -> Steve ? 9 : 11 + printSalesForecasts(λ) + Approvals.verify(...) + + + + printSalesForecasts(λ) + no-arg overload bakes + in the real λ + collectSalesScores + (names, λ) + print loop + (still in here) + + + + collectSalesScores(λ) + for name : names + scores.put(name, + λ.apply(name)) + return scores + pure ✓ + + + + λ + + + + + Scorer + never called by the test ✗ + + ✗ + + Approved: Steve score: 9 / Julie score: 11 / Francis score: 11 — every run, not just a lucky roll. +