Programming Paradigms
Programming Paradigms¶
Topic: Programming Lesson: 2 of 16 Prerequisites: What Is Programming Objective: Understand different programming paradigms, their principles, and when to use each approach.
What Is a Programming Paradigm?¶
A programming paradigm is a fundamental style or approach to programming. It defines: - How you structure your code - How you think about problems - What concepts and abstractions you use
Think of paradigms as different philosophies for solving problems with code.
Analogy: Just as there are different architectural styles (Gothic, Modernist, Brutalist), there are different programming styles. Each has its strengths, trade-offs, and appropriate use cases.
Imperative Programming¶
Core Idea¶
Tell the computer HOW to do something, step by step. Focus on changing state through explicit commands.
Characteristics¶
- Explicit sequence of statements
- Variables that change over time (mutable state)
- Control flow: loops, conditionals
- Think: "Do this, then do that, then do this other thing"
Example: Summing Array Elements¶
Python:
def sum_array(numbers):
total = 0 # Initial state
for num in numbers:
total = total + num # Mutate state
return total
result = sum_array([1, 2, 3, 4, 5])
print(result) # Output: 15
JavaScript:
function sumArray(numbers) {
let total = 0; // Initial state
for (let i = 0; i < numbers.length; i++) {
total = total + numbers[i]; // Mutate state
}
return total;
}
console.log(sumArray([1, 2, 3, 4, 5])); // Output: 15
Java:
public static int sumArray(int[] numbers) {
int total = 0; // Initial state
for (int num : numbers) {
total = total + num; // Mutate state
}
return total;
}
C++:
int sumArray(const std::vector<int>& numbers) {
int total = 0; // Initial state
for (int num : numbers) {
total = total + num; // Mutate state
}
return total;
}
Focus: We explicitly manage the total variable, updating it with each iteration.
Procedural Programming¶
Core Idea¶
Organize imperative code into procedures (also called functions or subroutines). Structure programs as a collection of procedures that call each other.
Characteristics¶
- Top-down design: break problems into procedures
- Each procedure performs a specific task
- Procedures can call other procedures
- Data and procedures are separate
- Emphasizes modularity and reusability
Example: Structured Program with Functions¶
Python:
def get_numbers():
"""Get numbers from user input"""
return [1, 2, 3, 4, 5]
def calculate_sum(numbers):
"""Calculate sum of numbers"""
total = 0
for num in numbers:
total += num
return total
def calculate_average(numbers):
"""Calculate average of numbers"""
return calculate_sum(numbers) / len(numbers)
def display_results(numbers):
"""Display results"""
total = calculate_sum(numbers)
avg = calculate_average(numbers)
print(f"Sum: {total}")
print(f"Average: {avg}")
def main():
"""Main procedure that orchestrates the program"""
numbers = get_numbers()
display_results(numbers)
# Entry point
main()
C:
#include <stdio.h>
// Procedures as separate functions
int calculate_sum(int numbers[], int size) {
int total = 0;
for (int i = 0; i < size; i++) {
total += numbers[i];
}
return total;
}
double calculate_average(int numbers[], int size) {
return (double)calculate_sum(numbers, size) / size;
}
void display_results(int numbers[], int size) {
int total = calculate_sum(numbers, size);
double avg = calculate_average(numbers, size);
printf("Sum: %d\n", total);
printf("Average: %.2f\n", avg);
}
int main() {
int numbers[] = {1, 2, 3, 4, 5};
int size = 5;
display_results(numbers, size);
return 0;
}
Benefits: Code is organized, reusable, and easier to test. Each procedure has a single responsibility.
Object-Oriented Programming (OOP)¶
Core Idea¶
Organize code around objects — bundles of data (attributes) and behavior (methods). Model real-world entities as objects that interact.
Key Concepts¶
- Encapsulation: Bundling data and methods together; hiding internal details
- Inheritance: Creating new classes based on existing ones
- Polymorphism: Same interface, different implementations
- Abstraction: Defining interfaces without specifying implementation
Example: Modeling a Bank Account¶
Python:
class BankAccount:
"""Represents a bank account"""
def __init__(self, owner, balance=0):
self.owner = owner
self.__balance = balance # Private attribute (encapsulation)
def deposit(self, amount):
"""Deposit money"""
if amount > 0:
self.__balance += amount
return True
return False
def withdraw(self, amount):
"""Withdraw money"""
if 0 < amount <= self.__balance:
self.__balance -= amount
return True
return False
def get_balance(self):
"""Get current balance"""
return self.__balance
class SavingsAccount(BankAccount):
"""Savings account with interest (inheritance)"""
def __init__(self, owner, balance=0, interest_rate=0.02):
super().__init__(owner, balance)
self.interest_rate = interest_rate
def apply_interest(self):
"""Apply interest to balance"""
interest = self.get_balance() * self.interest_rate
self.deposit(interest)
# Usage
account = SavingsAccount("Alice", 1000)
account.deposit(500)
account.apply_interest()
print(f"Balance: ${account.get_balance():.2f}") # Output: Balance: $1530.00
Java:
public class BankAccount {
private String owner;
private double balance; // Encapsulation: private field
public BankAccount(String owner, double balance) {
this.owner = owner;
this.balance = balance;
}
public boolean deposit(double amount) {
if (amount > 0) {
this.balance += amount;
return true;
}
return false;
}
public boolean withdraw(double amount) {
if (amount > 0 && amount <= this.balance) {
this.balance -= amount;
return true;
}
return false;
}
public double getBalance() {
return this.balance;
}
}
// Inheritance
public class SavingsAccount extends BankAccount {
private double interestRate;
public SavingsAccount(String owner, double balance, double interestRate) {
super(owner, balance);
this.interestRate = interestRate;
}
public void applyInterest() {
double interest = getBalance() * interestRate;
deposit(interest);
}
}
C++:
class BankAccount {
private:
std::string owner;
double balance; // Encapsulation
public:
BankAccount(const std::string& owner, double balance = 0)
: owner(owner), balance(balance) {}
bool deposit(double amount) {
if (amount > 0) {
balance += amount;
return true;
}
return false;
}
bool withdraw(double amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
return true;
}
return false;
}
double getBalance() const {
return balance;
}
};
// Inheritance
class SavingsAccount : public BankAccount {
private:
double interestRate;
public:
SavingsAccount(const std::string& owner, double balance = 0, double interestRate = 0.02)
: BankAccount(owner, balance), interestRate(interestRate) {}
void applyInterest() {
double interest = getBalance() * interestRate;
deposit(interest);
}
};
Benefits: Models real-world entities, promotes code reuse through inheritance, encapsulates complexity.
Functional Programming (FP)¶
Core Idea¶
Treat computation as the evaluation of mathematical functions. Avoid changing state and mutable data.
Key Principles¶
- Pure Functions: Same input always produces same output; no side effects
- Immutability: Data cannot be changed after creation
- First-Class Functions: Functions are values (can be passed, returned)
- Higher-Order Functions: Functions that take/return other functions
- Declarative: Express WHAT to compute, not HOW
Example: Imperative vs Functional¶
Imperative approach (Python):
# Mutable state, explicit loops
def get_even_squares(numbers):
result = []
for num in numbers:
if num % 2 == 0:
result.append(num ** 2)
return result
print(get_even_squares([1, 2, 3, 4, 5, 6])) # [4, 16, 36]
Functional approach (Python):
# Immutable, declarative, higher-order functions
def get_even_squares(numbers):
return list(
map(lambda x: x ** 2,
filter(lambda x: x % 2 == 0, numbers))
)
# Or with list comprehension (more Pythonic)
def get_even_squares(numbers):
return [x ** 2 for x in numbers if x % 2 == 0]
print(get_even_squares([1, 2, 3, 4, 5, 6])) # [4, 16, 36]
JavaScript (functional style):
const getEvenSquares = (numbers) =>
numbers
.filter(x => x % 2 === 0) // Keep even numbers
.map(x => x ** 2); // Square them
console.log(getEvenSquares([1, 2, 3, 4, 5, 6])); // [4, 16, 36]
Pure Functions¶
Impure function (has side effects):
total = 0 # External state
def add_to_total(x):
global total
total += x # Modifies external state (side effect)
return total
Pure function (no side effects):
def add(x, y):
return x + y # Only depends on inputs, no external state
Benefits of FP¶
- Predictable: Pure functions always return the same output for the same input
- Testable: Easy to test (no hidden state)
- Parallelizable: No shared state means safe concurrency
- Composable: Combine small functions to build complex behavior
Declarative Programming¶
Core Idea¶
Describe WHAT you want, not HOW to achieve it. The system figures out the implementation.
Examples¶
SQL (declarative query):
-- You describe WHAT you want
SELECT name, age
FROM users
WHERE age > 18
ORDER BY name;
-- You don't specify HOW to:
-- - Scan the table
-- - Apply the filter
-- - Sort the results
-- The database engine decides the execution plan
HTML (declarative structure):
<!-- You describe WHAT the page should look like -->
<html>
<body>
<h1>Welcome</h1>
<p>This is a paragraph.</p>
</body>
</html>
<!-- You don't specify HOW to:
- Render pixels
- Apply default styles
- Handle layout
The browser does that
-->
CSS (declarative styling):
/* Describe WHAT styles you want */
.button {
background-color: blue;
color: white;
padding: 10px;
}
/* Browser handles HOW to apply styles */
Contrast with Imperative (JavaScript):
// Imperative: HOW to create and style a button
const button = document.createElement('button');
button.style.backgroundColor = 'blue';
button.style.color = 'white';
button.style.padding = '10px';
button.textContent = 'Click me';
document.body.appendChild(button);
Logic Programming¶
Core Idea¶
Express logic as facts and rules. The system performs logical inference to answer queries.
Example: Prolog¶
% Facts
parent(tom, bob).
parent(tom, liz).
parent(bob, ann).
parent(bob, pat).
parent(pat, jim).
% Rules
grandparent(X, Z) :- parent(X, Y), parent(Y, Z).
ancestor(X, Y) :- parent(X, Y).
ancestor(X, Y) :- parent(X, Z), ancestor(Z, Y).
% Queries
?- grandparent(tom, ann). % true
?- ancestor(tom, jim). % true
You define WHAT is true, and Prolog figures out HOW to find answers using backtracking and unification.
Use cases: Expert systems, natural language processing, theorem proving.
Event-Driven Programming¶
Core Idea¶
Program flow is determined by events (user actions, messages, sensor inputs). The system responds to events via callbacks or event handlers.
Characteristics¶
- Event loop: Continuously checks for events
- Callbacks/Handlers: Functions executed in response to events
- Asynchronous: Events can occur at any time
Example: GUI Programming¶
JavaScript (browser):
// Event handler
function handleClick(event) {
console.log('Button clicked!');
console.log('Mouse position:', event.clientX, event.clientY);
}
// Register event listener
const button = document.getElementById('myButton');
button.addEventListener('click', handleClick);
// The event loop waits for events and calls handlers
Python (with tkinter):
import tkinter as tk
def on_button_click():
print("Button clicked!")
# Create window
root = tk.Tk()
# Create button with event handler
button = tk.Button(root, text="Click Me", command=on_button_click)
button.pack()
# Start event loop
root.mainloop() # Waits for events (clicks, key presses, etc.)
Use cases: GUIs, web applications, game engines, IoT systems.
Reactive Programming¶
Core Idea¶
Programming with asynchronous data streams. React to changes over time.
Characteristics¶
- Observables: Streams of data/events over time
- Observers: Subscribe to observables and react to emissions
- Operators: Transform, filter, combine streams
Example: RxJS (JavaScript)¶
import { fromEvent, interval } from 'rxjs';
import { map, filter, debounceTime } from 'rxjs/operators';
// Stream of click events
const clicks$ = fromEvent(document, 'click');
// Transform stream: get click positions
clicks$
.pipe(
map(event => ({ x: event.clientX, y: event.clientY }))
)
.subscribe(pos => console.log('Clicked at:', pos));
// Stream of time ticks
const ticks$ = interval(1000); // Every second
ticks$
.pipe(
filter(n => n % 2 === 0) // Only even numbers
)
.subscribe(n => console.log('Even tick:', n));
// Search input with debounce
const searchInput = document.getElementById('search');
const search$ = fromEvent(searchInput, 'input');
search$
.pipe(
map(event => event.target.value),
debounceTime(300) // Wait for 300ms pause
)
.subscribe(query => console.log('Search:', query));
Use cases: Real-time data (stock tickers, chat), user input handling, complex async workflows.
Multi-Paradigm Languages¶
Most modern languages support multiple paradigms, giving you flexibility to choose the best approach for each problem.
Python: Imperative, OOP, Functional¶
# Imperative
total = 0
for i in range(10):
total += i
# Object-Oriented
class Counter:
def __init__(self):
self.count = 0
def increment(self):
self.count += 1
# Functional
from functools import reduce
total = reduce(lambda acc, x: acc + x, range(10), 0)
JavaScript: Imperative, OOP, Functional, Event-Driven¶
// Imperative
let total = 0;
for (let i = 0; i < 10; i++) {
total += i;
}
// Object-Oriented (class syntax)
class Counter {
constructor() {
this.count = 0;
}
increment() {
this.count++;
}
}
// Functional
const total = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
.reduce((acc, x) => acc + x, 0);
// Event-Driven
button.addEventListener('click', () => console.log('Clicked'));
Scala: OOP, Functional¶
// Object-Oriented
class BankAccount(var balance: Double) {
def deposit(amount: Double): Unit = {
balance += amount
}
}
// Functional
val numbers = List(1, 2, 3, 4, 5)
val doubled = numbers.map(_ * 2)
val sum = numbers.reduce(_ + _)
Comparing Paradigms: When to Use Which?¶
Imperative/Procedural¶
When: - Performance-critical code - Low-level systems programming - Algorithms that naturally involve state changes
Example: Device drivers, game engines, embedded systems
Object-Oriented¶
When: - Modeling complex domains with many entities - Code reuse through inheritance - Large teams and codebases (encapsulation helps manage complexity)
Example: Enterprise applications, GUI frameworks, simulations
Functional¶
When: - Data transformations - Concurrent/parallel processing (no shared state) - Mathematical computations - Predictable, testable code
Example: Data pipelines, stream processing, compilers, financial systems
Declarative¶
When: - You want to express intent without implementation details - The framework/library provides efficient implementation
Example: Database queries (SQL), UI markup (HTML/CSS), configuration (YAML)
Event-Driven¶
When: - User interfaces - Asynchronous I/O - Real-time systems
Example: Web apps, mobile apps, IoT, network servers
Trade-Offs¶
| Paradigm | Strengths | Weaknesses |
|---|---|---|
| Imperative | Explicit control, performance | Verbose, harder to reason about state |
| OOP | Models real-world, encapsulation, reuse | Can be over-engineered, inheritance issues |
| Functional | Predictable, testable, concurrency-safe | Learning curve, performance overhead (immutability) |
| Declarative | Concise, clear intent | Less control over execution |
| Event-Driven | Responsive, asynchronous | Can be hard to debug, "callback hell" |
Exercises¶
Exercise 1: Paradigm Recognition¶
Identify the paradigm(s) used in each code snippet:
A)
numbers = [1, 2, 3, 4, 5]
result = sum(filter(lambda x: x % 2 == 0, numbers))
B)
public class Car {
private int speed;
public void accelerate() {
speed += 10;
}
}
C)
document.getElementById('btn').addEventListener('click', function() {
alert('Clicked!');
});
Exercise 2: Imperative to Functional¶
Rewrite this imperative code in a functional style:
def process_data(numbers):
result = []
for num in numbers:
if num > 0:
result.append(num * 2)
return result
Exercise 3: OOP Design¶
Design classes for a simple library system:
- Book: title, author, ISBN
- Member: name, member ID, borrowed books
- Library: collection of books, members, borrow/return methods
Implement in your language of choice. Apply encapsulation, consider inheritance (maybe EBook extends Book?).
Exercise 4: Functional vs OOP¶
Solve this problem using both OOP and functional approaches:
Problem: Calculate statistics (sum, average, max, min) for a list of numbers.
- OOP: Create a
Statisticsclass - Functional: Use pure functions and higher-order functions
Which approach feels more natural? Why?
Exercise 5: Multi-Paradigm¶
Write a program that:
1. Defines a Person class (OOP)
2. Uses a list of Person objects
3. Filters adults (age >= 18) using functional programming
4. Sorts by name using a built-in function
5. Prints results imperatively with a loop
Exercise 6: Event-Driven¶
Write a simple event-driven program (web or GUI): - Create a text input and button - On button click, display the input text in an alert/label - (Bonus) Add a counter that increments with each click
Summary¶
Programming paradigms are different ways of thinking about code:
- Imperative: Step-by-step commands, mutable state
- Procedural: Organizing imperative code into functions
- Object-Oriented: Modeling with objects, encapsulation, inheritance
- Functional: Pure functions, immutability, composition
- Declarative: Describe WHAT, not HOW
- Logic: Facts, rules, queries, inference
- Event-Driven: Responding to events asynchronously
- Reactive: Programming with data streams
Key Insight: No single paradigm is "best" — choose based on the problem, team, and ecosystem. Modern languages support multiple paradigms, giving you flexibility.
Navigation¶
← Previous: What Is Programming | Next: Data Types & Abstraction →