Files
functional-programming-snake/docs/superpowers/specs/2026-10-28-pure-snake-course-design.md
T
2026-10-28 23:31:16 +01:00

9.6 KiB

Pure Snake — a functional programming course (design)

Intent

A self-paced course that teaches functional programming by building a complete, playable Snake game in which everything is pure: the game rules, randomness, rendering, time, keyboard input, and the program itself. Side effects exist only as values (an IO type and event streams) that the student builds from scratch; a single run call at the very edge of the program is the only place anything actually happens.

Why: students who've had one Scala course know map/filter/fold and case classes, but rarely know how to apply them to an interactive, stateful system. The outcome we want is practical: by the end, a student can take a game written the usual way (snake.currentPos = ..., apple.pos = ...) and convert it into a purely functional system — and explain each step of how and why — because they have actually done it, not just read about it.

Audience

  • Bachelor's/master's CS students with one prior Scala course.
  • Working alone at home. Two supported modes:
    1. Solo — the text, tiered hints and tests carry them.
    2. With an AI as a Socratic mentor — the course ships instructions that make an AI assistant guide by questioning rather than handing over answers.
  • Assumed: basic JS syntax familiarity is not required to be strong; Module 1 bridges from Scala.

Goals

  • A fully functioning Snake game at the end, built by the student.
  • Teaching and understanding over speed: each module explains why, not just what.
  • Test suites the student runs against their own code at every step.
  • Links to online sources for every idea used (papers, docs, blog posts, talks).
  • Three tracks: JavaScript (primary), Scala 3, F# — each with its own runnable test suites, so the course is fully doable in any of the three.
  • A transferable method: a capstone where the student converts a different game.

Non-goals

  • Not a "before" stateful version for the student to start from — the student can reason about stateful code themselves. (Stateful snippets may appear briefly as illustration in prose, but there is no stateful codebase to refactor.)
  • No FP libraries in the student's code (no Ramda, RxJS, cats-effect, fs2, FSharp async/observables as the implementation). Production libraries are linked as "what this looks like in the real world".
  • No TypeScript, no bundlers, no frameworks.
  • Not a course on game design or graphics.

Anti-goals (failure even if it "works")

  • Course reads as a fill-in-the-blanks exercise that can be completed by pattern-matching tests without understanding.
  • Impurity smuggled in "just for convenience" (a let that's reassigned, a Math.random() in game logic, addEventListener outside the edge).
  • The Scala/F# tracks feel like second-class afterthoughts with broken or missing tests.
  • AI mentor mode that just dumps solutions.

Constraints and decided tooling

  • JS: modern JavaScript, ES modules, zero dependencies. Tests with Node's built-in runner (node --test). Game renders to an HTML <canvas>; a thin index.html only loads the module.
  • Scala 3: tests with munit, run via scala-cli (no sbt project). Game runs in the browser via Scala.js (scala-cli supports it) on the same canvas page.
  • F#: tests with Expecto via dotnet test (or dotnet run for Expecto). Game runs in the browser via Fable on the same canvas page.
  • Students hand-build IO and streams in all three languages; the APIs have the same shape and names across tracks (adjusted for idiom, e.g. flatMap vs bind), documented side-by-side.
  • Where possible, pure-logic test cases live as shared language-neutral data files (JSON fixtures: given state + input → expected state) consumed by all three suites, so behaviour cannot drift between tracks. Tests for IO/streams are written natively per language but mirror each other one-to-one.
  • Reference solutions ship in a separate solutions/ folder per track; students are told to try first. All test suites must pass against the solutions (this is how the course itself is verified).

Course structure (decisions marked [call] were made by the designer, not the requester)

  • One shared text, side-by-side code. [call] Each module's prose is written once and language-neutral where possible; every code listing, signature and exercise appears in all three languages (JS / Scala / F#) next to each other. Where a concept is language-specific (Module 1, tagged unions in Module 2), the module has a short per-track section. Bridges go from Scala (the shared background) to each track — for F#, "the Scala thing you know is spelled like this in F#".
  • Canonical data model, but designed first by the student. [call] Module 2 has the student sketch their own model and answer design questions before revealing the course's canonical model, which they then adopt and compare with theirs. The canonical model is fixed so that the shared JSON fixtures work identically in every track.
  • One growing codebase per student, per track. [call] The student builds a single project across modules. Each module states which files it touches. If stuck, a student may copy the previous module's checkpoint from solutions/ to continue — tests are organised per module so earlier ones keep passing.
  • All three tracks ship together, complete. [call] Following "not second-class": every module is finished in all three tracks, with tests passing against reference solutions, before the course is called done. No hour budget; depth is whatever the concept needs.
  • Cross-language determinism. The PRNG must give bit-identical sequences in JS, Scala and F# (32-bit integer arithmetic) so fixtures involving apples are shared.

Module sequence

Each module contains: concept text with Scala bridges, learning objectives, exercises with signatures in all three languages, tiered hints (nudge → stronger hint → near-solution), Socratic discussion questions, test suites per track, "further reading" links, and a short "in production" note (cats-effect, fs2, Elm, Elmish, RxJS, etc.).

  1. Setup & how to use the course — tracks, running tests, solo vs. mentor mode.
  2. Purity & immutability in JS for Scala people — referential transparency, const vs. immutable data, Object.freeze, spread copies, functions as values, currying/composition (pipe, compose).
  3. The game as data — Point, Direction, Snake, GameState; algebraic data types as tagged unions in JS (mapping from Scala enum/case classes and F# discriminated unions); smart constructors.
  4. Pure game rules — step : State → State: movement, growth, wall/self collision, illegal 180° turns, eating.
  5. Randomness without side effects — seeded PRNG as seed → [value, seed]; apple placement avoiding the snake; first encounter with the state-passing pattern (naming the State monad).
  6. Folding time — the game as reduce over a list of inputs; deterministic replays; why this makes testing trivial.
  7. Rendering as data — view : State → Scene returning a description of what to draw (rectangles, text), not drawing it.
  8. The IO type — effects as values; of/map/flatMap; monad laws (tested); interpreting a Scene into an IO of canvas operations; a test interpreter so IO is verifiable without a screen.
  9. Event streams — ticks and key presses as values; map/filter/merge/scan built from scratch; streams as descriptions that do nothing until run.
  10. Wiring it together — main is a single IO value: scan(update) over merged input + tick streams, rendered each frame; exactly one run at the edge. Game is playable.
  11. Extensions, all pure — score, game over, restart, pause, speed-up.
  12. Capstone: the method — an explicit recipe for converting a stateful game into a functional one, then a guided conversion of a second game (Pong or Tetris) with lighter scaffolding and its own tests.

Socratic mentor mode

  • A MENTOR.md (with thin AGENTS.md / CLAUDE.md pointers) that instructs an AI assistant to: ask questions before explaining, never write solution code unless the student explicitly gives up on an exercise, use the module's discussion questions and hint tiers in order, check understanding by asking the student to explain/predict, and consult solutions/ only to evaluate the student's work.
  • Each module lists the discussion questions the mentor (or the solo student) should be able to answer before moving on.

Sources

Each module ends with links to the ideas used. Expected anchors include: Hughes "Why Functional Programming Matters"; the Elm Architecture guide; Haskell/Scala IO writings (cats-effect docs, "Functional Programming in Scala"); Conal Elliott on FRP; "Mostly Adequate Guide to FP" (JS); Expecto/munit/Node test docs; Scala.js and Fable docs; PCG/xorshift/mulberry32 PRNG references; Gary Bernhardt "Functional Core, Imperative Shell" (as the contrast to what we're doing). Links must be checked as live at build time.

Left to the builder

  • Exact repository layout, file names, and module file format (Markdown assumed).
  • Exact API names for IO/stream operations, as long as they're consistent across tracks and documented side-by-side.
  • Stream implementation strategy (push vs. pull), with the choice explained to the student.
  • Choice of PRNG algorithm, grid size, tick rate.
  • Which capstone game (Pong or Tetris) — pick whichever teaches the method best.
  • Visual style of the game page (keep it clean and simple).