Lab 13: Functional Programming

Objective

Apply functional programming (FP) principles in JavaScript — pure functions, immutability, higher-order functions, function composition, currying, and partial application — to write predictable, testable, and elegant code.

Background

Functional programming treats computation as the evaluation of mathematical functions, avoiding shared state and mutable data. JavaScript is a multi-paradigm language that fully supports FP patterns alongside OOP. Modern codebases — React, Redux, Ramda, fp-ts — are built on FP principles. Understanding FP makes you a significantly better JavaScript developer.

Time

45 minutes

Prerequisites

  • Lab 03 (Functions & Scope)

  • Lab 04 (Arrays & Objects)

Tools

  • Node.js 20 LTS

  • Docker image: innozverse-js:latest


Lab Instructions

Step 1: Pure Functions & Side Effects

A pure function always returns the same output for the same input and has no side effects.

💡 Pure functions are testable by definition — no mocks, no setup, no teardown. They're also safe to run in parallel, memoize, and compose. The discipline of writing pure functions forces better architecture.

📸 Verified Output:


Step 2: Higher-Order Functions

Functions that take or return other functions. The foundation of FP in JavaScript.

💡 Memoization is a classic FP optimization: cache results of pure functions keyed by arguments. It's safe only for pure functions — impure functions with side effects would return stale cached results.

📸 Verified Output:


Step 3: Function Composition

Combine small, focused functions into larger ones.

💡 pipe vs compose: pipe(f, g, h)(x) = h(g(f(x))) — left to right, readable as a sequence of transformations. compose(f, g, h)(x) = f(g(h(x))) — right to left, mathematical notation. Prefer pipe for readability.

📸 Verified Output:


Step 4: Currying & Partial Application

Currying transforms f(a, b, c) into f(a)(b)(c). Partial application pre-fills some arguments.

💡 Currying enables point-free style — you build specialized functions by partial application rather than anonymous functions. prices.map(tenPercentOff) reads like English. Compare to prices.map(p => discount(0.1, p)) — same result, less expressive.

📸 Verified Output:


Step 5: Immutability — Working Without Mutation

Avoid mutations to prevent bugs in shared state.

💡 Spread for nested objects is verbose but explicit. Libraries like Immer let you write "mutating" code that's actually immutable under the hood — they use Proxy to intercept mutations and produce new objects. For complex state, consider Immer.

📸 Verified Output:


Step 6: Functors & Monads — the Maybe Pattern

A practical introduction to monadic patterns for safe null handling.

💡 The Maybe monad eliminates null reference errors by wrapping values in a container. map applies a function only if the value exists, skipping it on null/undefined. This is the pattern behind Optional in Java, Option in Rust/Scala, and JavaScript's optional chaining ?..

📸 Verified Output:


Step 7: Transducers — Efficient Data Pipeline

Compose transformations that iterate only once, no matter how many steps.

💡 Transducers iterate the source once regardless of pipeline length. With large datasets (millions of rows), they avoid creating N intermediate arrays. Libraries like Ramda and transducers-js bring this to production code.

📸 Verified Output:


Step 8: Putting It Together — Functional Data Processing Pipeline

Build a complete FP-style data analysis pipeline.

💡 This pipeline is entirely composable. Each utility (filterBy, groupBy, sumBy) can be reused in any combination. Adding a new analysis is one more pipe() call. This is the power of FP: small pieces, infinite combinations.

📸 Verified Output:


Verification

Expected: Tech spending and user breakdown printed correctly.

Common Mistakes

Mistake
Fix

Mutating array arguments

Use [...arr], .slice(), or .map() to copy first

Treating impure HOFs as pure

Math.random(), Date.now() inside HOFs = impure

Deep mutation in spread

Spread is shallow — nested objects still share references

Over-engineering with FP

Use FP where it adds clarity; OOP is fine for stateful entities

Forgetting curried function arity

curry uses .length — rest params ...args have length 0

Summary

You've applied pure functions, higher-order functions, pipe/compose, currying, partial application, immutability, the Maybe monad, transducers, and a full FP data pipeline. Functional programming isn't about avoiding all state — it's about being deliberate: isolate side effects, prefer pure functions, and compose small pieces into powerful wholes.

Further Reading

Last updated