Framed reading 35 minutes + canonical resource

Why Study Programming Languages: Paradigms, Not Syntax

Hook #

You already know how to program. Probably Ruby, maybe Python, some C from earlier in this path. The natural question this course asks is: why learn another language? If you can already solve problems, what does Elixir or SML or Haskell give you? The honest answer isn't "more tools in your toolbox" — that's the marketing version. The real answer is that each paradigm changes what's cheap to express, and changing what's cheap changes what you reach for first. Learning a second paradigm rewires the thinking behind the language you already use. That's the engineering payoff, and it's the case this lesson makes.

What you'll be able to do by the end of this lesson #

  • Distinguish syntax (how a language looks) from paradigm (what kinds of thinking it makes cheap).
  • Name four major programming paradigms — imperative, object-oriented, functional, logical — and what each makes cheap.
  • Articulate why learning a second paradigm changes the way you write code in your first language.
  • Recognise why functional programming has become widely adopted in the last decade after sitting at the margins for fifty years.

A quick try before we start #

Look at this Ruby code:

rubydef sum(numbers)
  total = 0
  numbers.each { |n| total += n }
  total
end

And this:

rubydef sum(numbers)
  numbers.reduce(0, :+)
end

They do the same thing. Both work. Both are Ruby.

Now write down: what's different? Not stylistically — what does each one say about the programmer's mental model?

Predict your answer before reading on. The contrast between these two is the contrast this course is about.

The core idea, from first principles #

A programming paradigm is a style of thinking a language makes natural. It's not the syntax. It's the shape of the solutions you reach for when you write code in that language.

The major paradigms most engineers encounter:

Imperative #

Core idea: the program is a sequence of commands that change state. "First do this. Then do that. Now check this variable."

Cheap to express: mutation, iteration, step-by-step procedures, low-level control over what the machine does.

Languages: C, Assembly, Pascal, Fortran, BASIC, most of what shell scripts do.

The hook from the warm-up: the first Ruby sum function is imperative-flavoured. It declares a total, mutates it in a loop, returns it. The mental model: there's a running total; we're updating it.

Object-oriented #

Core idea: the program is a network of objects that hold state and respond to messages. "Send this object a message; it figures out what to do."

Cheap to express: encapsulation (hide state behind methods), polymorphism (different objects respond differently to the same message), modelling real-world entities.

Languages: Smalltalk, Java, Ruby, C++, Python, Swift, Kotlin.

The case for OOP: when your domain naturally has things with identity (users, accounts, sessions), OOP gives you a natural mapping. Most production application code in the last 40 years has been OOP-flavoured.

Functional #

Core idea: the program is a composition of functions. Functions take inputs and return outputs; they don't have hidden state. Programs are built by combining functions, not by sequencing commands.

Cheap to express: transformations of data (map a sequence; reduce it; filter it), immutability, concurrency (since immutable data has no race conditions), reasoning about correctness (functions you can reason about in isolation).

Languages: Lisp, Scheme, Haskell, ML/SML/OCaml, Erlang/Elixir, Clojure, F#. Plus functional features in Ruby, JavaScript, Python, Java, Kotlin, Swift, Scala.

The hook from the warm-up: the second Ruby sum function is functional-flavoured. It says: "the sum is the result of reducing the list with the + operation starting from 0." The mental model: I'm describing a transformation, not a procedure.

Logical #

Core idea: the program is a set of facts and rules; the runtime infers what follows.

Cheap to express: constraint satisfaction, search, logical inference.

Languages: Prolog, Datalog (used in SQL extensions), Mercury.

Less common in production code than the other three; appears in specific niches (some database query planners, some constraint solvers, some compiler passes).

(The four are not exhaustive — array-oriented (APL/J), concatenative (Forth/Factor), declarative (SQL) also exist. The four cover ~95% of working engineers' exposure.)

So what does "paradigm changes what's cheap" actually mean? #

Consider the problem: given a list of users, find those over 18, get their names, sort alphabetically.

Imperative (C-style pseudocode): filtered = [] for user in users: if user.age >= 18: filtered.append(user) names = [] for user in filtered: names.append(user.name) sort(names) return names

Functional (Elixir): elixir users |> Enum.filter(&(&1.age >= 18)) |> Enum.map(& &1.name) |> Enum.sort()

Both do the same thing. The functional version reads as a description of the transformation. The imperative version reads as a recipe of steps. Once you've internalised the functional style, the imperative version starts to feel like over-specification — you've told the computer how to walk a loop and accumulate, which isn't your actual intent.

The mental shift: you stop thinking in steps and start thinking in transformations. This is what people mean by "functional programming changes how you think." It's not a slogan — it's a measurable change in the shape of the code you reach for first.

Why this matters #

Three reasons learning a second paradigm pays off, even if you never deploy code in the new language:

1. You write better code in your first language. Ruby engineers who learn Elixir write Ruby with more map/filter/reduce and less mutation. C engineers who learn functional thinking write C with more "compute then return" and less "mutate as you go." The first-language code gets cleaner because the second paradigm changed what occurs to you first.

2. You debug other people's code better. Modern codebases mix paradigms — Ruby with functional Enumerable chains; JavaScript with mutable arrays and immutable React state; Rust with traits (OOP-shaped) and algebraic types (functional-shaped). Knowing the paradigms lets you read code in any of these styles fluently.

3. The paradigms tell you what to reach for. Functional thinking is the natural fit for: ETL pipelines, data transformations, concurrent systems, anything where correctness matters more than performance. OOP is the natural fit for: stateful UI, simulations, domain models with identity. Imperative is the natural fit for: tight inner loops, embedded systems, hardware-near code. Once you can recognise which paradigm fits the problem, you pick the right tool faster.

Why functional programming is having its moment now #

Functional programming has been around since the 1950s (Lisp, 1958). Why has it suddenly mainstreamed in the last decade?

Three reasons converged:

Multi-core hardware. Single-thread performance stopped growing in 2005-ish. The path forward was parallelism. Functional programming's immutability eliminates whole classes of concurrency bugs (no shared mutable state → no data races). Suddenly the niche academic paradigm had the property the industry desperately needed.

Distributed systems. Cloud computing made every non-trivial system distributed. Message-passing (Erlang/Elixir's BEAM model) and immutable data (Clojure, functional Ruby/JS) became standard architecture. The languages that had been doing this for 30 years (Erlang since 1986) became suddenly relevant.

Type systems caught up. Modern functional languages (OCaml, Haskell, Rust, Swift, Kotlin) have type systems that catch bugs at compile time — the kind of bugs that dynamic-language production code spends millions of dollars catching in monitoring. The bargain shifted: type-system overhead became worth its weight in fewer night-time pages.

The combination of those three forces is why your tech stack in 2026 looks more functional than it did in 2010. React's useState and useReducer. Rust's Option<T> and Result<T, E>. Kotlin's data classes. Elixir's broad adoption for backend services. JavaScript's map/filter/reduce as the canonical iteration idiom. Each is a piece of functional programming embedded in a multi-paradigm language.

What this course will give you #

By the end of Course 3.4, you'll have:

  • The functional toolkit: pure functions, immutability, higher-order functions, map/filter/reduce, pattern matching, algebraic data types.
  • The Elixir piece: enough Elixir to read OTP-style code, understand BEAM concurrency, and write a small concurrent system.
  • The Ruby-to-Elixir bridge: how the functional bits of Ruby (blocks, procs, Enumerable) translate to Elixir, and what's different about Elixir that Ruby can't do (real OS-level isolated processes; supervisor trees).
  • The mental shift: by Course 3.4's end, when you see a problem, you'll think "is this a transformation pipeline?" before "is this a state machine?" That's the paradigm shift this course is for.

Explain it back to me #

A junior engineer says: "I write Ruby for a living. Why should I spend a quarter learning Elixir? I won't deploy Elixir code at my job."

In your own words, answer in two sentences. Use the words "paradigm" and "first-language." What's the concrete payoff for their Ruby code?

And one more: a teammate says they've heard functional programming is "just using lambdas and avoiding mutation." Untangle this. What's missing from that definition? (Hint: think pattern matching, ADTs, type systems.)

A small exercise #

Three short tasks:

  1. Rewrite imperative as functional. Take this Ruby: ruby def squared_evens(numbers) result = [] numbers.each do |n| if n.even? result << n * n end end result end Rewrite using Enumerable methods (map, select, filter, reject) in a single chain. Compare the line count and what each version says about your mental model.

  2. Predict the paradigm. For each problem, name the paradigm that fits best and justify in one sentence: a. A web server handling 100k concurrent connections. b. A real-time game engine simulating physics at 60 FPS. c. A SQL query planner optimising joins. d. A model of a parking lot with cars entering and leaving.

  3. Find a piece of code you wrote last week. Re-read it. Identify which paradigm dominates. Could you rewrite it in a different paradigm? Would the rewrite be better or worse, and why?

Discussion of (2) a. **Functional + actor model (BEAM).** Concurrency at this scale wants isolation and message passing; Elixir/Erlang are designed exactly for this. b. **Imperative.** Tight inner loops, mutation for performance, hardware-near. Game engines are imperative even when written in C++. c. **Logical / declarative.** SQL itself is declarative; some database planners use Datalog-like rules. d. **Object-oriented.** Cars and parking spaces have identity; OOP gives a natural mapping. (You *could* model it functionally as state transitions, but OOP is the natural fit.)

Where this connects #

Backward: Course 1.3 (Systems Programming) gave you imperative C and a glimpse of Zig (a modern imperative language with functional touches). Course 2.x put you in OOP territory (Ruby data structures). Course 3.1-3.3 stayed mostly paradigm-agnostic — algorithms work in any paradigm. This course is where you meet the functional cousin of everything you've done.

Forward: Lessons 2-5 of this course teach the functional paradigm concretely — pure functions, higher-order functions, pattern matching, BEAM concurrency. Course 5.x will return to PL theory (compiler construction, language design). Course 7.x (Distributed Systems) heavily uses functional thinking for state coordination. Course 11.x (AI / ML) is a mix — TensorFlow is imperative-with-functional-flavour; PyTorch is more functional; modern frameworks (JAX) are fully functional in style.

The zc-concurrent-web-scraper-in-elixir project for this course is your first real Elixir code. Build it after lesson 5 (when you've met BEAM); by then the project will be a concrete payoff for the paradigm shift this course teaches.

Carry forward one mental habit: when you see a piece of code, ask "what does this paradigm make cheap?" The answer is what the code is naturally good at. Code that fights its paradigm — imperative loops where a reduce would do; OOP setters when immutability would simplify — is the code that becomes hard to maintain. Paradigm-aware coding is the meta-skill of senior engineering.

That's the free preview. Sign in to continue this course.

Sign in to continue

New here? Make a desk →